Миграция
masterror спроектирован как прямая замена одновременно thiserror
(синтаксис derive) и anyhow (эргономика). Большинство миграций — это замена
зависимости плюс постепенное принятие типизированных возможностей. Запускаемые
пошаговые примеры:
examples/migrate_from_thiserror.rs
и
examples/migrate_from_anyhow.rs.
С thiserror
Шаг 1 механический — поменяйте импорт; синтаксис derive совместим:
-use thiserror::Error;
+use masterror::Error;
Совместимость атрибутов
| thiserror | masterror | Примечания |
|---|---|---|
#[error("...")] с плейсхолдерами {field} | так же | 1:1, включая позиционные {0} |
Спецификаторы формата :>8, :.3, :x, :p, :e | так же | TemplateFormatter повторяет детекцию форматтеров thiserror |
#[error(transparent)] | так же | требует обёрток с одним полем, пробрасывающих Display/source |
#[from] | так же | генерирует From<...>, проверяет форму обёртки |
#[source] | так же | подключает цепочку source() |
#[backtrace] | так же | учитывается на полях |
| — | #[app_error(kind = ..., code = ..., message)] | добавлено: генерирует From<YourError> for AppError (и AppCode); message пробрасывает Display как публичное сообщение |
| — | #[provide(ref = T, value = T)] | добавлено: типизированная телеметрия через std::error::Request; поля Option<T> предоставляют значение только при Some |
| — | #[derive(Masterror)] + #[masterror(...)] | добавлено: полное отображение с category, redact(message, fields(...)), telemetry(...), map.grpc, map.problem |
Существующие enum продолжают компилироваться без изменений. Что даёт их аннотирование:
use masterror::{AppCode, AppError, AppErrorKind, Error};
#[derive(Debug, Error)]
#[error("user {user_id} not found")]
#[app_error(kind = AppErrorKind::NotFound, code = AppCode::NotFound, message)]
struct UserMissing {
user_id: u64
}
let app: AppError = UserMissing { user_id: 42 }.into();
assert_eq!(app.kind, AppErrorKind::NotFound);
Enum отображаются повариантно — каждый вариант несёт собственный
#[app_error(...)], а derive генерирует единственный From<Enum> for AppError.
Рекомендуемый порядок: (1) поменять импорт, (2) добавить #[app_error] типам,
пересекающим границу API, (3) заменить рукописные реализации
From<DomainError> for AppError сгенерированными, (4) принять
#[masterror(...)] там, где нужны редактирование или метаданные.
С anyhow
| anyhow | masterror | Примечания |
|---|---|---|
anyhow::Result<T> | masterror::AppResult<T> | псевдоним для Result<T, AppError> |
anyhow::Error | masterror::AppError / Error | несёт вид, код и метаданные вместо «блоба» |
.context("msg") | .context("msg") | идентично, через masterror::ResultExt |
.with_context(|| ...) | .ctx(|| Context::new(kind)...) | ленив, как в anyhow, но строит типизированный Context (вид, код, поля, редактирование) |
bail!(err) | fail!(err) | принимает выражение типизированной ошибки, без машинерии форматирования |
ensure!(cond, "msg {x}") | ensure!(cond, AppError::...) | условие + типизированная ошибка; без форматирования на успешном пути |
err.chain() | err.chain() | тот же итератор по цепочке источников |
err.root_cause() | err.root_cause() | так же |
err.is::<E>() / downcast / downcast_ref / downcast_mut | те же имена на AppError | паритет по даункастингу |
Обёртывание в стиле #[from] | Реализации From<...> за флагами функций | sqlx/redis/reqwest/… приходят уже классифицированными |
.context() работает ровно так, как вы ожидаете:
use masterror::{AppResult, ResultExt};
fn read_config(path: &str) -> AppResult<String> {
let content = std::fs::read_to_string(path).context("Failed to read config file")?;
Ok(content)
}
ensure!/fail! меняют строковое форматирование anyhow на типизированные
ошибки:
use masterror::{AppError, AppResult, ensure, fail, field};
fn parse(content: &str, path: &str) -> AppResult<()> {
ensure!(
!content.is_empty(),
AppError::bad_request("Config file is empty")
.with_field(field::str("path", path.to_owned()))
);
if content.starts_with("invalid") {
fail!(AppError::bad_request("Invalid config format"));
}
Ok(())
}
Выражение ошибки вычисляется только при срабатывании условия, поэтому счастливый путь остаётся без выделений памяти — та же гарантия, что даёт anyhow, плюс машиночитаемый код.
Где у anyhow нет эквивалента
Миграция даёт возможности, у которых нет аналога в anyhow:
- Типизированная таксономия —
AppErrorKind(внутренний) иAppCode(публичный, SCREAMING_SNAKE_CASE) вместо строкового контекста. См. Виды и коды ошибок. - Транспортные отображения — RFC 7807
problem+jsonдля Axum/Actix иtonic::Statusдля gRPC, выводимые из одной таблицы кодов. См. Веб-фреймворки. - Телеметрия — автоматические события
tracing, метрикиerror_total{code,category}и ленивые бэктрейсы на границе. См. Наблюдаемость. - Редактирование — сообщения
redactable()и политикиHash/Last4/Redactдля каждого поля, соблюдаемые каждым транспортом. См. Лучшие практики. - Структурированные метаданные — типизированные
field::str/u64/duration/ip/...вместо форматирования значений в сообщение.
На что обратить внимание
- У формы anyhow
ensure!(cond, "format {}", x)с форматированным сообщением нет прямого двойника: конструируйте ошибку явно (AppError::bad_request(format!(...))) или, что лучше, используйте статическое сообщение плюс поля метаданных. anyhow::Errorпринимает любойE: Error + Send + Sync. В masterror вы выбираете вид в момент оборачивания (Context::new(kind)или конверсияFrom) — эта точка принятия решения и есть возможность, а не трение: именно здесь происходит классификация.- И
ensure!, иfail!разворачиваются вreturn Err(...), поэтому работают в любой функции, возвращающейResult<_, E>, где ваше выражение уже является типом ошибки — конверсияIntoне вставляется.
См. также: Начало работы · Derive-макросы · Контекст и метаданные · Лучшие практики