4.2 Первый след
Люди довольно быстро обнаружили, что память ненадёжна. Это открытие не остановило никого от того, чтобы на неё полагаться, - зато появилась письменность, что само по себе замечательный результат. Разница между словом произнесённым и словом записанным проста: одно исчезает, другое остаётся. Иногда - слишком надолго. Артефакт умеет переводить задачи в текст. Текст живёт в памяти процесса и заканчивается там же. Это не запись. Это черновик, который никто не читает.
Попытка
Задача ясна: взять список задач, превратить в JSON, записать в файл. Создать
src/persistence/mod.rs и объявить модуль в src/main.rs через mod persistence;.
Логика save выглядит прямолинейно:
// src/persistence/mod.rs
use crate::error::TqResult;
use crate::task::Task;
use std::fs;
use std::path::Path;
pub fn save(tasks: &[Task], path: &Path) -> TqResult<()> {
let json = serde_json::to_string_pretty(tasks)?;
fs::write(path, json)?;
Ok(())
}
cargo build возвращает две ошибки:
error[E0277]: `?` couldn't convert the error to `TqError`
--> src/persistence/mod.rs:XX:XX
|
XX | let json = serde_json::to_string_pretty(tasks)?;
| ^ the trait `From<serde_json::Error>`
| is not implemented for `TqError`
error[E0277]: `?` couldn't convert the error to `TqError`
--> src/persistence/mod.rs:XX:XX
|
XX | fs::write(path, json)?;
| ^ the trait `From<std::io::Error>`
| is not implemented for `TqError`
Что знает компилятор
Оператор ? пытается преобразовать ошибку в тип, который ждёт функция - а именно
TqError. Для этого нужна реализация From<E> for TqError, где E - тип ошибки внутри.
Её нет ни для serde_json::Error, ни для io::Error. Оба ? падают по одной причине.
Добавлять From не обязательно. Достаточно сделать преобразование явным до того, как ?
успеет потребовать его.
Заклинание
Сначала - src/error/mod.rs. Нужен один новый вариант:
// src/error/mod.rs - CHANGED
use std::fmt;
#[derive(Debug)]
pub enum TqError {
EmptyTitle,
NotFound(u64),
Parse(String), // NEW
}
pub type TqResult<T> = Result<T, TqError>;
impl fmt::Display for TqError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
TqError::EmptyTitle => write!(f, "task title cannot be empty"),
TqError::NotFound(id) => write!(f, "task {} not found", id),
TqError::Parse(msg) => write!(f, "parse error: {}", msg), // NEW
}
}
}
Новый вариант есть - теперь ошибкам есть куда идти. В src/persistence/mod.rs обе строки
превращают ошибку в строку и кладут в TqError::Parse:
// src/persistence/mod.rs - CHANGED
use crate::error::{TqError, TqResult};
use crate::task::Task;
use std::fs;
use std::path::Path;
pub fn save(tasks: &[Task], path: &Path) -> TqResult<()> {
let json = serde_json::to_string_pretty(tasks).map_err(|e| TqError::Parse(e.to_string()))?;
fs::write(path, json).map_err(|e| TqError::Parse(e.to_string()))?;
Ok(())
}
#[cfg(test)]
mod tests;
.map_err принимает замыкание и применяет его к ошибке, если она есть. ? после него
ведёт себя как обычно - только тип уже TqError, и никакого From не требуется.
В src/main.rs - добавить use std::path::Path; и вызов save перед Ok(()):
// src/main.rs - добавить перед Ok(())
persistence::save(store.all(), Path::new("tasks.json"))?;
cargo run завершается без ошибок. Рядом с бинарником появляется tasks.json:
[
{
"id": 1,
"title": "Buy coffee",
"status": "Done"
},
{
"id": 2,
"title": "Buy milk",
"status": "Todo"
},
{
"id": 3,
"title": "Buy eggs",
"status": "Todo"
}
]
В src/persistence/tests.rs - два теста: один проверяет содержимое файла, второй - что
пустой список записывается корректно:
// src/persistence/tests.rs - NEW
use super::*;
use crate::task::Task;
use std::fs;
#[test]
fn save_writes_tasks_as_json() {
let path = std::env::temp_dir().join("tq_test_m42_save.json");
let tasks = vec![Task::new(1, "Buy coffee").unwrap()];
save(&tasks, &path).unwrap();
let content = fs::read_to_string(&path).unwrap();
assert!(content.contains(r#""title": "Buy coffee""#));
assert!(content.contains(r#""status": "Todo""#));
let _ = fs::remove_file(&path);
}
#[test]
fn save_writes_empty_array_for_no_tasks() {
let path = std::env::temp_dir().join("tq_test_m42_empty.json");
save(&[], &path).unwrap();
let content = fs::read_to_string(&path).unwrap();
assert_eq!(content.trim(), "[]");
let _ = fs::remove_file(&path);
}
make ci проходит. У tq теперь есть первый след.
Полный код
tqдля этой главы -4-memory/02-the-first-trace/.
Lore: Как работает .map_err
serde_json::to_string_pretty(tasks)
.map_err(|e| TqError::Parse(e.to_string()))?
|e| - аргумент замыкания: ошибка, которую вернул serde_json. Ok-значения проходят
насквозь. Развернуть вручную можно так:
match serde_json::to_string_pretty(tasks) {
Ok(v) => v,
Err(e) => return Err(TqError::Parse(e.to_string())),
}
.map_err не меняет того, что происходит - только делает запись короче. Замыкания будут
появляться часто; умение разворачивать их вручную - полезный рефлекс.