4.1 Язык машин
Хороший мастер ведёт записи. Плохой мастер полагается на память - и это сходит с рук ровно до тех пор, пока не приходится объяснять, почему три заказа пропали. Память удобна: она всегда под рукой и всегда, как правило, уже немного другая. Записи неудобны: их нужно вести. Но только одно из этих двух переживает закрытые двери.
Что нужно
tq умеет добавлять задачи, завершать их и находить по идентификатору. Каждый раз -
заново. Всё, что было добавлено в предыдущем запуске, исчезло вместе с ним. У tq нет
записей.
Чтобы сохранить задачи в файл, их нужно перевести в текстовый формат. JSON подходит:
он читается людьми, он читается машинами, и большинство инструментов его понимают.
Инструмент для перевода - serde - уже есть. Осталось объяснить Task, как им пользоваться.
Словарь
В src/task/mod.rs добавляем импорт и атрибуты к Status и 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)] нужен не только Task, но и Status: serde умеет
работать со стандартными типами - числами, строками, - но Status это наш собственный
тип, и без явного указания сериализовать его он не сможет.
Добавим в src/main.rs одну строку - чтобы убедиться, что перевод работает:
// src/main.rs - добавить перед Ok(())
let json = serde_json::to_string_pretty(store.get(1)?).unwrap();
println!("as JSON:\n{}", json);
.unwrap() здесь - временная мера: тип ошибки serde_json пока не подключён к TqResult.
Результат
#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"
}
В src/task/tests.rs два новых теста - проверяют перевод в обе стороны:
// src/task/tests.rs - добавить в конец
#[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 проходит. Записать на диск - следующий шаг.
Полный код
tqдля этой главы -4-memory/01-the-language-of-machines/.
Lore: derive-макросы
#[derive(Debug)] и #[derive(Clone)] встроены в компилятор - для них не нужен никакой
крейт. #[derive(Serialize, Deserialize)] - другой случай: это процедурный макрос,
поставляемый самим крейтом serde. Именно его и включает флаг --features derive из
прошлой главы.
Во время компиляции он читает определение типа и создаёт реализацию трейта Serialize -
ту самую, которую можно было бы написать вручную.
Это не магия - это инструмент, который пишет код за вас.
Почему отдельная фича? Компиляция процедурных макросов добавляет время сборки. Крейт,
который использует serde только как набор трейтов - в библиотеке, не в бинарнике, -
может обойтись без кодогенерации. Флаг позволяет не платить за то, что не используется.