Building your first Rust app: a guided learning approach
Gain a deeper understanding of the concepts of Rust, vs copy/pasting code
Are you a software developer looking to dive into Rust? This blog post offers a unique learning experience that goes beyond the typical tutorial. Instead of simply providing code to copy/paste, we'll provide you with:
The written requirements for a Todo List CLI App.
A skeleton of the application structure to give you a starting point.
A unit test suite which will pass given an implementation meeting the requirements.
This gives you the guardrails to get started, but it is left to you to research the implementations to fill in the provided scaffolding; empowering you to develop a deeper understanding of Rust.
Whether you're coming from Python, Java, JavaScript, Ruby, or any other programming language, this project will help you gain practical experience with Rust and some key standard libraries.
Project Overview: Todo List CLI App
You'll be building a command-line interface (CLI) application that allows users to manage their todo list. With this app, users can add new tasks, mark items as completed, and view their todo list.
Sneak Peek of What You'll Build
#
# Starting the app greets the user and displays the help menu
#
$ cargo run
Welcome to your Todo app
Available commands:
add <todo_text> - Add a new todo item
complete <todo_id> - Mark a todo item as completed
list - List all todo items
? - Show available commands
x - Exit the program
>
#
# Adding a todo performs that action then lists all todos
#
> add Finish homework
Todo item added successfully!
[ ] 0: Buy groceries
[ ] 1: Do laundry
[ ] 2: Finish homework
#
# Completing a todo performs that action then lists all todos
#
> complete 0
Todo item marked as completed!
[x] 0: Buy groceries
[ ] 1: Do laundry
[ ] 2: Finish homework
#
# Attempt to complete a nonexistent Todo
#
> complete 99
Todo item with id 99 not found
[x] 0: Buy groceries
[ ] 1: Do laundry
[ ] 2: Finish homework
>
#
# x, exits the application
#
> x
Exiting...
$
Written requirements
This is a simple Todo list application accessible through a CLI as seen above.
Define a
TodoItem
struct with the following fields:id: <appropriate int type>
: The unique identifier of the todo item, which is the index of the item in the vector.text: String
: The text description of the todo item.completed: bool
: A boolean flag indicating whether the item is completed or not.
Define a
TodoApp
struct that holds a vector ofTodoItem
structs.items: Vec<TodoItem>
Implement the following methods for the
TodoApp
struct. Parameters and return types are described but not given in the function signature. See the Unit tests for more detail of the expected implementations.
Note:These docs are a good overview of methods used with Structs in Rust. Key is know when to pass a reference to self (&self
) and when that reference would need to be mutable (&mut
).fn new()
: Creates a new instance ofTodoApp
with an empty vector of items.fn add()
: Adds a new todo item with the provided text to the list. Theid
of the new item will be its index in the vector.fn complete()
: Marks the todo item with the specifiedid
as completed by setting itscompleted
flag totrue
. Returnstrue
if the todo was found,false
if not found.
Note: Todo items are never removed from the vector but rather marked as completed.fn list()
: Returns a list of all the todo items, showing theirid
, completion status, and text. Prefix completed items with[x]
and incomplete items with[ ]
.
Note: For simplicity there will be no durable storage; todo items will be stored in a runtime vector (Vec
) and all lost when the program exits.
Handle error cases gracefully, such as when the user enters an invalid
id
for completion.Implement a CLI interface to interact with the
TodoApp
struct. The CLI will loop until the user chooses the exit command. See the CLI usage example above.The CLI should handle the following commands
add <todo_text>
: This will add a new Todo itemcomplete <todo_id>
: This will complete the task for the given id. An error should be shown if the Todo id is not found.list
: This will list all the Todo's with their id and completion status?
This will list the commands availablex
: This will exit the program
Unit Tests: Ensuring Correctness
To ensure that your implementation meets the requirements, we've provided this set of (currently failing) unit tests. This test suite is what you will use to validate your implementations of TodoApp
and TodoItem
.
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add_todo() {
let mut app = TodoApp::new();
app.add(String::from("Buy groceries"));
assert_eq!(app.items.len(), 1);
assert_eq!(app.items[0].id, 0);
assert_eq!(app.items[0].text, "Buy groceries");
assert_eq!(app.items[0].completed, false);
}
#[test]
fn test_complete_todo() {
let mut app = TodoApp::new();
app.add(String::from("Buy groceries"));
app.add(String::from("Do laundry"));
let todo_found = app.complete(0);
assert!(todo_found);
assert_eq!(app.items[0].completed, true);
assert_eq!(app.items[1].completed, false);
}
#[test]
fn test_complete_todo_not_found() {
let mut app = TodoApp::new();
app.add(String::from("Buy groceries"));
app.add(String::from("Do laundry"));
let todo_found = app.complete(99);
assert_eq!(todo_found, false);
}
#[test]
fn test_view_todos() {
let mut app = TodoApp::new();
app.add(String::from("Buy groceries"));
app.add(String::from("Do laundry"));
app.complete(1);
let output_vec = app.list();
assert!(output_vec.contains(&"[ ] :0 Buy groceries".to_string()));
assert!(output_vec.contains(&"[x] :1 Do laundry".to_string()));
}
}
cargo test
will run the unit tests. Unit tests in Rust are straightforward, with just a little syntax to learn. Rust by Example provides a great summary to get you started.
Getting started
Head over to Github and clone this repo. This will give you the starting skeleton files for main.rs
and todo_app.rs
(which includes the unit test suite).
git clone git@github.com:samkeen/learning-rust-todo-cli-app.git
cd learning-rust-todo-cli-app
cargo test
LOTS OF ERRORS :(
Your initial task is to review the requirements and the skeleton implementation of todo_app.rs
(including the unit test suite). Implement the TodoApp
and TodoItem
structs and any associated functions. As you make progress continue to run cargo test
to validate your progress.
All Tests passing ✅
cargo test
running 4 tests
test todo_app::tests::test_add_todo ... ok
test todo_app::tests::test_complete_todo ... ok
test todo_app::tests::test_view_todos ... ok
test todo_app::tests::test_complete_todo_not_found ... ok
test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
When all tests pass, move to main.rs
to implement the CLI, which wont be too difficult now that you have working models for your todo app. Once the CLI functionality matches the spec, you are done, congratulations!!!
Wrapping Up
By working through the skeleton code and implementing the functionality yourself, you've gained valuable hands-on experience with Rust and its concepts. Remember, the real learning comes from tackling the challenges and referring to the Rust documentation, tutorials, and examples as needed. There is an example working implementation on this branch in the repo, but I highly encourage you to not peek unless you really get stuck. Also, realize the example implementation is just one possibility of meeting the requirements, yours will probably look different and that's completely fine and expected.
As you continue your Rust journey, embrace the opportunity to explore further, experiment with different approaches, and engage with the supportive Rust community. Building projects from scratch, guided by tests and a solid foundation, is a powerful way to reinforce your understanding and grow as a Rust developer.
Keep coding, keep learning, and most importantly, enjoy the process! Your newfound Rust skills will undoubtedly open up exciting possibilities in your programming career.
If you like this format of instruction, let me know and I'll publish more (I really enjoyed creating this one).
Happy coding, and welcome to the world of Rust!