3.3 A Place of Its Own
A box is not built to last forever. It is built because scattered things are inconvenient - and a well-made box gives shape even when it is eventually replaced. This is usually called a temporary solution. The accurate word is a first step.
What You Need
The current approach has two visible problems.
The identifier is supposed to be unique, but main decides which one to assign. With a
handful of tasks that is easy to track by hand. A skipped or repeated number is something
the compiler will not notice; a single authority for issuing identifiers is needed.
Three functions - add, get, and list - always travel together. Each takes either
&mut Vec<Task> or &[Task] as its first argument. The task list is their shared state;
it lives in main and is passed by hand on every call.
This works while there are three functions and one craftsman.
The Build
Move the state inside: let a struct own the list and take responsibility for it.
struct TaskStore {
items: Vec<Task>,
next_id: u64,
}
A constructor and three methods - in one impl TaskStore:
impl TaskStore {
fn new() -> Self {
TaskStore {
items: Vec::new(),
next_id: 1,
}
}
fn add(&mut self, task: impl Into<Task>) -> Result<(), TqError> {
let mut task = task.into();
task.id = self.next_id;
self.next_id += 1;
self.items.push(task);
Ok(())
}
fn get(&self, id: u64) -> Result<&Task, TqError> {
self.items
.iter()
.find(|task| task.id == id)
.ok_or(TqError::NotFound(id))
}
fn all(&self) -> &[Task] {
&self.items
}
}
new creates an empty store with next_id: 1. add accepts impl Into<Task> (chapter
3.2): through From<&str> for Task from chapter 3.1, a &str qualifies; the identifier
is assigned inside. get finds a task by id and returns a reference - or
TqError::NotFound if there is none. all returns a slice &[Task] - instead of list,
which printed immediately; now the caller decides what to do with the collection.
The Result
The free functions add, get, and list are gone. main works with TaskStore:
fn main() -> Result<(), TqError> {
let mut store = TaskStore::new();
store.add("Buy coffee")?;
store.add("Buy milk")?;
store.add("Buy eggs")?;
for task in store.all() {
println!("#{}: {} - {:?}", task.id, task.title, task.status);
}
match store.get(1) {
Ok(task) => println!("found: #{}: {}", task.id, task.title),
Err(e) => println!("error: {:?}", e),
}
match store.get(99) {
Ok(task) => println!("#{}: {}", task.id, task.title),
Err(TqError::NotFound(id)) => println!("task {} not found", id),
Err(e) => println!("error: {:?}", e),
}
let task = &mut store.items[0];
task.complete();
println!("done: {}", task.is_done());
match Task::new(0, "") {
Ok(task) => store.add(task)?,
Err(e) => println!("rejected: {:?}", e),
}
Ok(())
}
store.add("Buy coffee")? - a &str passes directly: From<&str> and impl Into<Task>
meet here. Completing a task works the same as before, only tasks[0] has become
store.items[0].
Two tests cover the new behaviour:
#[test]
fn store_assigns_sequential_ids() {
let mut store = TaskStore::new();
store.add("A").unwrap();
store.add("B").unwrap();
assert_eq!(store.all()[0].id, 1);
assert_eq!(store.all()[1].id, 2);
}
#[test]
fn store_get_returns_not_found() {
let store = TaskStore::new();
assert!(matches!(store.get(99), Err(TqError::NotFound(99))));
}
make ci passes. Tasks live in TaskStore - add, find, enumerate. The store knows
everything about tasks; about the possibility that tasks might not be the only kind of data
it holds, it knows nothing yet.
The complete
tqcode for this chapter is in3-a-voice/03-a-store-for-anything/.