diff --git a/CHANGELOG.md b/CHANGELOG.md index 9bb6c3a..55d1a36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,14 @@ All notable changes to this project will be documented in this file. ### Added - _Nothing yet._ +## [0.5.13] - 2025-10-05 + +### Documentation +- Documented the formatter trait helpers (`TemplateFormatter::is_alternate`, + `TemplateFormatter::from_kind`, and `TemplateFormatterKind::specifier`/`supports_alternate`) + across README variants and crate docs, including guidance on the extended + formatter table and compatibility with `thiserror` v2. + ## [0.5.12] - 2025-10-04 ### Tests diff --git a/Cargo.lock b/Cargo.lock index 22f7026..9d969e3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1527,7 +1527,7 @@ dependencies = [ [[package]] name = "masterror" -version = "0.5.12" +version = "0.5.13" dependencies = [ "actix-web", "axum", diff --git a/Cargo.toml b/Cargo.toml index 06bccb8..a23bf9f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "masterror" -version = "0.5.12" +version = "0.5.13" rust-version = "1.90" edition = "2024" license = "MIT OR Apache-2.0" diff --git a/README.md b/README.md index e079b4e..4c7ddd3 100644 --- a/README.md +++ b/README.md @@ -29,9 +29,9 @@ Stable categories, conservative HTTP mapping, no `unsafe`. ~~~toml [dependencies] -masterror = { version = "0.5.12", default-features = false } +masterror = { version = "0.5.13", default-features = false } # or with features: -# masterror = { version = "0.5.12", features = [ +# masterror = { version = "0.5.13", features = [ # "axum", "actix", "openapi", "serde_json", # "sqlx", "sqlx-migrate", "reqwest", "redis", # "validator", "config", "tokio", "multipart", @@ -66,10 +66,10 @@ masterror = { version = "0.5.12", default-features = false } ~~~toml [dependencies] # lean core -masterror = { version = "0.5.12", default-features = false } +masterror = { version = "0.5.13", default-features = false } # with Axum/Actix + JSON + integrations -# masterror = { version = "0.5.12", features = [ +# masterror = { version = "0.5.13", features = [ # "axum", "actix", "openapi", "serde_json", # "sqlx", "sqlx-migrate", "reqwest", "redis", # "validator", "config", "tokio", "multipart", @@ -162,20 +162,34 @@ assert_eq!(wrapped.to_string(), "I/O failed: disk offline"); #### 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` | +formatters via the same specifiers supported by `thiserror` v2. +`TemplateFormatter::is_alternate()` tracks the `#` flag, while +`TemplateFormatterKind` exposes the underlying `core::fmt` trait so derived +code can branch on the requested renderer without manual pattern matching. +Unsupported formatters surface a compile error that mirrors `thiserror`'s +diagnostics. + +| Specifier | `core::fmt` trait | Example output | Notes | +|------------------|----------------------------|------------------------|-------| +| _default_ | `core::fmt::Display` | `value` | User-facing strings; `#` has no effect. | +| `:?` / `:#?` | `core::fmt::Debug` | `Struct { .. }` / multi-line | Mirrors `Debug`; `#` pretty-prints structs. | +| `:x` / `:#x` | `core::fmt::LowerHex` | `0x2a` | Hexadecimal; `#` prepends `0x`. | +| `:X` / `:#X` | `core::fmt::UpperHex` | `0x2A` | Uppercase hex; `#` prepends `0x`. | +| `:p` / `:#p` | `core::fmt::Pointer` | `0x1f00` / `0x1f00` | Raw pointers; `#` is accepted for compatibility. | +| `:b` / `:#b` | `core::fmt::Binary` | `101010` / `0b101010` | Binary; `#` prepends `0b`. | +| `:o` / `:#o` | `core::fmt::Octal` | `52` / `0o52` | Octal; `#` prepends `0o`. | +| `:e` / `:#e` | `core::fmt::LowerExp` | `1.5e-2` | Scientific notation; `#` forces the decimal point. | +| `:E` / `:#E` | `core::fmt::UpperExp` | `1.5E-2` | Uppercase scientific; `#` forces the decimal point. | + +- `TemplateFormatterKind::supports_alternate()` reports whether the `#` flag is + meaningful for the requested trait (pointer accepts it even though the output + matches the non-alternate form). +- `TemplateFormatterKind::specifier()` returns the canonical format specifier + character when one exists, enabling custom derives to re-render placeholders + in their original style. +- `TemplateFormatter::from_kind(kind, alternate)` reconstructs a formatter from + the lightweight `TemplateFormatterKind`, making it easy to toggle the + alternate flag in generated code. ~~~rust use core::ptr; @@ -222,18 +236,38 @@ let template = ErrorTemplate::parse("{code:#x} → {payload:?}").expect("parse") let mut placeholders = template.placeholders(); let code = placeholders.next().expect("code placeholder"); +let code_formatter = code.formatter(); assert!(matches!( - code.formatter(), + code_formatter, TemplateFormatter::LowerHex { alternate: true } )); -assert_eq!(code.formatter().kind(), TemplateFormatterKind::LowerHex); -assert!(code.formatter().is_alternate()); +let code_kind = code_formatter.kind(); +assert_eq!(code_kind, TemplateFormatterKind::LowerHex); +assert!(code_formatter.is_alternate()); +assert_eq!(code_kind.specifier(), Some('x')); +assert!(code_kind.supports_alternate()); +let lowered = TemplateFormatter::from_kind(code_kind, false); +assert!(matches!( + lowered, + TemplateFormatter::LowerHex { alternate: false } +)); let payload = placeholders.next().expect("payload placeholder"); +let payload_formatter = payload.formatter(); assert_eq!( - payload.formatter(), + payload_formatter, TemplateFormatter::Debug { alternate: false } ); +let payload_kind = payload_formatter.kind(); +assert_eq!(payload_kind, TemplateFormatterKind::Debug); +assert_eq!(payload_kind.specifier(), Some('?')); +assert!(payload_kind.supports_alternate()); +let pretty_debug = TemplateFormatter::from_kind(payload_kind, true); +assert!(matches!( + pretty_debug, + TemplateFormatter::Debug { alternate: true } +)); +assert!(pretty_debug.is_alternate()); ~~~ > **Compatibility with `thiserror` v2:** the derive understands the extended @@ -349,13 +383,13 @@ assert_eq!(resp.status, 401); Minimal core: ~~~toml -masterror = { version = "0.5.12", default-features = false } +masterror = { version = "0.5.13", default-features = false } ~~~ API (Axum + JSON + deps): ~~~toml -masterror = { version = "0.5.12", features = [ +masterror = { version = "0.5.13", features = [ "axum", "serde_json", "openapi", "sqlx", "reqwest", "redis", "validator", "config", "tokio" ] } @@ -364,7 +398,7 @@ masterror = { version = "0.5.12", features = [ API (Actix + JSON + deps): ~~~toml -masterror = { version = "0.5.12", features = [ +masterror = { version = "0.5.13", features = [ "actix", "serde_json", "openapi", "sqlx", "reqwest", "redis", "validator", "config", "tokio" ] } diff --git a/README.ru.md b/README.ru.md index 5bb4348..521a5ac 100644 --- a/README.ru.md +++ b/README.ru.md @@ -27,9 +27,9 @@ ~~~toml [dependencies] -masterror = { version = "0.5.7", default-features = false } +masterror = { version = "0.5.13", default-features = false } # или с нужными интеграциями -# masterror = { version = "0.5.7", features = [ +# masterror = { version = "0.5.13", features = [ # "axum", "actix", "openapi", "serde_json", # "sqlx", "sqlx-migrate", "reqwest", "redis", # "validator", "config", "tokio", "multipart", @@ -81,23 +81,30 @@ fn do_work(flag: bool) -> AppResult<()> { ## Форматирование шаблонов `#[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` | +подстановка может запросить другой форматтер. +`TemplateFormatter::is_alternate()` фиксирует флаг `#`, а `TemplateFormatterKind` +сообщает, какой трейт `core::fmt` нужен, поэтому порождённый код может +переключаться между вариантами без ручного `match`. Неподдержанные спецификаторы +приводят к диагностике на этапе компиляции, совпадающей с `thiserror`. + +| Спецификатор | Трейт | Пример результата | Примечания | +|------------------|-------------------------|--------------------------|------------| +| _по умолчанию_ | `core::fmt::Display` | `value` | Пользовательские сообщения; `#` игнорируется. | +| `:?` / `:#?` | `core::fmt::Debug` | `Struct { .. }` / многострочный | Поведение `Debug`; `#` включает pretty-print. | +| `:x` / `:#x` | `core::fmt::LowerHex` | `0x2a` | Шестнадцатеричный вывод; `#` добавляет `0x`. | +| `:X` / `:#X` | `core::fmt::UpperHex` | `0x2A` | Верхний регистр; `#` добавляет `0x`. | +| `:p` / `:#p` | `core::fmt::Pointer` | `0x1f00` / `0x1f00` | Сырые указатели; `#` поддерживается для совместимости. | +| `:b` / `:#b` | `core::fmt::Binary` | `101010` / `0b101010` | Двоичный вывод; `#` добавляет `0b`. | +| `:o` / `:#o` | `core::fmt::Octal` | `52` / `0o52` | Восьмеричный вывод; `#` добавляет `0o`. | +| `:e` / `:#e` | `core::fmt::LowerExp` | `1.5e-2` | Научная запись; `#` заставляет выводить десятичную точку. | +| `:E` / `:#E` | `core::fmt::UpperExp` | `1.5E-2` | Верхний регистр научной записи; `#` заставляет выводить точку. | + +- `TemplateFormatterKind::supports_alternate()` сообщает, имеет ли смысл `#` для + выбранного трейта (для указателей вывод совпадает с обычным). +- `TemplateFormatterKind::specifier()` возвращает канонический символ + спецификатора, что упрощает повторный рендеринг плейсхолдеров. +- `TemplateFormatter::from_kind(kind, alternate)` собирает форматтер из + `TemplateFormatterKind`, позволяя программно переключать флаг `#`. ~~~rust use core::ptr; @@ -148,18 +155,38 @@ let template = ErrorTemplate::parse("{code:#x} → {payload:?}").expect("parse") let mut placeholders = template.placeholders(); let code = placeholders.next().expect("code placeholder"); +let code_formatter = code.formatter(); assert!(matches!( - code.formatter(), + code_formatter, TemplateFormatter::LowerHex { alternate: true } )); -assert_eq!(code.formatter().kind(), TemplateFormatterKind::LowerHex); -assert!(code.formatter().is_alternate()); +let code_kind = code_formatter.kind(); +assert_eq!(code_kind, TemplateFormatterKind::LowerHex); +assert!(code_formatter.is_alternate()); +assert_eq!(code_kind.specifier(), Some('x')); +assert!(code_kind.supports_alternate()); +let lowered = TemplateFormatter::from_kind(code_kind, false); +assert!(matches!( + lowered, + TemplateFormatter::LowerHex { alternate: false } +)); let payload = placeholders.next().expect("payload placeholder"); +let payload_formatter = payload.formatter(); assert_eq!( - payload.formatter(), + payload_formatter, TemplateFormatter::Debug { alternate: false } ); +let payload_kind = payload_formatter.kind(); +assert_eq!(payload_kind, TemplateFormatterKind::Debug); +assert_eq!(payload_kind.specifier(), Some('?')); +assert!(payload_kind.supports_alternate()); +let pretty_debug = TemplateFormatter::from_kind(payload_kind, true); +assert!(matches!( + pretty_debug, + TemplateFormatter::Debug { alternate: true } +)); +assert!(pretty_debug.is_alternate()); ~~~ > **Совместимость с `thiserror` v2.** Доступные спецификаторы, сообщения об diff --git a/README.template.md b/README.template.md index a3d263d..637cfc0 100644 --- a/README.template.md +++ b/README.template.md @@ -156,20 +156,34 @@ assert_eq!(wrapped.to_string(), "I/O failed: disk offline"); #### 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` | +formatters via the same specifiers supported by `thiserror` v2. +`TemplateFormatter::is_alternate()` tracks the `#` flag, while +`TemplateFormatterKind` exposes the underlying `core::fmt` trait so derived +code can branch on the requested renderer without manual pattern matching. +Unsupported formatters surface a compile error that mirrors `thiserror`'s +diagnostics. + +| Specifier | `core::fmt` trait | Example output | Notes | +|------------------|----------------------------|------------------------|-------| +| _default_ | `core::fmt::Display` | `value` | User-facing strings; `#` has no effect. | +| `:?` / `:#?` | `core::fmt::Debug` | `Struct { .. }` / multi-line | Mirrors `Debug`; `#` pretty-prints structs. | +| `:x` / `:#x` | `core::fmt::LowerHex` | `0x2a` | Hexadecimal; `#` prepends `0x`. | +| `:X` / `:#X` | `core::fmt::UpperHex` | `0x2A` | Uppercase hex; `#` prepends `0x`. | +| `:p` / `:#p` | `core::fmt::Pointer` | `0x1f00` / `0x1f00` | Raw pointers; `#` is accepted for compatibility. | +| `:b` / `:#b` | `core::fmt::Binary` | `101010` / `0b101010` | Binary; `#` prepends `0b`. | +| `:o` / `:#o` | `core::fmt::Octal` | `52` / `0o52` | Octal; `#` prepends `0o`. | +| `:e` / `:#e` | `core::fmt::LowerExp` | `1.5e-2` | Scientific notation; `#` forces the decimal point. | +| `:E` / `:#E` | `core::fmt::UpperExp` | `1.5E-2` | Uppercase scientific; `#` forces the decimal point. | + +- `TemplateFormatterKind::supports_alternate()` reports whether the `#` flag is + meaningful for the requested trait (pointer accepts it even though the output + matches the non-alternate form). +- `TemplateFormatterKind::specifier()` returns the canonical format specifier + character when one exists, enabling custom derives to re-render placeholders + in their original style. +- `TemplateFormatter::from_kind(kind, alternate)` reconstructs a formatter from + the lightweight `TemplateFormatterKind`, making it easy to toggle the + alternate flag in generated code. ~~~rust use core::ptr; @@ -216,18 +230,38 @@ let template = ErrorTemplate::parse("{code:#x} → {payload:?}").expect("parse") let mut placeholders = template.placeholders(); let code = placeholders.next().expect("code placeholder"); +let code_formatter = code.formatter(); assert!(matches!( - code.formatter(), + code_formatter, TemplateFormatter::LowerHex { alternate: true } )); -assert_eq!(code.formatter().kind(), TemplateFormatterKind::LowerHex); -assert!(code.formatter().is_alternate()); +let code_kind = code_formatter.kind(); +assert_eq!(code_kind, TemplateFormatterKind::LowerHex); +assert!(code_formatter.is_alternate()); +assert_eq!(code_kind.specifier(), Some('x')); +assert!(code_kind.supports_alternate()); +let lowered = TemplateFormatter::from_kind(code_kind, false); +assert!(matches!( + lowered, + TemplateFormatter::LowerHex { alternate: false } +)); let payload = placeholders.next().expect("payload placeholder"); +let payload_formatter = payload.formatter(); assert_eq!( - payload.formatter(), + payload_formatter, TemplateFormatter::Debug { alternate: false } ); +let payload_kind = payload_formatter.kind(); +assert_eq!(payload_kind, TemplateFormatterKind::Debug); +assert_eq!(payload_kind.specifier(), Some('?')); +assert!(payload_kind.supports_alternate()); +let pretty_debug = TemplateFormatter::from_kind(payload_kind, true); +assert!(matches!( + pretty_debug, + TemplateFormatter::Debug { alternate: true } +)); +assert!(pretty_debug.is_alternate()); ~~~ > **Compatibility with `thiserror` v2:** the derive understands the extended diff --git a/src/error.rs b/src/error.rs index 22eb4cc..4022add 100644 --- a/src/error.rs +++ b/src/error.rs @@ -15,6 +15,10 @@ //! `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. +//! `TemplateFormatter::is_alternate()` surfaces the `#` flag, and +//! [`TemplateFormatterKind`](crate::error::template::TemplateFormatterKind) +//! describes the underlying `core::fmt` trait with helpers like +//! `specifier()`/`supports_alternate()` for programmatic inspection. //! //! ```rust //! use core::ptr; @@ -56,26 +60,52 @@ //! formatters via [`ErrorTemplate`](crate::error::template::ErrorTemplate): //! //! ```rust -//! use masterror::error::template::{ErrorTemplate, TemplateFormatter}; +//! use masterror::error::template::{ErrorTemplate, TemplateFormatter, TemplateFormatterKind}; //! //! let template = ErrorTemplate::parse("{code:#x} → {payload:?}").expect("parse"); //! let mut placeholders = template.placeholders(); //! //! let code = placeholders.next().expect("code placeholder"); +//! let code_formatter = code.formatter(); //! assert!(matches!( -//! code.formatter(), +//! code_formatter, //! TemplateFormatter::LowerHex { //! alternate: true //! } //! )); +//! let code_kind = code_formatter.kind(); +//! assert_eq!(code_kind, TemplateFormatterKind::LowerHex); +//! assert!(code_formatter.is_alternate()); +//! assert_eq!(code_kind.specifier(), Some('x')); +//! assert!(code_kind.supports_alternate()); +//! let lowered = TemplateFormatter::from_kind(code_kind, false); +//! assert!(matches!( +//! lowered, +//! TemplateFormatter::LowerHex { +//! alternate: false +//! } +//! )); //! //! let payload = placeholders.next().expect("payload placeholder"); +//! let payload_formatter = payload.formatter(); //! assert_eq!( -//! payload.formatter(), +//! payload_formatter, //! TemplateFormatter::Debug { //! alternate: false //! } //! ); +//! let payload_kind = payload_formatter.kind(); +//! assert_eq!(payload_kind, TemplateFormatterKind::Debug); +//! assert_eq!(payload_kind.specifier(), Some('?')); +//! assert!(payload_kind.supports_alternate()); +//! let pretty_debug = TemplateFormatter::from_kind(payload_kind, true); +//! assert!(matches!( +//! pretty_debug, +//! TemplateFormatter::Debug { +//! alternate: true +//! } +//! )); +//! assert!(pretty_debug.is_alternate()); //! ``` /// Parser and formatter helpers for `#[error("...")]` templates.