Milestone Project: Building a Task Management App

by Solomon Eseme

.

Updated Sat Sep 02 2023

Milestone Project: Building a Task Management App

We'll build a task management application using Rust in this exciting milestone project. This project will encompass concepts covered in the previous chapters, including structuring your code, error handling, file I/O, and more. Following these steps, you'll create a functional task manager allowing users to add, list, and mark tasks as completed.

Note: The complete implementation is done at the end, but you are given free rein to use your newly acquired knowledge to implement the functions.

Step 1: Project Setup

  1. Create a new Rust project using Cargo:

    cargo new task_manager
    
  2. Navigate to the project directory:

    cd task_manager
    

Step 2: Define Task Struct

  1. Open the src/main.rs file and define a Task struct:

    struct Task {
        id: u32,
        title: String,
        completed: bool,
    }
    

Step 3: Implement Basic Functions

  1. Inside the main function, create an empty vector to store tasks:

    fn main() {
        let mut tasks: Vec<Task> = Vec::new();
        // ...
    }
    
  2. Implement a function to add tasks:

    fn add_task(tasks: &mut Vec<Task>, title: &str) {
        let id = (tasks.len() + 1) as u32;
        let task = Task { id, title: title.to_string(), completed: false };
        tasks.push(task);
    }
    
  3. Implement a function to list tasks:

    fn list_tasks(tasks: &[Task]) {
        for task in tasks {
            let status = if task.completed { "[X]" } else { "[ ]" };
            println!("{} {}: {}", status, task.id, task.title);
        }
    }
    
  4. Implement a function to mark tasks as completed:

    fn complete_task(tasks: &mut Vec<Task>, id: u32) -> Result<(), String> {
        if let Some(task) = tasks.iter_mut().find(|t| t.id == id) {
            task.completed = true;
            Ok(())
        } else {
            Err("Task not found".to_string())
        }
    }
    

Step 4: Implement File I/O

  1. Create a tasks.txt file in the project directory with task data.

  2. Implement a function to read tasks from the file:

    fn read_tasks_from_file(file_path: &str) -> Result<Vec<Task>, std::io::Error> {
        // Read tasks from the file and return them as a Vec<Task>
    }
    
  3. Implement a function to write tasks to the file:

    fn write_tasks_to_file(file_path: &str, tasks: &[Task]) -> Result<(), std::io::Error> {
        // Write tasks to the file
    }
    

Step 5: Integrate Error Handling

  1. Update functions to return Result with custom error types.

  2. Implement the main function:

    fn main() -> Result<(), Box<dyn std::error::Error>> {
        // Load tasks from the file
        let tasks = read_tasks_from_file("tasks.txt")?;
    
        // ...
    
        // Save tasks to the file
        write_tasks_to_file("tasks.txt", &tasks)?;
    
        Ok(())
    }
    

Step 6: User Interaction

  1. Implement a simple command-line interface using the std::env module to handle user input.

  2. Parse user commands to add, list, and mark tasks as completed.

Step 7: Run and Test

  1. Run the project using Cargo:

    cargo run
    
  2. Test your task manager by adding, listing, and completing tasks using the command-line interface.

Step 8: Enhancements

  1. Consider enhancing the project by adding more features like editing, deleting, and prioritizing tasks.

  2. Explore additional Rust libraries, like colored terminal output or interactive input libraries, to improve the user experience.

Congratulations! You've successfully built a task management application using Rust. This milestone project combines concepts learned throughout the tutorial to create a practical and functional application. Keep exploring Rust and applying these concepts to more ambitious projects.

Happy coding!

Appendix

Here's the complete code for the task management application, as described in the milestone project:

use std::fs;
use std::io::{Read, Write};
use std::env;
use serde_json

struct Task {
    id: u32,
    title: String,
    completed: bool,
}

fn add_task(tasks: &mut Vec<Task>, title: &str) {
    let id = (tasks.len() + 1) as u32;
    let task = Task {
        id,
        title: title.to_string(),
        completed: false,
    };
    tasks.push(task);
}

fn list_tasks(tasks: &[Task]) {
    for task in tasks {
        let status = if task.completed { "[X]" } else { "[ ]" };
        println!("{} {}: {}", status, task.id, task.title);
    }
}

fn complete_task(tasks: &mut Vec<Task>, id: u32) -> Result<(), String> {
    if let Some(task) = tasks.iter_mut().find(|t| t.id == id) {
        task.completed = true;
        Ok(())
    } else {
        Err("Task not found".to_string())
    }
}

fn read_tasks_from_file(file_path: &str) -> Result<Vec<Task>, std::io::Error> {
    let mut content = String::new();
    let mut file = fs::File::open(file_path)?;
    file.read_to_string(&mut content)?;

    let tasks: Vec<Task> = serde_json::from_str(&content)?;
    Ok(tasks)
}

fn write_tasks_to_file(file_path: &str, tasks: &[Task]) -> Result<(), std::io::Error> {
    let json_data = serde_json::to_string(tasks)?;
    let mut file = fs::File::create(file_path)?;
    file.write_all(json_data.as_bytes())?;
    Ok(())
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let args: Vec<String> = env::args().collect();
    let file_path = "tasks.txt";

    let mut tasks = if fs::metadata(file_path).is_ok() {
        read_tasks_from_file(file_path)?
    } else {
        Vec::new()
    };

    match args.get(1).map(|arg| arg.as_str()) {
        Some("add") => {
            let title = args.get(2).ok_or("Title not provided")?;
            add_task(&mut tasks, title);
        }
        Some("list") => {
            list_tasks(&tasks);
        }
        Some("complete") => {
            let id = args
                .get(2)
                .ok_or("Task ID not provided")?
                .parse::<u32>()
                .map_err(|_| "Invalid task ID")?;
            complete_task(&mut tasks, id)?;
        }
        _ => {
            println!("Usage:");
            println!("task_manager add <title>");
            println!("task_manager list");
            println!("task_manager complete <task_id>");
        }
    }

    write_tasks_to_file(file_path, &tasks)?;

    Ok(())
}

To run this code, follow these steps:

  1. Install the serde and serde_json crates by adding the following dependencies to your Cargo.toml file:

[dependencies]
serde = "1.0"
serde_json = "1.0"
  1. Save the above code to the src/main.rs file within your task_manager project directory.

  2. Run the project using Cargo:

    cargo run add "Buy groceries"
    cargo run add "Complete project"
    cargo run list
    cargo run complete 1
    

This code demonstrates the complete task management application, including file I/O, error handling, and user interaction. Adjust the code as needed and explore enhancements to make the application more robust and user-friendly.

Whenever you're ready

There are 4 ways we can help you become a great backend engineer:

The MB Platform

Join 1000+ backend engineers learning backend engineering. Build real-world backend projects, learn from expert-vetted courses and roadmaps, track your learnings and set schedules, and solve backend engineering tasks, exercises, and challenges.

The MB Academy

The “MB Academy” is a 6-month intensive Advanced Backend Engineering BootCamp to produce great backend engineers.

Join Backend Weekly

If you like post like this, you will absolutely enjoy our exclusive weekly newsletter, Sharing exclusive backend engineering resources to help you become a great Backend Engineer.

Get Backend Jobs

Find over 2,000+ Tailored International Remote Backend Jobs or Reach 50,000+ backend engineers on the #1 Backend Engineering Job Board

Topic:

Backend Tips, Every week

Backend Tips, Every week