Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
84 changes: 59 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"
] }
Expand All @@ -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"
] }
Expand Down
73 changes: 50 additions & 23 deletions README.ru.md
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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.** Доступные спецификаторы, сообщения об
Expand Down
70 changes: 52 additions & 18 deletions README.template.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading