← Оглавление

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 только как набор трейтов - в библиотеке, не в бинарнике, - может обойтись без кодогенерации. Флаг позволяет не платить за то, что не используется.