tutorials/rust/todo-list-redux/src/main.rs

250 lines
8.2 KiB
Rust

use std::io;
// Lets us type `Add("Todo item".to_string())` instead of `TodoAction::Add("Todo item".to_string())
use TodoAction::{ Add, Remove, Toggle };
// Same with the Action enum and VisibilityFilter, Action::* would work too, but this way we list what we use
use Action::{ Todos, Visibility };
use VisibilityFilter::{ ShowActive, ShowAll, ShowCompleted };
// Ripping off the canonical Redux todo example we'll add a
// visibility filter to our state in addation to the todos we already had
// This state struct will be the single source of state for our todo list program
#[derive(Clone, Debug)]
struct State {
todos: Vec<Todo>,
visibility_filter: VisibilityFilter
}
// By implementing a struct we are creating something very much like
// a class, we can attach methods to it refering to `&self` or `&mut self`
impl State {
// Can be called with State::default()
fn default() -> State {
State {
todos: Vec::new(),
visibility_filter: VisibilityFilter::ShowAll,
}
}
}
// Same Todo as last time...
#[derive(Clone, Debug)]
struct Todo {
id: i16,
title: String,
completed: bool,
deleted: bool,
}
// Create a convenient Todo::new(id, title) method
impl Todo {
pub fn new(id: i16, title: String) -> Todo {
Todo {
id: id,
title: title,
completed: false,
deleted: false,
}
}
}
// Redux store implementation
struct Store {
state: State,
listeners: Vec<fn(&State)>,
reducer: fn(&State, Action) -> State,
}
impl Store {
// Takes a reducer function as the only argument
// To keep it simple, State::default() privides the initial state in this example
fn create_store(reducer: fn(&State, Action) -> State) -> Store {
Store {
state: State::default(),
listeners: Vec::new(),
reducer: reducer,
}
}
// Pushes a listener that will be called for any state change
fn subscribe(&mut self, listener: fn(&State)) {
self.listeners.push(listener);
}
// Simply returns a borrowed reference to the state
#[allow(dead_code)]
fn get_state(&self) -> &State {
&self.state
}
// Called for every new action, calls the reducer to update the state
// and then calls every listener with the new State
fn dispatch(&mut self, action: Action) {
self.state = (self.reducer)(&self.state, action);
for listener in &self.listeners {
listener(&self.state);
}
}
}
// Rust has enums, so the enum type can replace the "type" property of Redux objects
// The enums will replace `action_creators` too since Todos(Add("Todo item".to_string()))
// is pretty clear
#[derive(Clone, Debug)]
enum Action {
Todos(TodoAction),
Visibility(VisibilityFilter),
}
// mark_done from the previous example becomes Toggle to align with the Redux example
// otherwise functionality is the same
#[derive(Clone, Debug)]
enum TodoAction {
Add(String),
Toggle(i16),
Remove(i16),
}
// Our 3 visibility states
#[derive(Clone, Debug)]
enum VisibilityFilter {
ShowActive,
ShowAll,
ShowCompleted,
}
// Helper function for getting a mutable todo from a vector by todo_id
fn get_mut_todo(todos: &mut Vec<Todo>, todo_id: i16) -> Option<&mut Todo> {
todos.iter_mut().find(|todo| todo.id == todo_id)
}
// Our main reducer, returns a new State with the results of the child-reducers
// No combineReducers is implemented here, so it calls the child reducers
// by function name
fn reducer(state: &State, action: Action) -> State {
// Always return a new state
State {
todos: todo_reducer(&state.todos, &action),
visibility_filter: visibility_reducer(&state.visibility_filter, &action),
}
}
// Our todo reducer, takes in state (todo list) and returns a new/cloned version
// after applying the action (is applicable)
fn todo_reducer(state: &Vec<Todo>, action: &Action) -> Vec<Todo> {
let mut new_state: Vec<Todo> = state.clone();
// First we make sure it's a `Todos` action, otherwise return clone of incoming state
match *action {
Todos(ref todo_action) => match *todo_action {
// Pretty simple from here on, check the type of Todos enum type
// If Add push a new item, and if `Toggle` or `Remove` user our get_mut_todo
// helper function and then change a property on the todo
Add(ref title) => {
let new_id = new_state.len() as i16 + 1;
new_state.push(Todo::new(new_id, title.to_string()))
},
Toggle(todo_id) => {
if let Some(todo) = get_mut_todo(&mut new_state, todo_id) {
if todo.completed { todo.completed = false; }
else { todo.completed = true }
}
},
Remove(todo_id) => {
if let Some(todo) = get_mut_todo(&mut new_state, todo_id) {
todo.deleted = true;
}
},
},
// If it's not a Todos action change nothing
_ => (),
}
return new_state;
}
// Very simple reducer since the action will either be a VisibilityFilter, in which
// case we will return that, otherwise just return the incoming state
fn visibility_reducer(state: &VisibilityFilter, action: &Action) -> VisibilityFilter {
match *action {
Visibility(ref vis_action) => vis_action.clone(),
_ => state.clone(),
}
}
// Very simple function to print a todo
fn print_todo(todo: &Todo) {
let done = if todo.completed { "" } else { " " };
println!("[{}] {} {}", done, todo.id, todo.title);
}
fn print_todos(state: &State) {
let visibility = &state.visibility_filter;
println!("\n\nTodo List:\n--------------------");
for todo in &state.todos {
if !todo.deleted {
match *visibility {
ShowAll => print_todo(&todo),
ShowCompleted => if todo.completed { print_todo(&todo) },
ShowActive => if !todo.completed { print_todo(&todo) },
}
}
}
println!("-------------------\nVisibility filter: {:?}", visibility);
print_instructions();
}
fn print_instructions() {
println!("\nAvailable commands: \nadd [text] - toggle [id] - remove [id]\n show [all|active|completed]");
}
fn invalid_command(command: &str) {
println!("Invalid command: '{}'", command);
}
fn main() {
// Let's create our store and subscribe with print_todos so every update is printed
let mut store = Store::create_store(reducer);
store.subscribe(print_todos);
print_instructions();
// Same input handling as the last time, the interesting parts will be in our match statement
loop {
// Assign input lines to the `command` variable
let mut command = String::new();
io::stdin()
.read_line(&mut command)
.expect("failed to read line");
// Split input on whitespace
let command_parts: Vec<&str> = command.split_whitespace().collect();
// Now match the size of the vector holding the separate words in the command
match command_parts.len() {
// If 0 we can't really do much
0 => invalid_command(&command),
_ => {
match command_parts[0] {
// Since we prepared so well we just need to call dispatch on our store
// with the right action
"add" => store.dispatch(Todos(Add(command_parts[1..].join(" ").to_string() ))),
"remove" => if let Ok(num) = command_parts[1].parse::<i16>() {
store.dispatch(Todos(Remove(num)));
},
"toggle" => if let Ok(num) = command_parts[1].parse::<i16>() {
store.dispatch(Todos(Toggle(num)));
},
"show" => match command_parts[1] {
"all" => store.dispatch(Visibility(ShowAll)),
"active" => store.dispatch(Visibility(ShowActive)),
"completed" => store.dispatch(Visibility(ShowCompleted)),
_ => invalid_command(&command),
},
_ => invalid_command(&command),
}
}
}
}
}