task-manager/task_manager_v2.rs

1210 lines
40 KiB
Rust

//! Task Manager - A CLI application for managing tasks with priorities and subtasks.
//!
//! This module provides a complete task management system demonstrating
//! idiomatic Rust patterns including error handling, traits, and collections.
use std::collections::HashMap;
use std::fmt;
use std::io::{self, Write};
// ============================================================================
// Constants
// ============================================================================
/// Maximum tasks before warning about performance.
/// Chosen because HashMap operations remain O(1) up to this scale,
/// and terminal output remains readable.
const MAX_RECOMMENDED_TASKS: usize = 1000;
// ============================================================================
// Enums
// ============================================================================
/// Task priority levels, ordered from lowest to highest urgency.
///
/// Priority affects the urgency score calculation and sort order
/// when listing tasks. Critical tasks are flagged in statistics.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Priority {
Low,
Medium,
High,
Critical,
}
impl Priority {
/// Parses a priority from user input string.
///
/// Accepts both full names and single-letter abbreviations,
/// case-insensitive. Returns `None` for unrecognized input.
///
/// # Examples
/// ```
/// assert_eq!(Priority::from_str("high"), Some(Priority::High));
/// assert_eq!(Priority::from_str("H"), Some(Priority::High));
/// assert_eq!(Priority::from_str("invalid"), None);
/// ```
pub fn from_str(s: &str) -> Option<Priority> {
match s.to_lowercase().as_str() {
"low" | "l" => Some(Priority::Low),
"medium" | "m" => Some(Priority::Medium),
"high" | "h" => Some(Priority::High),
"critical" | "c" => Some(Priority::Critical),
_ => None,
}
}
/// Returns a numeric score for sorting and urgency calculation.
///
/// Higher scores indicate higher priority. Scale is 1-4 to allow
/// multiplication in urgency formulas without overflow concerns.
fn score(&self) -> u8 {
match self {
Priority::Low => 1,
Priority::Medium => 2,
Priority::High => 3,
Priority::Critical => 4,
}
}
}
impl fmt::Display for Priority {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let symbol = match self {
Priority::Low => "[ ]",
Priority::Medium => "[=]",
Priority::High => "[!]",
Priority::Critical => "[*]",
};
write!(f, "{:?} {}", self, symbol)
}
}
/// Task workflow status.
///
/// Status transitions follow a typical kanban flow:
/// Todo -> InProgress -> Done, with Blocked as a side state.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Status {
Todo,
InProgress,
Blocked,
Done,
}
impl fmt::Display for Status {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let (name, symbol) = match self {
Status::Todo => ("Todo", "[ ]"),
Status::InProgress => ("In Progress", "[~]"),
Status::Blocked => ("Blocked", "[x]"),
Status::Done => ("Done", "[v]"),
};
write!(f, "{} {}", name, symbol)
}
}
// ============================================================================
// Error Types
// ============================================================================
/// Errors that can occur during task operations.
///
/// Each variant provides context for user-friendly error messages.
/// Implements std::error::Error for compatibility with error handling crates.
#[derive(Debug)]
pub enum TaskError {
/// Task with the specified ID does not exist.
NotFound(u32),
/// User provided invalid input (with explanation).
InvalidInput(String),
/// Attempted to add a duplicate tag to a task.
DuplicateTag(String),
}
impl fmt::Display for TaskError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
TaskError::NotFound(id) => write!(f, "Task with ID {} not found", id),
TaskError::InvalidInput(msg) => write!(f, "Invalid input: {}", msg),
TaskError::DuplicateTag(tag) => write!(f, "Tag '{}' already exists", tag),
}
}
}
impl std::error::Error for TaskError {}
/// Convenience type alias for Results with TaskError.
pub type Result<T> = std::result::Result<T, TaskError>;
// ============================================================================
// Traits
// ============================================================================
/// Provides a one-line summary of an item for list views.
pub trait Summarize {
fn summary(&self) -> String;
}
/// Calculates an urgency/priority score for sorting.
pub trait Scoreable {
fn calculate_score(&self) -> u32;
}
// ============================================================================
// Core Data Structures
// ============================================================================
/// A task with metadata, subtasks, and tags.
///
/// Tasks are the core entity in the system. Each task has a unique ID
/// assigned by the TaskManager, along with user-defined attributes.
///
/// # Invariants
/// - `id` is unique within a TaskManager instance
/// - `tags` contains no duplicates (enforced by `add_tag`)
#[derive(Debug, Clone)]
pub struct Task {
id: u32,
title: String,
description: Option<String>,
priority: Priority,
status: Status,
tags: Vec<String>,
subtasks: Vec<Subtask>,
}
/// A subtask within a parent task.
///
/// Subtasks are simpler than tasks - they only have a title and
/// completion status. They cannot be nested further.
#[derive(Debug, Clone)]
pub struct Subtask {
title: String,
completed: bool,
}
impl Task {
/// Creates a new task with the given title and priority.
///
/// The task starts in `Todo` status with no description, tags, or subtasks.
///
/// # Arguments
/// * `id` - Unique identifier (assigned by TaskManager)
/// * `title` - Brief description of the task
/// * `priority` - Initial priority level
fn new(id: u32, title: impl Into<String>, priority: Priority) -> Self {
Task {
id,
title: title.into(),
description: None,
priority,
status: Status::Todo,
tags: Vec::new(),
subtasks: Vec::new(),
}
}
/// Adds a tag to this task.
///
/// Tags are case-sensitive and must be unique within a task.
/// Returns an error if the tag already exists.
pub fn add_tag(&mut self, tag: impl Into<String>) -> Result<()> {
let tag = tag.into();
if self.tags.contains(&tag) {
return Err(TaskError::DuplicateTag(tag));
}
self.tags.push(tag);
Ok(())
}
/// Adds a new subtask to this task.
///
/// Subtasks start as incomplete. The order of subtasks is preserved.
pub fn add_subtask(&mut self, title: impl Into<String>) {
self.subtasks.push(Subtask {
title: title.into(),
completed: false,
});
}
/// Marks a subtask as completed by its index.
///
/// # Arguments
/// * `index` - Zero-based index of the subtask
///
/// # Errors
/// Returns `InvalidInput` if the index is out of bounds.
pub fn complete_subtask(&mut self, index: usize) -> Result<()> {
self.subtasks
.get_mut(index)
.ok_or_else(|| {
TaskError::InvalidInput(format!("Subtask index {} out of bounds", index))
})?
.completed = true;
Ok(())
}
/// Calculates the percentage of completed subtasks.
///
/// If there are no subtasks, returns 100% for Done tasks, 0% otherwise.
/// This provides meaningful progress even for tasks without subtasks.
pub fn progress_percentage(&self) -> f64 {
if self.subtasks.is_empty() {
return if self.status == Status::Done {
100.0
} else {
0.0
};
}
let completed = self.subtasks.iter().filter(|s| s.completed).count();
(completed as f64 / self.subtasks.len() as f64) * 100.0
}
/// Returns true if this task is critical and not yet done.
///
/// Used to flag high-priority incomplete work in statistics.
fn is_critical_incomplete(&self) -> bool {
self.priority == Priority::Critical && self.status != Status::Done
}
}
impl Summarize for Task {
fn summary(&self) -> String {
let tags_str = if self.tags.is_empty() {
String::from("none")
} else {
self.tags.join(", ")
};
format!(
"[#{}] {} | Priority: {} | Status: {} | Tags: {} | Progress: {:.0}%",
self.id,
self.title,
self.priority,
self.status,
tags_str,
self.progress_percentage()
)
}
}
impl Scoreable for Task {
/// Calculates urgency score for task prioritization.
///
/// Formula: (priority * 10) + status_penalty + incomplete_subtasks
///
/// This weights priority highest, then penalizes blocked/todo status,
/// then adds urgency for incomplete subtasks. Higher score = more urgent.
fn calculate_score(&self) -> u32 {
let priority_score = self.priority.score() as u32 * 10;
let status_penalty = match self.status {
Status::Done => 0,
Status::InProgress => 5,
Status::Todo => 10,
Status::Blocked => 15, // Blocked items need attention
};
let subtask_bonus = self.subtasks.iter().filter(|s| !s.completed).count() as u32;
priority_score + status_penalty + subtask_bonus
}
}
impl fmt::Display for Task {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "+----------------------------------------------------------------+")?;
writeln!(f, "| Task #{}: {}", self.id, self.title)?;
writeln!(f, "+----------------------------------------------------------------+")?;
writeln!(f, "| Priority: {}", self.priority)?;
writeln!(f, "| Status: {}", self.status)?;
if let Some(ref desc) = self.description {
writeln!(f, "| Description: {}", desc)?;
}
if !self.tags.is_empty() {
writeln!(f, "| Tags: [{}]", self.tags.join(", "))?;
}
if !self.subtasks.is_empty() {
writeln!(f, "| Subtasks:")?;
for (i, subtask) in self.subtasks.iter().enumerate() {
let check = if subtask.completed { "v" } else { " " };
writeln!(f, "| {}. [{}] {}", i + 1, check, subtask.title)?;
}
}
writeln!(f, "| Progress: {:.1}%", self.progress_percentage())?;
writeln!(f, "| Urgency Score: {}", self.calculate_score())?;
write!(f, "+----------------------------------------------------------------+")
}
}
// ============================================================================
// Task Manager
// ============================================================================
/// Manages a collection of tasks with CRUD operations and queries.
///
/// TaskManager owns all tasks and assigns unique IDs. It provides
/// filtering, searching, and statistical aggregation.
///
/// # Example
/// ```
/// let mut manager = TaskManager::new();
/// let id = manager.create_task("My task", Priority::High);
/// manager.update_status(id, Status::InProgress)?;
/// ```
pub struct TaskManager {
tasks: HashMap<u32, Task>,
next_id: u32,
}
impl TaskManager {
/// Creates an empty TaskManager.
pub fn new() -> Self {
TaskManager {
tasks: HashMap::new(),
next_id: 1,
}
}
/// Creates a new task and returns its assigned ID.
///
/// IDs are assigned sequentially starting from 1.
/// Warns if approaching the recommended task limit.
pub fn create_task(&mut self, title: impl Into<String>, priority: Priority) -> u32 {
let id = self.next_id;
self.next_id += 1;
if self.tasks.len() >= MAX_RECOMMENDED_TASKS {
eprintln!(
"Warning: {} tasks exceeds recommended limit of {}",
self.tasks.len(),
MAX_RECOMMENDED_TASKS
);
}
let task = Task::new(id, title, priority);
self.tasks.insert(id, task);
id
}
/// Returns an immutable reference to a task by ID.
pub fn get_task(&self, id: u32) -> Result<&Task> {
self.tasks.get(&id).ok_or(TaskError::NotFound(id))
}
/// Returns a mutable reference to a task by ID.
pub fn get_task_mut(&mut self, id: u32) -> Result<&mut Task> {
self.tasks.get_mut(&id).ok_or(TaskError::NotFound(id))
}
/// Deletes a task and returns it.
pub fn delete_task(&mut self, id: u32) -> Result<Task> {
self.tasks.remove(&id).ok_or(TaskError::NotFound(id))
}
/// Updates a task's status.
pub fn update_status(&mut self, id: u32, status: Status) -> Result<()> {
self.get_task_mut(id)?.status = status;
Ok(())
}
/// Returns all tasks sorted by urgency score (highest first).
pub fn list_all(&self) -> Vec<&Task> {
let mut tasks: Vec<&Task> = self.tasks.values().collect();
tasks.sort_by(|a, b| b.calculate_score().cmp(&a.calculate_score()));
tasks
}
/// Returns tasks matching the given status.
pub fn filter_by_status(&self, status: Status) -> Vec<&Task> {
self.tasks
.values()
.filter(|t| t.status == status)
.collect()
}
/// Returns tasks containing the given tag (case-insensitive).
pub fn filter_by_tag(&self, tag: &str) -> Vec<&Task> {
self.tasks
.values()
.filter(|t| t.tags.iter().any(|t| t.eq_ignore_ascii_case(tag)))
.collect()
}
/// Searches tasks by title and description (case-insensitive substring).
pub fn search(&self, query: &str) -> Vec<&Task> {
let query_lower = query.to_lowercase();
self.tasks
.values()
.filter(|t| {
t.title.to_lowercase().contains(&query_lower)
|| t.description
.as_ref()
.map(|d| d.to_lowercase().contains(&query_lower))
.unwrap_or(false)
})
.collect()
}
/// Computes aggregate statistics across all tasks.
pub fn statistics(&self) -> Statistics {
let total = self.tasks.len();
let by_status = |s: Status| self.tasks.values().filter(|t| t.status == s).count();
let by_priority = |p: Priority| self.tasks.values().filter(|t| t.priority == p).count();
let avg_progress = if total > 0 {
self.tasks
.values()
.map(|t| t.progress_percentage())
.sum::<f64>()
/ total as f64
} else {
0.0
};
let critical_incomplete = self
.tasks
.values()
.filter(|t| t.is_critical_incomplete())
.count();
Statistics {
total,
todo: by_status(Status::Todo),
in_progress: by_status(Status::InProgress),
blocked: by_status(Status::Blocked),
done: by_status(Status::Done),
low_priority: by_priority(Priority::Low),
medium_priority: by_priority(Priority::Medium),
high_priority: by_priority(Priority::High),
critical_priority: by_priority(Priority::Critical),
average_progress: avg_progress,
critical_incomplete,
}
}
}
impl Default for TaskManager {
fn default() -> Self {
Self::new()
}
}
// ============================================================================
// Statistics
// ============================================================================
/// Aggregated statistics across all tasks.
pub struct Statistics {
pub total: usize,
pub todo: usize,
pub in_progress: usize,
pub blocked: usize,
pub done: usize,
pub low_priority: usize,
pub medium_priority: usize,
pub high_priority: usize,
pub critical_priority: usize,
pub average_progress: f64,
pub critical_incomplete: usize,
}
impl fmt::Display for Statistics {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "\n## Task Statistics")?;
writeln!(f, "========================================")?;
writeln!(f, "Total Tasks: {}", self.total)?;
writeln!(f)?;
writeln!(f, "By Status:")?;
writeln!(f, " [ ] Todo: {}", self.todo)?;
writeln!(f, " [~] In Progress: {}", self.in_progress)?;
writeln!(f, " [x] Blocked: {}", self.blocked)?;
writeln!(f, " [v] Done: {}", self.done)?;
writeln!(f)?;
writeln!(f, "By Priority:")?;
writeln!(f, " Low: {}", self.low_priority)?;
writeln!(f, " Medium: {}", self.medium_priority)?;
writeln!(f, " High: {}", self.high_priority)?;
writeln!(f, " Critical: {}", self.critical_priority)?;
writeln!(f)?;
writeln!(f, "Average Progress: {:.1}%", self.average_progress)?;
if self.critical_incomplete > 0 {
writeln!(
f,
"!! Critical tasks incomplete: {}",
self.critical_incomplete
)?;
}
write!(f, "========================================")
}
}
// ============================================================================
// CLI Command Handlers
// ============================================================================
// Each handler is a small function (<50 lines) following the single
// responsibility principle. This makes testing individual commands easy.
/// Result of executing a command.
enum CommandResult {
Continue,
Exit,
}
/// Parses a task ID from a string slice.
fn parse_id(s: Option<&&str>, usage: &str) -> Result<u32> {
s.and_then(|s| s.parse().ok())
.ok_or_else(|| TaskError::InvalidInput(usage.into()))
}
/// Handles the 'add' command.
fn cmd_add(manager: &mut TaskManager, parts: &[&str]) -> Result<CommandResult> {
if parts.len() < 2 {
return Err(TaskError::InvalidInput("Usage: add <title> [priority]".into()));
}
let priority = parts
.get(2)
.and_then(|p| Priority::from_str(p))
.unwrap_or(Priority::Medium);
let id = manager.create_task(parts[1], priority);
println!("[OK] Created task #{}", id);
Ok(CommandResult::Continue)
}
/// Handles the 'list' command.
fn cmd_list(manager: &TaskManager) -> Result<CommandResult> {
let tasks = manager.list_all();
if tasks.is_empty() {
println!("No tasks found. Use 'demo' to load sample data.");
} else {
println!("\n## All Tasks (sorted by urgency):\n");
for task in tasks {
println!(" {}", task.summary());
}
}
Ok(CommandResult::Continue)
}
/// Handles the 'show' command.
fn cmd_show(manager: &TaskManager, parts: &[&str]) -> Result<CommandResult> {
let id = parse_id(parts.get(1), "Usage: show <id>")?;
let task = manager.get_task(id)?;
println!("{}", task);
Ok(CommandResult::Continue)
}
/// Handles status update commands (done, progress, block).
fn cmd_update_status(
manager: &mut TaskManager,
parts: &[&str],
status: Status,
label: &str,
) -> Result<CommandResult> {
let usage = format!("Usage: {} <id>", label);
let id = parse_id(parts.get(1), &usage)?;
manager.update_status(id, status)?;
println!("[OK] Task #{} marked as {}", id, label);
Ok(CommandResult::Continue)
}
/// Handles the 'delete' command.
fn cmd_delete(manager: &mut TaskManager, parts: &[&str]) -> Result<CommandResult> {
let id = parse_id(parts.get(1), "Usage: delete <id>")?;
let task = manager.delete_task(id)?;
println!("[OK] Deleted task: {}", task.title);
Ok(CommandResult::Continue)
}
/// Handles the 'tag' command.
fn cmd_tag(manager: &mut TaskManager, parts: &[&str]) -> Result<CommandResult> {
if parts.len() < 3 {
return Err(TaskError::InvalidInput("Usage: tag <id> <tag>".into()));
}
let id: u32 = parts[1]
.parse()
.map_err(|_| TaskError::InvalidInput("Invalid ID".into()))?;
manager.get_task_mut(id)?.add_tag(parts[2])?;
println!("[OK] Added tag '{}' to task #{}", parts[2], id);
Ok(CommandResult::Continue)
}
/// Handles the 'subtask' command.
fn cmd_subtask(manager: &mut TaskManager, parts: &[&str]) -> Result<CommandResult> {
if parts.len() < 3 {
return Err(TaskError::InvalidInput("Usage: subtask <id> <title>".into()));
}
let id: u32 = parts[1]
.parse()
.map_err(|_| TaskError::InvalidInput("Invalid ID".into()))?;
manager.get_task_mut(id)?.add_subtask(parts[2]);
println!("[OK] Added subtask to task #{}", id);
Ok(CommandResult::Continue)
}
/// Handles the 'check' command.
fn cmd_check(manager: &mut TaskManager, parts: &[&str]) -> Result<CommandResult> {
if parts.len() < 3 {
return Err(TaskError::InvalidInput("Usage: check <id> <subtask#>".into()));
}
let id: u32 = parts[1]
.parse()
.map_err(|_| TaskError::InvalidInput("Invalid ID".into()))?;
let subtask_idx: usize = parts[2]
.parse::<usize>()
.map_err(|_| TaskError::InvalidInput("Invalid subtask number".into()))?
.saturating_sub(1); // Convert 1-based to 0-based index
manager.get_task_mut(id)?.complete_subtask(subtask_idx)?;
println!("[OK] Completed subtask");
Ok(CommandResult::Continue)
}
/// Handles the 'desc' command.
fn cmd_desc(manager: &mut TaskManager, parts: &[&str]) -> Result<CommandResult> {
if parts.len() < 3 {
return Err(TaskError::InvalidInput(
"Usage: desc <id> <description>".into(),
));
}
let id: u32 = parts[1]
.parse()
.map_err(|_| TaskError::InvalidInput("Invalid ID".into()))?;
manager.get_task_mut(id)?.description = Some(parts[2].into());
println!("[OK] Updated description for task #{}", id);
Ok(CommandResult::Continue)
}
/// Handles the 'filter' command.
fn cmd_filter(manager: &TaskManager, parts: &[&str]) -> Result<CommandResult> {
let status_str = parts.get(1).unwrap_or(&"");
let tasks = match status_str.to_lowercase().as_str() {
"todo" => manager.filter_by_status(Status::Todo),
"progress" | "inprogress" => manager.filter_by_status(Status::InProgress),
"blocked" => manager.filter_by_status(Status::Blocked),
"done" => manager.filter_by_status(Status::Done),
_ => {
return Err(TaskError::InvalidInput(
"Usage: filter <todo|progress|blocked|done>".into(),
));
}
};
println!("\nFiltered tasks:");
if tasks.is_empty() {
println!(" No tasks match the filter.");
} else {
for task in tasks {
println!(" {}", task.summary());
}
}
Ok(CommandResult::Continue)
}
/// Handles the 'search' command.
fn cmd_search(manager: &TaskManager, parts: &[&str]) -> Result<CommandResult> {
if parts.len() < 2 {
return Err(TaskError::InvalidInput("Usage: search <query>".into()));
}
let query = parts[1..].join(" ");
let tasks = manager.search(&query);
println!("\nSearch results for '{}':", query);
if tasks.is_empty() {
println!(" No tasks found.");
} else {
for task in tasks {
println!(" {}", task.summary());
}
}
Ok(CommandResult::Continue)
}
// ============================================================================
// CLI Infrastructure
// ============================================================================
fn print_help() {
println!("\n## Task Manager Commands:");
println!("==========================================================");
println!(" add <title> [priority] - Add a new task (priority: l/m/h/c)");
println!(" list - List all tasks");
println!(" show <id> - Show task details");
println!(" done <id> - Mark task as done");
println!(" progress <id> - Mark task as in progress");
println!(" block <id> - Mark task as blocked");
println!(" delete <id> - Delete a task");
println!(" tag <id> <tag> - Add tag to task");
println!(" subtask <id> <title> - Add subtask");
println!(" check <id> <subtask#> - Complete subtask");
println!(" desc <id> <description> - Add description");
println!(" filter <status> - Filter by status");
println!(" search <query> - Search tasks");
println!(" stats - Show statistics");
println!(" demo - Load demo data");
println!(" help - Show this help");
println!(" quit - Exit");
println!("==========================================================\n");
}
/// Loads sample tasks for demonstration and testing.
fn load_demo_data(manager: &mut TaskManager) {
let id1 = manager.create_task("Deploy production server", Priority::Critical);
if let Ok(task) = manager.get_task_mut(id1) {
task.description =
Some("Deploy the new version to production with zero downtime".into());
let _ = task.add_tag("devops");
let _ = task.add_tag("urgent");
task.add_subtask("Backup database");
task.add_subtask("Run migrations");
task.add_subtask("Deploy new containers");
task.add_subtask("Verify health checks");
let _ = task.complete_subtask(0);
task.status = Status::InProgress;
}
let id2 = manager.create_task("Fix authentication bug", Priority::High);
if let Ok(task) = manager.get_task_mut(id2) {
task.description = Some("Users getting logged out unexpectedly".into());
let _ = task.add_tag("bug");
let _ = task.add_tag("security");
task.status = Status::Blocked;
}
let id3 = manager.create_task("Implement user dashboard", Priority::Medium);
if let Ok(task) = manager.get_task_mut(id3) {
let _ = task.add_tag("feature");
let _ = task.add_tag("frontend");
task.add_subtask("Design mockups");
task.add_subtask("Create React components");
task.add_subtask("Add API integration");
task.add_subtask("Write tests");
let _ = task.complete_subtask(0);
let _ = task.complete_subtask(1);
}
let id4 = manager.create_task("Update documentation", Priority::Low);
if let Ok(task) = manager.get_task_mut(id4) {
let _ = task.add_tag("docs");
}
let id5 = manager.create_task("Setup CI/CD pipeline", Priority::High);
if let Ok(task) = manager.get_task_mut(id5) {
let _ = task.add_tag("devops");
task.add_subtask("Configure GitHub Actions");
task.add_subtask("Add test stage");
task.add_subtask("Add deploy stage");
let _ = task.complete_subtask(0);
let _ = task.complete_subtask(1);
let _ = task.complete_subtask(2);
task.status = Status::Done;
}
println!("[OK] Loaded 5 demo tasks!");
}
/// Routes a command to the appropriate handler.
///
/// This dispatcher keeps each command handler small and testable.
/// Adding new commands requires only adding a new match arm and handler.
fn execute_command(manager: &mut TaskManager, parts: &[&str]) -> Result<CommandResult> {
let command = parts[0].to_lowercase();
match command.as_str() {
"quit" | "q" | "exit" => {
println!("Goodbye!");
Ok(CommandResult::Exit)
}
"help" | "h" | "?" => {
print_help();
Ok(CommandResult::Continue)
}
"demo" => {
load_demo_data(manager);
Ok(CommandResult::Continue)
}
"add" => cmd_add(manager, parts),
"list" | "ls" => cmd_list(manager),
"show" => cmd_show(manager, parts),
"done" => cmd_update_status(manager, parts, Status::Done, "done"),
"progress" => cmd_update_status(manager, parts, Status::InProgress, "in progress"),
"block" => cmd_update_status(manager, parts, Status::Blocked, "blocked"),
"delete" | "rm" => cmd_delete(manager, parts),
"tag" => cmd_tag(manager, parts),
"subtask" => cmd_subtask(manager, parts),
"check" => cmd_check(manager, parts),
"desc" => cmd_desc(manager, parts),
"filter" => cmd_filter(manager, parts),
"search" => cmd_search(manager, parts),
"stats" => {
println!("{}", manager.statistics());
Ok(CommandResult::Continue)
}
_ => Err(TaskError::InvalidInput(format!(
"Unknown command: '{}'. Type 'help' for commands.",
command
))),
}
}
/// Main REPL loop for the CLI.
fn run_interactive(manager: &mut TaskManager) {
println!("\n## Welcome to Rust Task Manager!\n");
print_help();
loop {
print!("> ");
io::stdout().flush().unwrap();
let mut input = String::new();
if io::stdin().read_line(&mut input).is_err() {
println!("Error reading input");
continue;
}
let parts: Vec<&str> = input.trim().splitn(3, ' ').collect();
if parts.is_empty() || parts[0].is_empty() {
continue;
}
match execute_command(manager, &parts) {
Ok(CommandResult::Exit) => break,
Ok(CommandResult::Continue) => {}
Err(e) => println!("[ERROR] {}", e),
}
}
}
// ============================================================================
// Entry Point
// ============================================================================
fn main() {
let mut manager = TaskManager::new();
run_interactive(&mut manager);
}
// ============================================================================
// Unit Tests
// ============================================================================
#[cfg(test)]
mod tests {
use super::*;
// -------------------------------------------------------------------------
// Priority Tests
// -------------------------------------------------------------------------
#[test]
fn test_priority_from_str_full_names() {
assert_eq!(Priority::from_str("low"), Some(Priority::Low));
assert_eq!(Priority::from_str("medium"), Some(Priority::Medium));
assert_eq!(Priority::from_str("high"), Some(Priority::High));
assert_eq!(Priority::from_str("critical"), Some(Priority::Critical));
}
#[test]
fn test_priority_from_str_abbreviations() {
assert_eq!(Priority::from_str("l"), Some(Priority::Low));
assert_eq!(Priority::from_str("m"), Some(Priority::Medium));
assert_eq!(Priority::from_str("h"), Some(Priority::High));
assert_eq!(Priority::from_str("c"), Some(Priority::Critical));
}
#[test]
fn test_priority_from_str_case_insensitive() {
assert_eq!(Priority::from_str("HIGH"), Some(Priority::High));
assert_eq!(Priority::from_str("High"), Some(Priority::High));
assert_eq!(Priority::from_str("H"), Some(Priority::High));
}
#[test]
fn test_priority_from_str_invalid() {
assert_eq!(Priority::from_str("invalid"), None);
assert_eq!(Priority::from_str(""), None);
assert_eq!(Priority::from_str("x"), None);
}
#[test]
fn test_priority_score_ordering() {
assert!(Priority::Low.score() < Priority::Medium.score());
assert!(Priority::Medium.score() < Priority::High.score());
assert!(Priority::High.score() < Priority::Critical.score());
}
// -------------------------------------------------------------------------
// Task Tests
// -------------------------------------------------------------------------
#[test]
fn test_task_new_defaults() {
let task = Task::new(1, "Test task", Priority::Medium);
assert_eq!(task.id, 1);
assert_eq!(task.title, "Test task");
assert_eq!(task.priority, Priority::Medium);
assert_eq!(task.status, Status::Todo);
assert!(task.description.is_none());
assert!(task.tags.is_empty());
assert!(task.subtasks.is_empty());
}
#[test]
fn test_task_add_tag_success() {
let mut task = Task::new(1, "Test", Priority::Low);
assert!(task.add_tag("rust").is_ok());
assert_eq!(task.tags, vec!["rust"]);
}
#[test]
fn test_task_add_tag_duplicate_error() {
let mut task = Task::new(1, "Test", Priority::Low);
task.add_tag("rust").unwrap();
let result = task.add_tag("rust");
assert!(result.is_err());
match result {
Err(TaskError::DuplicateTag(tag)) => assert_eq!(tag, "rust"),
_ => panic!("Expected DuplicateTag error"),
}
}
#[test]
fn test_task_add_subtask() {
let mut task = Task::new(1, "Test", Priority::Low);
task.add_subtask("Subtask 1");
task.add_subtask("Subtask 2");
assert_eq!(task.subtasks.len(), 2);
assert_eq!(task.subtasks[0].title, "Subtask 1");
assert!(!task.subtasks[0].completed);
}
#[test]
fn test_task_complete_subtask_success() {
let mut task = Task::new(1, "Test", Priority::Low);
task.add_subtask("Subtask 1");
assert!(task.complete_subtask(0).is_ok());
assert!(task.subtasks[0].completed);
}
#[test]
fn test_task_complete_subtask_out_of_bounds() {
let mut task = Task::new(1, "Test", Priority::Low);
task.add_subtask("Subtask 1");
let result = task.complete_subtask(5);
assert!(result.is_err());
}
#[test]
fn test_task_progress_no_subtasks_todo() {
let task = Task::new(1, "Test", Priority::Low);
assert_eq!(task.progress_percentage(), 0.0);
}
#[test]
fn test_task_progress_no_subtasks_done() {
let mut task = Task::new(1, "Test", Priority::Low);
task.status = Status::Done;
assert_eq!(task.progress_percentage(), 100.0);
}
#[test]
fn test_task_progress_with_subtasks() {
let mut task = Task::new(1, "Test", Priority::Low);
task.add_subtask("Sub 1");
task.add_subtask("Sub 2");
task.add_subtask("Sub 3");
task.add_subtask("Sub 4");
task.complete_subtask(0).unwrap();
task.complete_subtask(1).unwrap();
assert_eq!(task.progress_percentage(), 50.0);
}
#[test]
fn test_task_urgency_score() {
let mut task = Task::new(1, "Test", Priority::High);
task.status = Status::Todo;
task.add_subtask("Incomplete subtask");
// priority(3) * 10 + status_todo(10) + 1 incomplete subtask = 41
assert_eq!(task.calculate_score(), 41);
}
// -------------------------------------------------------------------------
// TaskManager Tests
// -------------------------------------------------------------------------
#[test]
fn test_manager_create_task_assigns_sequential_ids() {
let mut manager = TaskManager::new();
let id1 = manager.create_task("Task 1", Priority::Low);
let id2 = manager.create_task("Task 2", Priority::Low);
let id3 = manager.create_task("Task 3", Priority::Low);
assert_eq!(id1, 1);
assert_eq!(id2, 2);
assert_eq!(id3, 3);
}
#[test]
fn test_manager_get_task_success() {
let mut manager = TaskManager::new();
let id = manager.create_task("Test", Priority::High);
let task = manager.get_task(id).unwrap();
assert_eq!(task.title, "Test");
assert_eq!(task.priority, Priority::High);
}
#[test]
fn test_manager_get_task_not_found() {
let manager = TaskManager::new();
let result = manager.get_task(999);
assert!(result.is_err());
match result {
Err(TaskError::NotFound(id)) => assert_eq!(id, 999),
_ => panic!("Expected NotFound error"),
}
}
#[test]
fn test_manager_delete_task() {
let mut manager = TaskManager::new();
let id = manager.create_task("To delete", Priority::Low);
let deleted = manager.delete_task(id).unwrap();
assert_eq!(deleted.title, "To delete");
assert!(manager.get_task(id).is_err());
}
#[test]
fn test_manager_update_status() {
let mut manager = TaskManager::new();
let id = manager.create_task("Test", Priority::Low);
manager.update_status(id, Status::InProgress).unwrap();
assert_eq!(manager.get_task(id).unwrap().status, Status::InProgress);
}
#[test]
fn test_manager_list_all_sorted_by_urgency() {
let mut manager = TaskManager::new();
manager.create_task("Low priority", Priority::Low);
manager.create_task("Critical priority", Priority::Critical);
manager.create_task("Medium priority", Priority::Medium);
let tasks = manager.list_all();
assert_eq!(tasks[0].priority, Priority::Critical);
assert_eq!(tasks[2].priority, Priority::Low);
}
#[test]
fn test_manager_filter_by_status() {
let mut manager = TaskManager::new();
let id1 = manager.create_task("Task 1", Priority::Low);
let id2 = manager.create_task("Task 2", Priority::Low);
manager.create_task("Task 3", Priority::Low);
manager.update_status(id1, Status::Done).unwrap();
manager.update_status(id2, Status::Done).unwrap();
let done_tasks = manager.filter_by_status(Status::Done);
assert_eq!(done_tasks.len(), 2);
}
#[test]
fn test_manager_search_by_title() {
let mut manager = TaskManager::new();
manager.create_task("Fix authentication bug", Priority::High);
manager.create_task("Add new feature", Priority::Medium);
manager.create_task("Fix database bug", Priority::High);
let results = manager.search("fix");
assert_eq!(results.len(), 2);
}
#[test]
fn test_manager_search_case_insensitive() {
let mut manager = TaskManager::new();
manager.create_task("Authentication Module", Priority::High);
let results = manager.search("authentication");
assert_eq!(results.len(), 1);
}
#[test]
fn test_manager_statistics() {
let mut manager = TaskManager::new();
let id1 = manager.create_task("Task 1", Priority::Critical);
let id2 = manager.create_task("Task 2", Priority::High);
manager.create_task("Task 3", Priority::Low);
manager.update_status(id1, Status::Done).unwrap();
manager.update_status(id2, Status::InProgress).unwrap();
let stats = manager.statistics();
assert_eq!(stats.total, 3);
assert_eq!(stats.done, 1);
assert_eq!(stats.in_progress, 1);
assert_eq!(stats.todo, 1);
assert_eq!(stats.critical_priority, 1);
assert_eq!(stats.critical_incomplete, 0); // Critical task is done
}
// -------------------------------------------------------------------------
// Command Handler Tests
// -------------------------------------------------------------------------
#[test]
fn test_parse_id_success() {
let parts = vec!["show", "42"];
let id = parse_id(parts.get(1), "Usage: show <id>").unwrap();
assert_eq!(id, 42);
}
#[test]
fn test_parse_id_missing() {
let parts: Vec<&str> = vec!["show"];
let result = parse_id(parts.get(1), "Usage: show <id>");
assert!(result.is_err());
}
#[test]
fn test_parse_id_invalid() {
let parts = vec!["show", "abc"];
let result = parse_id(parts.get(1), "Usage: show <id>");
assert!(result.is_err());
}
}