← Table of contents

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 tq code for this chapter is in 3-a-voice/03-a-store-for-anything/.