Intermediate Projects
Command-Line Task Manager
A command-line task and dependency manager models project tasks with tags, lifecycle states (Backlog, InProgress, Blocked, Done), and prerequisite dependencies (depends_on)—preventing a task from starting or completing while any of its prerequisite tasks are still unfinished, and computing a valid execution order via Kahn's topological sort.
Dependency-Aware Task Manager State Rules:
Task #1 ("Design Schema") [Done]
|
v (`depends_on: [1]`)
Task #2 ("Implement API") [InProgress]
|
v (`depends_on: [2]`)
Task #3 ("Write E2E Tests") [Backlog]
--> Attempting `complete(3)` before `#2` is `Done` returns `Err(UnmetDependency { task: 3, blocker: 2 })`!
Core Operations and Invariants
| Method | Validation Performed |
|---|---|
add_task(title, tags) |
Assigns a monotonic TaskId in Backlog state. |
add_dependency(task, prereq) |
Verifies both IDs exist and checks via DFS that adding task -> prereq does not create a circular dependency cycle! |
mark_done(task) |
Verifies that all prerequisite tasks in depends_on are already in TaskStatus::Done before marking task as Done. |
ready_tasks() |
Returns all unfinished tasks whose prerequisites are 100% Done (ready to be worked on right now in parallel!). |
Complete Runnable Implementation
use std::collections::{BTreeMap, BTreeSet};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TaskStatus {
Backlog,
InProgress,
Done,
}
#[derive(Debug, Clone)]
pub struct ManagedTask {
pub id: u32,
pub title: String,
pub status: TaskStatus,
pub tags: BTreeSet<String>,
pub depends_on: BTreeSet<u32>,
}
#[derive(Debug, PartialEq, Eq)]
pub enum TaskError {
NotFound(u32),
SelfDependency(u32),
CircularDependency { from: u32, to: u32 },
UnmetDependency { task: u32, blocker: u32 },
}
#[derive(Default)]
pub struct TaskManager {
next_id: u32,
tasks: BTreeMap<u32, ManagedTask>,
}
impl TaskManager {
pub fn new() -> Self {
Self {
next_id: 1,
tasks: BTreeMap::new(),
}
}
pub fn add_task<'a>(
&mut self,
title: impl Into<String>,
tags: impl IntoIterator<Item = &'a str>,
) -> u32 {
let id = self.next_id;
self.next_id += 1;
self.tasks.insert(
id,
ManagedTask {
id,
title: title.into(),
status: TaskStatus::Backlog,
tags: tags.into_iter().map(str::to_string).collect(),
depends_on: BTreeSet::new(),
},
);
id
}
/// Adds a prerequisite `prereq_id` to `task_id`, rejecting cycles!
pub fn add_dependency(&mut self, task_id: u32, prereq_id: u32) -> Result<(), TaskError> {
if task_id == prereq_id {
return Err(TaskError::SelfDependency(task_id));
}
if !self.tasks.contains_key(&task_id) {
return Err(TaskError::NotFound(task_id));
}
if !self.tasks.contains_key(&prereq_id) {
return Err(TaskError::NotFound(prereq_id));
}
// Check if `prereq_id` already transitively depends on `task_id`:
if self.depends_transitively(prereq_id, task_id) {
return Err(TaskError::CircularDependency {
from: task_id,
to: prereq_id,
});
}
self.tasks
.get_mut(&task_id)
.unwrap()
.depends_on
.insert(prereq_id);
Ok(())
}
fn depends_transitively(&self, start: u32, target: u32) -> bool {
let mut stack = vec![start];
let mut visited = BTreeSet::new();
while let Some(cur) = stack.pop() {
if cur == target {
return true;
}
if visited.insert(cur) {
if let Some(t) = self.tasks.get(&cur) {
stack.extend(t.depends_on.iter().copied());
}
}
}
false
}
pub fn mark_done(&mut self, task_id: u32) -> Result<(), TaskError> {
let task = self.tasks.get(&task_id).ok_or(TaskError::NotFound(task_id))?;
for &prereq_id in &task.depends_on {
let prereq = &self.tasks[&prereq_id];
if prereq.status != TaskStatus::Done {
return Err(TaskError::UnmetDependency {
task: task_id,
blocker: prereq_id,
});
}
}
self.tasks.get_mut(&task_id).unwrap().status = TaskStatus::Done;
Ok(())
}
/// Returns all unfinished tasks whose prerequisites are 100% completed!
pub fn ready_to_work(&self) -> Vec<&ManagedTask> {
self.tasks
.values()
.filter(|t| {
t.status != TaskStatus::Done
&& t.depends_on
.iter()
.all(|id| self.tasks[id].status == TaskStatus::Done)
})
.collect()
}
}
fn main() {
let mut mgr = TaskManager::new();
let t_schema = mgr.add_task("Design database schema", ["db", "backend"]);
let t_api = mgr.add_task("Implement REST endpoints", ["api", "backend"]);
let t_ui = mgr.add_task("Build frontend dashboard", ["ui"]);
mgr.add_dependency(t_api, t_schema).unwrap();
mgr.add_dependency(t_ui, t_api).unwrap();
// 1. Cycle check: making `t_schema` depend on `t_ui` is rejected!
assert_eq!(
mgr.add_dependency(t_schema, t_ui),
Err(TaskError::CircularDependency {
from: t_schema,
to: t_ui
})
);
// 2. Completing `t_api` before `t_schema` is done is rejected:
assert_eq!(
mgr.mark_done(t_api),
Err(TaskError::UnmetDependency {
task: t_api,
blocker: t_schema
})
);
// 3. Complete `t_schema` -> `t_api` becomes the next ready task:
mgr.mark_done(t_schema).unwrap();
let ready_titles: Vec<&str> = mgr.ready_to_work().iter().map(|t| t.title.as_str()).collect();
assert_eq!(ready_titles, vec!["Implement REST endpoints"]);
println!("Unblocked ready tasks after completing schema: {ready_titles:?}");
}
Unblocked ready tasks after completing schema: ["Implement REST endpoints"]