From 498df9f7ea2b9dae1c6aef4b4dcd3ccdb2672497 Mon Sep 17 00:00:00 2001 From: RA <70325462+RAprogramm@users.noreply.github.com> Date: Fri, 19 Sep 2025 13:22:56 +0700 Subject: [PATCH] Document formatter traits and bump to 0.5.7 --- CHANGELOG.md | 10 +++++ Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 94 +++++++++++++++++++++++++++++++++++++++++---- README.ru.md | 96 +++++++++++++++++++++++++++++++++++++++++++--- README.template.md | 80 ++++++++++++++++++++++++++++++++++++++ src/error.rs | 68 ++++++++++++++++++++++++++++++++ 7 files changed, 338 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b42f842..74a8624 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,11 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +### Added +- _Nothing yet._ + +## [0.5.7] - 2025-09-29 + ### Added - `masterror::error::template` module providing a parsed representation of `#[error("...")]` strings and a formatter hook for future custom derives. @@ -17,6 +22,11 @@ All notable changes to this project will be documented in this file. - `masterror::Error` now uses the in-tree derive, removing the dependency on `thiserror` while keeping the same runtime behaviour and diagnostics. +### Documentation +- Documented formatter trait usage across README.md, README.ru.md and the + `masterror::error` module, noting compatibility with `thiserror` v2 and + demonstrating programmatic `TemplateFormatter` inspection. + ## [0.5.6] - 2025-09-28 ### Tests diff --git a/Cargo.lock b/Cargo.lock index 88861ba..92f93b5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1527,7 +1527,7 @@ dependencies = [ [[package]] name = "masterror" -version = "0.5.6" +version = "0.5.7" dependencies = [ "actix-web", "axum", diff --git a/Cargo.toml b/Cargo.toml index 4779d51..a246b48 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "masterror" -version = "0.5.6" +version = "0.5.7" rust-version = "1.90" edition = "2024" license = "MIT OR Apache-2.0" diff --git a/README.md b/README.md index 551a9f0..51b21a6 100644 --- a/README.md +++ b/README.md @@ -29,9 +29,9 @@ Stable categories, conservative HTTP mapping, no `unsafe`. ~~~toml [dependencies] -masterror = { version = "0.5.6", default-features = false } +masterror = { version = "0.5.7", default-features = false } # or with features: -# masterror = { version = "0.5.6", features = [ +# masterror = { version = "0.5.7", features = [ # "axum", "actix", "openapi", "serde_json", # "sqlx", "sqlx-migrate", "reqwest", "redis", # "validator", "config", "tokio", "multipart", @@ -66,10 +66,10 @@ masterror = { version = "0.5.6", default-features = false } ~~~toml [dependencies] # lean core -masterror = { version = "0.5.6", default-features = false } +masterror = { version = "0.5.7", default-features = false } # with Axum/Actix + JSON + integrations -# masterror = { version = "0.5.6", features = [ +# masterror = { version = "0.5.7", features = [ # "axum", "actix", "openapi", "serde_json", # "sqlx", "sqlx-migrate", "reqwest", "redis", # "validator", "config", "tokio", "multipart", @@ -152,6 +152,86 @@ assert_eq!(wrapped.to_string(), "I/O failed: disk offline"); - `masterror::error::template::ErrorTemplate` parses `#[error("...")]` strings, exposing literal and placeholder segments so custom derives can be implemented without relying on `thiserror`. +- `TemplateFormatter` mirrors `thiserror`'s formatter detection so existing + derives that relied on hexadecimal, pointer or exponential renderers keep + compiling. + +#### Formatter traits + +Placeholders default to `Display` (`{value}`) but can opt into richer +formatters via the same specifiers supported by `thiserror` v2. Unsupported +formatters surface a compile error that mirrors `thiserror`'s diagnostics. + +| Specifier | `core::fmt` trait | Example output | +|------------------|----------------------------|------------------------| +| _default_ | `core::fmt::Display` | `value` | +| `:?` / `:#?` | `core::fmt::Debug` | `Struct { .. }` / multi-line | +| `:x` / `:#x` | `core::fmt::LowerHex` | `0x2a` | +| `:X` / `:#X` | `core::fmt::UpperHex` | `0x2A` | +| `:p` / `:#p` | `core::fmt::Pointer` | `0x1f00` / `0x1f00` | +| `:b` / `:#b` | `core::fmt::Binary` | `101010` / `0b101010` | +| `:o` / `:#o` | `core::fmt::Octal` | `52` / `0o52` | +| `:e` / `:#e` | `core::fmt::LowerExp` | `1.5e-2` | +| `:E` / `:#E` | `core::fmt::UpperExp` | `1.5E-2` | + +~~~rust +use core::ptr; + +use masterror::Error; + +#[derive(Debug, Error)] +#[error( + "debug={payload:?}, hex={id:#x}, ptr={ptr:p}, bin={mask:#b}, \ + oct={mask:o}, lower={ratio:e}, upper={ratio:E}" +)] +struct FormattedError { + id: u32, + payload: String, + ptr: *const u8, + mask: u8, + ratio: f32, +} + +let err = FormattedError { + id: 0x2a, + payload: "hello".into(), + ptr: ptr::null(), + mask: 0b1010_0001, + ratio: 0.15625, +}; + +let rendered = err.to_string(); +assert!(rendered.contains("debug=\"hello\"")); +assert!(rendered.contains("hex=0x2a")); +assert!(rendered.contains("ptr=0x0")); +assert!(rendered.contains("bin=0b10100001")); +assert!(rendered.contains("oct=241")); +assert!(rendered.contains("lower=1.5625e-1")); +assert!(rendered.contains("upper=1.5625E-1")); +~~~ + +~~~rust +use masterror::error::template::{ErrorTemplate, TemplateFormatter}; + +let template = ErrorTemplate::parse("{code:#x} → {payload:?}").expect("parse"); +let mut placeholders = template.placeholders(); + +let code = placeholders.next().expect("code placeholder"); +assert!(matches!( + code.formatter(), + TemplateFormatter::LowerHex { alternate: true } +)); + +let payload = placeholders.next().expect("payload placeholder"); +assert_eq!( + payload.formatter(), + TemplateFormatter::Debug { alternate: false } +); +~~~ + +> **Compatibility with `thiserror` v2:** the derive understands the extended +> formatter set introduced in `thiserror` 2.x and reports identical diagnostics +> for unsupported specifiers, so migrating existing derives is drop-in. ```rust use masterror::error::template::{ErrorTemplate, TemplateIdentifier}; @@ -262,13 +342,13 @@ assert_eq!(resp.status, 401); Minimal core: ~~~toml -masterror = { version = "0.5.6", default-features = false } +masterror = { version = "0.5.7", default-features = false } ~~~ API (Axum + JSON + deps): ~~~toml -masterror = { version = "0.5.6", features = [ +masterror = { version = "0.5.7", features = [ "axum", "serde_json", "openapi", "sqlx", "reqwest", "redis", "validator", "config", "tokio" ] } @@ -277,7 +357,7 @@ masterror = { version = "0.5.6", features = [ API (Actix + JSON + deps): ~~~toml -masterror = { version = "0.5.6", features = [ +masterror = { version = "0.5.7", features = [ "actix", "serde_json", "openapi", "sqlx", "reqwest", "redis", "validator", "config", "tokio" ] } diff --git a/README.ru.md b/README.ru.md index 23f5f26..7fb065a 100644 --- a/README.ru.md +++ b/README.ru.md @@ -27,13 +27,13 @@ ~~~toml [dependencies] -masterror = { version = "0.5.2", default-features = false } +masterror = { version = "0.5.7", default-features = false } # или с нужными интеграциями -# masterror = { version = "0.5.2", features = [ +# masterror = { version = "0.5.7", features = [ # "axum", "actix", "openapi", "serde_json", -# "sqlx", "reqwest", "redis", "validator", -# "config", "tokio", "multipart", "teloxide", -# "telegram-webapp-sdk", "frontend", "turnkey" +# "sqlx", "sqlx-migrate", "reqwest", "redis", +# "validator", "config", "tokio", "multipart", +# "teloxide", "telegram-webapp-sdk", "frontend", "turnkey" # ] } ~~~ @@ -66,15 +66,101 @@ fn do_work(flag: bool) -> AppResult<()> { ## Дополнительные интеграции - `sqlx` — классификация `sqlx::Error` по видам ошибок. +- `sqlx-migrate` — обработка `sqlx::migrate::MigrateError` как базы данных. - `reqwest` — перевод сетевых/HTTP-сбоев в доменные категории. - `redis` — корректная обработка ошибок кеша. - `validator` — преобразование `ValidationErrors` в валидационные ошибки API. - `config` — типизированные ошибки конфигурации. - `tokio` — маппинг таймаутов (`tokio::time::error::Elapsed`). - `multipart` — обработка ошибок извлечения multipart в Axum. +- `teloxide` — маппинг `teloxide_core::RequestError` в доменные категории. +- `telegram-webapp-sdk` — обработка ошибок валидации данных Telegram WebApp. - `frontend` — логирование в браузере и преобразование в `JsValue` для WASM. - `turnkey` — расширение таксономии для Turnkey SDK. +## Форматирование шаблонов `#[error]` + +Шаблон `#[error("...")]` по умолчанию использует `Display`, но любая +подстановка может запросить другой форматтер. `masterror::Error` понимает тот же +набор спецификаторов, что и `thiserror` v2: `:?`, `:x`, `:X`, `:p`, `:b`, `:o`, +`:e`, `:E`, а также их версии с `#` для альтернативного вывода. Неподдержанные +форматтеры приводят к диагностике на этапе компиляции, совпадающей с +`thiserror`. + +| Спецификатор | Трейт | Пример результата | +|------------------|-------------------------|--------------------------| +| _по умолчанию_ | `core::fmt::Display` | `value` | +| `:?` / `:#?` | `core::fmt::Debug` | `Struct { .. }` / многострочный | +| `:x` / `:#x` | `core::fmt::LowerHex` | `0x2a` | +| `:X` / `:#X` | `core::fmt::UpperHex` | `0x2A` | +| `:p` / `:#p` | `core::fmt::Pointer` | `0x1f00` / `0x1f00` | +| `:b` / `:#b` | `core::fmt::Binary` | `101010` / `0b101010` | +| `:o` / `:#o` | `core::fmt::Octal` | `52` / `0o52` | +| `:e` / `:#e` | `core::fmt::LowerExp` | `1.5e-2` | +| `:E` / `:#E` | `core::fmt::UpperExp` | `1.5E-2` | + +~~~rust +use core::ptr; + +use masterror::Error; + +#[derive(Debug, Error)] +#[error( + "debug={payload:?}, hex={id:#x}, ptr={ptr:p}, bin={mask:#b}, \ + oct={mask:o}, lower={ratio:e}, upper={ratio:E}" +)] +struct FormatterDemo { + id: u32, + payload: String, + ptr: *const u8, + mask: u8, + ratio: f32, +} + +let err = FormatterDemo { + id: 0x2a, + payload: "hello".into(), + ptr: ptr::null(), + mask: 0b1010_0001, + ratio: 0.15625, +}; + +let rendered = err.to_string(); +assert!(rendered.contains("debug=\"hello\"")); +assert!(rendered.contains("hex=0x2a")); +assert!(rendered.contains("ptr=0x0")); +assert!(rendered.contains("bin=0b10100001")); +assert!(rendered.contains("oct=241")); +assert!(rendered.contains("lower=1.5625e-1")); +assert!(rendered.contains("upper=1.5625E-1")); +~~~ + +`masterror::error::template::ErrorTemplate` позволяет разобрать шаблон и +программно проверить запрошенные форматтеры: + +~~~rust +use masterror::error::template::{ErrorTemplate, TemplateFormatter}; + +let template = ErrorTemplate::parse("{code:#x} → {payload:?}").expect("parse"); +let mut placeholders = template.placeholders(); + +let code = placeholders.next().expect("code placeholder"); +assert!(matches!( + code.formatter(), + TemplateFormatter::LowerHex { alternate: true } +)); + +let payload = placeholders.next().expect("payload placeholder"); +assert_eq!( + payload.formatter(), + TemplateFormatter::Debug { alternate: false } +); +~~~ + +> **Совместимость с `thiserror` v2.** Доступные спецификаторы, сообщения об +> ошибках и поведение совпадают с `thiserror` 2.x, поэтому миграция с +> `thiserror::Error` на `masterror::Error` не требует переписывать шаблоны. + ## Лицензия Проект распространяется по лицензии Apache-2.0 или MIT на ваш выбор. diff --git a/README.template.md b/README.template.md index 79c2cdb..9f72b55 100644 --- a/README.template.md +++ b/README.template.md @@ -146,6 +146,86 @@ assert_eq!(wrapped.to_string(), "I/O failed: disk offline"); - `masterror::error::template::ErrorTemplate` parses `#[error("...")]` strings, exposing literal and placeholder segments so custom derives can be implemented without relying on `thiserror`. +- `TemplateFormatter` mirrors `thiserror`'s formatter detection so existing + derives that relied on hexadecimal, pointer or exponential renderers keep + compiling. + +#### Formatter traits + +Placeholders default to `Display` (`{value}`) but can opt into richer +formatters via the same specifiers supported by `thiserror` v2. Unsupported +formatters surface a compile error that mirrors `thiserror`'s diagnostics. + +| Specifier | `core::fmt` trait | Example output | +|------------------|----------------------------|------------------------| +| _default_ | `core::fmt::Display` | `value` | +| `:?` / `:#?` | `core::fmt::Debug` | `Struct { .. }` / multi-line | +| `:x` / `:#x` | `core::fmt::LowerHex` | `0x2a` | +| `:X` / `:#X` | `core::fmt::UpperHex` | `0x2A` | +| `:p` / `:#p` | `core::fmt::Pointer` | `0x1f00` / `0x1f00` | +| `:b` / `:#b` | `core::fmt::Binary` | `101010` / `0b101010` | +| `:o` / `:#o` | `core::fmt::Octal` | `52` / `0o52` | +| `:e` / `:#e` | `core::fmt::LowerExp` | `1.5e-2` | +| `:E` / `:#E` | `core::fmt::UpperExp` | `1.5E-2` | + +~~~rust +use core::ptr; + +use masterror::Error; + +#[derive(Debug, Error)] +#[error( + "debug={payload:?}, hex={id:#x}, ptr={ptr:p}, bin={mask:#b}, \ + oct={mask:o}, lower={ratio:e}, upper={ratio:E}" +)] +struct FormattedError { + id: u32, + payload: String, + ptr: *const u8, + mask: u8, + ratio: f32, +} + +let err = FormattedError { + id: 0x2a, + payload: "hello".into(), + ptr: ptr::null(), + mask: 0b1010_0001, + ratio: 0.15625, +}; + +let rendered = err.to_string(); +assert!(rendered.contains("debug=\"hello\"")); +assert!(rendered.contains("hex=0x2a")); +assert!(rendered.contains("ptr=0x0")); +assert!(rendered.contains("bin=0b10100001")); +assert!(rendered.contains("oct=241")); +assert!(rendered.contains("lower=1.5625e-1")); +assert!(rendered.contains("upper=1.5625E-1")); +~~~ + +~~~rust +use masterror::error::template::{ErrorTemplate, TemplateFormatter}; + +let template = ErrorTemplate::parse("{code:#x} → {payload:?}").expect("parse"); +let mut placeholders = template.placeholders(); + +let code = placeholders.next().expect("code placeholder"); +assert!(matches!( + code.formatter(), + TemplateFormatter::LowerHex { alternate: true } +)); + +let payload = placeholders.next().expect("payload placeholder"); +assert_eq!( + payload.formatter(), + TemplateFormatter::Debug { alternate: false } +); +~~~ + +> **Compatibility with `thiserror` v2:** the derive understands the extended +> formatter set introduced in `thiserror` 2.x and reports identical diagnostics +> for unsupported specifiers, so migrating existing derives is drop-in. ```rust use masterror::error::template::{ErrorTemplate, TemplateIdentifier}; diff --git a/src/error.rs b/src/error.rs index 80ebef3..22eb4cc 100644 --- a/src/error.rs +++ b/src/error.rs @@ -9,6 +9,74 @@ //! The API is intentionally low level. It makes no assumptions about how the //! parsed data is going to be used and instead provides precise spans and //! formatting metadata that higher-level code can rely on. +//! +//! ## Formatter traits +//! +//! `TemplateFormatter` enumerates the formatting modes supported by +//! `#[error("...")]` placeholders. It mirrors the formatter detection logic in +//! `thiserror` v2 so migrating existing derives is a drop-in change. +//! +//! ```rust +//! use core::ptr; +//! +//! use masterror::Error; +//! +//! #[derive(Debug, Error)] +//! #[error( +//! "debug={payload:?}, hex={id:#x}, ptr={ptr:p}, bin={mask:#b}, \ +//! oct={mask:o}, lower={ratio:e}, upper={ratio:E}" +//! )] +//! struct FormatterShowcase { +//! id: u32, +//! payload: String, +//! ptr: *const u8, +//! mask: u8, +//! ratio: f32 +//! } +//! +//! let err = FormatterShowcase { +//! id: 0x2a, +//! payload: "hello".into(), +//! ptr: ptr::null(), +//! mask: 0b1010_0001, +//! ratio: 0.15625 +//! }; +//! +//! let rendered = err.to_string(); +//! assert!(rendered.contains("debug=\"hello\"")); +//! assert!(rendered.contains("hex=0x2a")); +//! assert!(rendered.contains("ptr=0x0")); +//! assert!(rendered.contains("bin=0b10100001")); +//! assert!(rendered.contains("oct=241")); +//! assert!(rendered.contains("lower=1.5625e-1")); +//! assert!(rendered.contains("upper=1.5625E-1")); +//! ``` +//! +//! Programmatic consumers can inspect placeholders and their requested +//! formatters via [`ErrorTemplate`](crate::error::template::ErrorTemplate): +//! +//! ```rust +//! use masterror::error::template::{ErrorTemplate, TemplateFormatter}; +//! +//! let template = ErrorTemplate::parse("{code:#x} → {payload:?}").expect("parse"); +//! let mut placeholders = template.placeholders(); +//! +//! let code = placeholders.next().expect("code placeholder"); +//! assert!(matches!( +//! code.formatter(), +//! TemplateFormatter::LowerHex { +//! alternate: true +//! } +//! )); +//! +//! let payload = placeholders.next().expect("payload placeholder"); +//! assert_eq!( +//! payload.formatter(), +//! TemplateFormatter::Debug { +//! alternate: false +//! } +//! ); +//! ``` /// Parser and formatter helpers for `#[error("...")]` templates. pub mod template;