Xây Dựng Dự Án CLI Todo App Hoàn Chỉnh Bằng Rust

Sau khi đã nắm vững toàn bộ các khái niệm cốt lõi từ Ownership, Borrowing, Struct/Enum cho đến cơ chế xử lý lỗi an toàn qua Result/Option ở các bài trước, đây là lúc chúng ta tổng hợp toàn bộ kiến thức để thực chiến. Trong bài viết này, chúng ta sẽ cùng nhau xây dựng một ứng dụng dòng lệnh (CLI Todo App) hoàn chỉnh, cho phép người dùng thêm, xem, hoàn thành và lưu trữ danh sách công việc trực tiếp xuống file.
Hãy mở terminal lên và bắt đầu hành trình lập trình thực tế ngay dưới đây.
🚀 1. Khởi tạo dự án Cargo và Cấu trúc Module
Đầu tiên, hãy tạo một dự án binary mới thông qua công cụ quản lý gói Cargo của Rust:
cargo new rust_todo_app
cd rust_todo_app
Để giữ cho mã nguồn mạch lạc, dễ mở rộng và tuân thủ các nguyên tắc thiết kế phần mềm chuyên nghiệp, chúng ta sẽ chia dự án thành các module rõ ràng:
- main.rs: Điểm khởi chạy chương trình, xử lý các tham số dòng lệnh từ người dùng.
- todo.rs: Định nghĩa cấu trúc dữ liệu công việc (Struct, Enum) và các phương thức xử lý logic nghiệp vụ.
- storage.rs: Đảm nhận việc đọc/ghi dữ liệu xuống file ổ cứng để lưu trữ lâu dài.
📝 2. Xây dựng Module Nghiệp Vụ (todo.rs)
Đầu tiên, chúng ta định nghĩa cấu trúc dữ liệu cho một công việc (TodoItem) và trạng thái của nó. Sử dụng Enum để biểu thị rõ ràng các trạng thái hoàn thành.
use serde::{Serialize, Deserialize};
// Trạng thái của công việc
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum TodoStatus {
Pending,
Completed,
}
// Cấu trúc một công việc đơn lẻ
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct TodoItem {
pub id: usize,
pub title: String,
pub status: TodoStatus,
}
impl TodoItem {
// Hàm khởi tạo công việc mới ở trạng thái Pending
pub fn new(id: usize, title: String) -> Self {
Self {
id,
title,
status: TodoStatus::Pending,
}
}
}
(Lưu ý: Để dễ dàng chuyển đổi dữ liệu thành định dạng JSON lưu vào file, chúng ta sử dụng thư viện serde. Hãy thêm serde = { version = "1.0", features = ["derive"] } và serde_json = "1.0" vào file Cargo.toml của bạn).
💾 3. Xây dựng Module Lưu Trữ (storage.rs)
Module này chịu trách nhiệm lưu danh sách công việc xuống một file JSON cục bộ trên máy và đọc ngược lại khi chương trình khởi động, sử dụng hệ thống xử lý lỗi Result an toàn mà chúng ta đã học ở bài trước.
use std::fs;
use std::io;
use crate::todo::TodoItem;
const FILE_NAME: &str = "todos.json";
// Đọc danh sách todo từ file
pub fn load_todos() -> Result<Vec<TodoItem>, io::Error> {
// Nếu file chưa tồn tại, trả về một vector rỗng thay vì báo lỗi
if !std::path::Path::new(FILE_NAME).exists() {
return Ok(Vec::new());
}
let data = fs::read_to_string(FILE_NAME)?;
let todos: Vec<TodoItem> = serde_json::from_str(&data).unwrap_or_else(|_| Vec::new());
Ok(todos)
}
// Lưu danh sách todo xuống file
pub fn save_todos(todos: &[TodoItem]) -> Result<(), io::Error> {
let data = serde_json::to_string_pretty(todos)?;
fs::write(FILE_NAME, data)?;
Ok(())
}
⚙️ 4. Xây dựng Logic Giao Diện Dòng Lệnh (main.rs)
Cuối cùng, chúng ta kết nối mọi thứ tại main.rs, phân tích các tham số dòng lệnh (ví dụ: add "Học Rust", list, done 1) và thực thi các thao tác tương ứng.
mod todo;
mod storage;
use std::env;
use todo::TodoItem;
fn main() {
let args: Vec<String> = env::args().collect();
// Đọc danh sách hiện tại từ file
let mut todos = storage::load_todos().unwrap_or_else(|_| {
println!("⚠️ Không thể tải dữ liệu cũ, khởi tạo danh sách mới.");
Vec::new()
});
if args.len() < 2 {
print_help();
return;
}
let command = &args[1];
match command.as_str() {
"add" => {
if args.len() < 3 {
println!("❌ Vui lòng nhập nội dung công việc! Ví dụ: cargo run add \"Học Ownership\"");
return;
}
let title = args[2].clone();
let new_id = todos.len() + 1;
let new_item = TodoItem::new(new_id, title);
todos.push(new_item);
storage::save_todos(&todos).expect("Lỗi khi lưu file!");
println!("✅ Đã thêm công việc thành công!");
}
"list" => {
if todos.is_empty() {
println!("📭 Danh sách công việc đang trống!");
return;
}
println!("\n📌 DANH SÁCH CÔNG VIỆC:");
for item in &todos {
let status_icon = match item.status {
todo::TodoStatus::Pending => "⏳ [Chưa làm]",
todo::TodoStatus::Completed => "✅ [Đã xong]",
};
println!("{}. {} - {}", item.id, item.title, status_icon);
}
println!();
}
"done" => {
if args.len() < 3 {
println!("❌ Vui lòng cung cấp ID công việc cần hoàn thành!");
return;
}
let id: usize = match args[2].parse() {
Ok(num) => num,
Err(_) => {
println!("❌ ID không hợp lệ!");
return;
}
};
if let Some(item) = todos.iter_mut().find(|t| t.id == id) {
item.status = todo::TodoStatus::Completed;
storage::save_todos(&todos).expect("Lỗi khi lưu file!");
println!("🎉 Đã đánh dấu hoàn thành công việc số {id}!");
} else {
println!("❌ Không tìm thấy công việc với ID = {id}!");
}
}
_ => {
print_help();
}
}
}
fn print_help() {
println!("🛠️ RUST TODO APP - HƯỚNG DẪN SỬ DỤNG:");
println!(" 1. Thêm việc: cargo run add \"Nội dung công việc\"");
println!(" 2. Xem danh sách: cargo run list");
println!(" 3. Hoàn thành: cargo run done <ID>");
}
🧪 5. Chạy Thử Nghiệm Ứng Dụng
Hãy mở terminal và thử nghiệm các lệnh ngay trên máy của bạn:
# Thêm công việc mới
cargo run add "Nghiên cứu hệ thống Ownership trong Rust"
cargo run add "Viết blog kỹ thuật bằng văn phong thực chiến"
# Xem danh sách công việc
cargo run list
# Đánh dấu hoàn thành công việc số 1
cargo run done 1
# Kiểm tra lại danh sách
cargo run list
Toàn bộ dữ liệu sẽ được tự động lưu vào file todos.json ngay tại thư mục gốc của dự án với tốc độ xử lý chớp nhoáng và sự an toàn tuyệt đối từ trình biên dịch Rust!
Khép lại chuỗi cơ bản này với Bài 8, bạn đã đi từ những bước cài đặt môi trường đầu tiên, làm chủ hệ thống kiểu dữ liệu, thấu hiểu cơ chế an toàn bộ nhớ cốt lõi (Ownership, Borrowing), cho đến việc tự tay hoàn thiện một ứng dụng dòng lệnh thực tế. Hãy tiếp tục thực hành, viết code mỗi ngày và tự tin chinh phục những thử thách lập trình hệ thống cao cấp hơn trong phần nâng cao sắp tới!
Bình luận