4.1 The Language of Machines
A good craftsman keeps records. A poor craftsman relies on memory - and that works fine right up until three orders have gone missing and need explaining. Memory is convenient: it is always at hand and always, as a rule, already slightly different. Records are inconvenient: they must be kept. But only one of the two survives a closed door.
What You Need
tq can add tasks, complete them, and find them by identifier. Each time - from nothing.
Everything added in the previous run disappeared with it. tq has no records.
To save tasks to a file, they must be translated into a text format. JSON fits: it is
readable by people, readable by machines, and most tools understand it. The translation
tool - serde - is already in place. What remains is to tell Task how to use it.
The Vocabulary
In src/task/mod.rs, add the import and attributes to Status and Task:
// src/task/mod.rs - CHANGED: serde import; derives on Status and Task
use crate::error::{TqError, TqResult};
use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Serialize, Deserialize)]
pub enum Status {
Todo,
Done,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Task {
pub id: u64,
pub title: String,
pub status: Status,
}
#[derive(Serialize, Deserialize)] is needed on Status as well as Task: serde knows
how to handle standard types - numbers, strings - but Status is our own type, and without
explicit instruction it cannot serialize it.
Add one line in src/main.rs to confirm the translation works:
// src/main.rs - add before Ok(())
let json = serde_json::to_string_pretty(store.get(1)?).unwrap();
println!("as JSON:\n{}", json);
The .unwrap() here is temporary: the serde_json error type is not yet connected to
TqResult.
The Result
#1: Buy coffee [Todo]
#2: Buy milk [Todo]
#3: Buy eggs [Todo]
found: #1: Buy coffee [Todo]
task 99 not found
done: true
rejected: task title cannot be empty
as JSON:
{
"id": 1,
"title": "Buy coffee",
"status": "Done"
}
Two new tests in src/task/tests.rs - they check the translation in both directions:
// src/task/tests.rs - add at the end
#[test]
fn task_serializes_to_json() {
let task = Task::new(1, "Buy coffee").unwrap();
let json = serde_json::to_string(&task).unwrap();
assert!(json.contains(r#""title":"Buy coffee""#));
assert!(json.contains(r#""status":"Todo""#));
}
#[test]
fn task_roundtrips_through_json() {
let task = Task::new(1, "Buy coffee").unwrap();
let json = serde_json::to_string(&task).unwrap();
let restored: Task = serde_json::from_str(&json).unwrap();
assert_eq!(restored.id, task.id);
assert_eq!(restored.title, task.title);
assert!(!restored.is_done());
}
make ci passes. Writing to disk is the next step.
The complete
tqcode for this chapter is in4-memory/01-the-language-of-machines/.
Lore: Derive Macros
#[derive(Debug)] and #[derive(Clone)] are built into the compiler - no crate needed.
#[derive(Serialize, Deserialize)] is a different case: this is a procedural macro
shipped by the serde crate itself. That is exactly what the --features derive flag from
the previous chapter enables.
At compile time, it reads the type definition and generates an implementation of the
Serialize trait - the same one you could have written by hand. This is not magic - it is
a tool that writes code for you.
Why a separate feature? Compiling procedural macros adds build time. A crate that uses
serde only as a set of traits - in a library, not a binary - can do without code
generation. The flag means you do not pay for what you do not use.