4.3 Возвращение
Написать - половина дела. Вторая половина называется прочитать. Мастер, умеющий записывать заготовки в журнал, но не умеющий их оттуда брать, ведёт очень дисциплинированную опись того, что потерял навсегда. След есть. Назад по нему пока не вернуться.
Что нужно
save записывает задачи в файл. load должна их оттуда брать. Без неё каждый запуск
начинается с пустого хранилища - что бы ни оказалось в tasks.json.
Первый запуск - особый случай: файла ещё нет. Отсутствие файла не ошибка, а просто начало.
load обязана это учитывать.
Сборка
load зеркальна save. В src/persistence/mod.rs:
// src/persistence/mod.rs - CHANGED
pub fn load(path: &Path) -> TqResult<Vec<Task>> {
if !path.exists() {
return Ok(Vec::new());
}
let content = fs::read_to_string(path)
.map_err(|e| TqError::Parse(e.to_string()))?;
let tasks: Vec<Task> = serde_json::from_str(&content)
.map_err(|e| TqError::Parse(e.to_string()))?;
Ok(tasks)
}
Загрузить задачи в хранилище через обычный add не получится - add выдаёт каждой задаче
новый идентификатор, затирая тот, что был сохранён. new получает новую сигнатуру: принимает
уже готовый список задач. В src/store/mod.rs:
// src/store/mod.rs - CHANGED
impl TaskStore {
pub fn new(tasks: Vec<Task>) -> Self { // CHANGED: принимает задачи напрямую
let next_id = tasks.iter().map(|t| t.id).max().unwrap_or(0) + 1;
TaskStore { items: tasks, next_id }
}
}
unwrap_or(0) + 1 даёт next_id = 1 для пустого списка - при первом запуске хранилище
начинается с единицы. Для загруженных задач - сдвиг выше максимума: новые идентификаторы
не столкнутся со старыми.
В src/main.rs - загрузить при старте, сохранить при выходе:
// src/main.rs - CHANGED
fn main() -> TqResult<()> {
let path = Path::new("tasks.json");
let tasks = persistence::load(path)?; // NEW
let mut store = TaskStore::new(tasks); // CHANGED: new(tasks)
println!("Loaded: {} task(s)", store.all().len()); // NEW
store.add("Buy coffee")?;
store.add("Buy milk")?;
store.add("Buy eggs")?;
for task in store.all() {
println!("{}", task);
}
match store.get(1) {
Ok(task) => println!("found: {}", task),
Err(e) => println!("{}", e),
}
match store.get(99) {
Ok(task) => println!("{}", task),
Err(e) => println!("{}", e),
}
store.get_mut(1)?.complete();
println!("done: {}", store.get(1)?.is_done());
match Task::new(0, "") {
Ok(task) => store.add(task)?,
Err(e) => println!("rejected: {}", e),
}
persistence::save(store.all(), path)?; // CHANGED: path вместо Path::new(...)
Ok(())
}
Результат
Для чистоты эксперимента вначале удалим ранее созданый tasks.json
rm tasks.json
Тогда первый запуск нам даст:
Loaded: 0 task(s)
#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
а второй запуск:
Loaded: 3 task(s)
#1: Buy coffee [Done]
#2: Buy milk [Todo]
#3: Buy eggs [Todo]
#4: Buy coffee [Todo]
#5: Buy milk [Todo]
#6: Buy eggs [Todo]
found: #1: Buy coffee [Done]
task 99 not found
done: true
rejected: task title cannot be empty
Задачи из первого запуска живы. Процесс завершился - они остались.
Ключевой тест - полный круг: сохранить и загрузить обратно. В src/persistence/tests.rs:
// src/persistence/tests.rs - CHANGED
#[test]
fn save_and_load_roundtrip() {
let path = std::env::temp_dir().join("tq_test_m43_roundtrip.json");
let tasks = vec![Task::new(1, "Buy coffee").unwrap()];
save(&tasks, &path).unwrap();
let loaded = load(&path).unwrap();
assert_eq!(loaded.len(), 1);
assert_eq!(loaded[0].title, "Buy coffee");
let _ = fs::remove_file(&path);
}
Тесты для отсутствующего файла и пустого списка - в tq/.
make ci проходит. Задачи переживают перезапуск.
Полный код
tqдля этой главы -4-memory/03-the-return/.
Lore: path.exists() и гонка состояний
path.exists() - проверка, после которой файл теоретически могут удалить до
fs::read_to_string. На практике это не проблема: tq однопоточный, файл меняет только
он сам. Но есть и другой подход - открывать файл и обрабатывать отсутствие явно:
use std::io::ErrorKind;
pub fn load(path: &Path) -> TqResult<Vec<Task>> {
match fs::read_to_string(path) {
Ok(content) => serde_json::from_str(&content)
.map_err(|e| TqError::Parse(e.to_string())),
Err(e) if e.kind() == ErrorKind::NotFound => Ok(Vec::new()),
Err(e) => Err(TqError::Parse(e.to_string())),
}
}
e.kind() == ErrorKind::NotFound - фильтр прямо в ветке match. Отсутствие файла идёт в
Ok(Vec::new()), любая другая io-ошибка - в Err. В tq оба варианта равнозначны.