4.5 Собственные инструкции
В порядочном цехе у верстака есть листок с настройками: где хранить работу, при каком режиме, что проверить первым. Мастер, уходя, не забирает его с собой - листок нужен инструменту, а не мастеру. Инструмент без листка тоже работает. Он делает всё так, как делал всегда, независимо от того, что изменилось снаружи. Это принято называть надёжностью. У этого есть и другое название.
Что нужно
Путь к файлу с задачами сейчас зашит прямо в код. Это работает - ровно до тех пор,
пока задачи нужно хранить именно там. Нужна структура Config, которая хранит настройки,
и файл config.toml, откуда эти настройки берутся. Если файла нет - работают умолчания.
Сборка
toml - крейт для чтения файлов в одноимённом формате. cargo add toml добавляет его
в зависимости. В Cargo.toml:
# Cargo.toml - CHANGED
[dependencies]
chrono = { version = "0.4", features = ["serde"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
toml = "1.1"
Config - новый модуль. В src/config/mod.rs:
// src/config/mod.rs - NEW
use crate::error::{TqError, TqResult};
use serde::Deserialize;
use std::fs;
use std::path::{Path, PathBuf};
#[derive(Deserialize)]
pub struct Config {
pub data_dir: PathBuf,
}
impl Default for Config {
fn default() -> Self {
Config {
data_dir: PathBuf::from("."),
}
}
}
impl Config {
pub fn load(path: &Path) -> TqResult<Config> {
let config = if !path.exists() {
Config::default()
} else {
let content = fs::read_to_string(path).map_err(|e| TqError::Parse(e.to_string()))?;
toml::from_str(&content).map_err(|e| TqError::Parse(e.to_string()))?
};
if !config.data_dir.exists() {
return Err(TqError::Parse(format!(
"data_dir '{}' does not exist; create it with: mkdir -p {}",
config.data_dir.display(),
config.data_dir.display()
)));
}
Ok(config)
}
}
#[derive(Deserialize)] - тот же механизм, что и для Task, только без Serialize:
конфигурацию читают, не пишут. toml::from_str читает содержимое файла и заполняет
структуру. Если файл отсутствует - Config::default() даёт умолчания.
impl Default - стандартный трейт Rust для значений по умолчанию. Он позволяет написать
Config::default() там, где нужен конфиг “из воздуха”, и объявляет, что это значит
явно, а не через магические константы где-то в коде.
Сам файл config.toml - в корне проекта, рядом с Cargo.toml. cargo run запускается оттуда
же, поэтому Path::new("config.toml") находит его правильно. Одна строка:
data_dir = "."
В src/main.rs путь теперь берётся из конфигурации. Добавить mod config и
use config::Config, заменить две строки в начале main:
// src/main.rs - CHANGED
fn main() -> TqResult<()> {
let config = Config::load(Path::new("config.toml"))?;
let path = config.data_dir.join("tasks.json"); // CHANGED
let tasks = persistence::load(&path)?;
// ...
persistence::save(store.all(), &path)?;
Одна строка с захардкоженным путём стала двумя: загрузить конфиг, собрать путь из него.
До этого переменная path была &Path - Path::new возвращает ссылку сразу. Теперь join возвращает
новый PathBuf, а не ссылку - поэтому в вызовах появился &path. Остальной main не меняется.
Результат
Без config.toml - поведение прежнее, tq ищет tasks.json в текущем каталоге.
С файлом:
data_dir = "/home/user/.tq"
задачи хранятся там - при условии что директория существует. Если нет:
Error: Parse("data_dir '/home/user/.tq' does not exist; create it with: mkdir -p /home/user/.tq")
load проверяет data_dir сразу после разбора конфига - тот же паттерн if !path.exists(),
что уже есть в persistence::load. Ошибка не просто сообщает о проблеме, но и говорит
что делать. Код не меняется. Только листок на верстаке.
Четыре теста фиксируют все пути. В src/config/tests.rs:
// src/config/tests.rs - NEW
use super::*;
#[test]
fn config_defaults_to_current_dir() {
let config = Config::default();
assert_eq!(config.data_dir, PathBuf::from("."));
}
#[test]
fn config_load_missing_file_returns_default() {
let path = std::env::temp_dir().join("tq_test_m45_no_config.toml");
let _ = std::fs::remove_file(&path);
let config = Config::load(&path).unwrap();
assert_eq!(config.data_dir, PathBuf::from("."));
}
#[test]
fn config_load_parses_data_dir() {
let path = std::env::temp_dir().join("tq_test_m45_config.toml");
std::fs::write(&path, r#"data_dir = "/tmp""#).unwrap();
let config = Config::load(&path).unwrap();
assert_eq!(config.data_dir, PathBuf::from("/tmp"));
let _ = std::fs::remove_file(&path);
}
#[test]
fn config_load_rejects_missing_data_dir() {
let config_path = std::env::temp_dir().join("tq_test_m45_bad_config.toml");
let missing = "/tmp/tq_nonexistent_xyz";
let _ = std::fs::remove_dir_all(missing);
std::fs::write(&config_path, format!(r#"data_dir = "{}""#, missing)).unwrap();
let result = Config::load(&config_path);
assert!(result.is_err());
let _ = std::fs::remove_file(&config_path);
}
make ci проходит.
Полный код
tqдля этой главы -4-memory/05-reading-its-own-instructions/.
Lore: use - псевдоним, не замок
В M4.1 в коде появилась строка use serde::{Deserialize, Serialize} - без объяснений,
как будто иначе нельзя. На самом деле use не открывает доступ к крейту: любой крейт
из Cargo.toml доступен по полному пути в любом месте кода без единого use.
use - это псевдоним. Он позволяет писать короткое имя вместо полного пути. В M4.1
псевдоним был нужен: #[derive(Deserialize)] - короткое имя в атрибуте, без use
пришлось бы писать #[derive(serde::Deserialize)]. В этой главе toml::from_str -
полный путь, используется один раз, сама запись проста - псевдоним был бы избыточен.
Та же логика у use std::fmt и use std::path::Path - просто удобство, не обязанность.
Lore: TOML как формат для конфигурации
JSON удобен для обмена данными между программами. Для файлов, которые редактирует человек,
у него есть неудобная особенность: комментарии запрещены. Написать # путь к данным
рядом со значением нельзя - это синтаксическая ошибка.
TOML проектировался именно для конфигурационных файлов. Комментарии - нативно:
# Where tasks are stored
data_dir = "."
Иерархия через секции, даты как отдельный тип, многострочные строки. Человек открывает файл и понимает его без документации. Именно поэтому большинство инструментов в экосистеме Rust - сам Cargo в том числе - используют TOML для конфигурации.