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
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,28 @@ All notable changes to this project will be documented in this file.

## [Unreleased]

## [0.13.0] - 2025-09-23

### Added
- Introduced `#[derive(Masterror)]` and the `#[masterror(...)]` attribute to
convert domain errors directly into [`masterror::Error`] while capturing
metadata, message redaction policy and optional transport mappings.
- Added transport mapping descriptors in `mapping::{HttpMapping, GrpcMapping,
ProblemMapping}` generated by the new derive for HTTP/gRPC/problem-json
integrations.

### Changed
- Re-exported the `Masterror` derive from the crate root alongside the existing
`Error` derive.

### Documentation
- Expanded crate docs and both READMEs with `Masterror` examples, telemetry
guidance and redaction policy notes.

### Tests
- Added integration tests and trybuild coverage exercising the
`#[masterror(...)]` attribute and generated mapping tables.

## [0.12.1] - 2025-10-30

### Added
Expand Down
4 changes: 2 additions & 2 deletions Cargo.lock

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

6 changes: 3 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "masterror"
version = "0.12.1"
version = "0.13.0"
rust-version = "1.90"
edition = "2024"
license = "MIT OR Apache-2.0"
Expand Down Expand Up @@ -71,11 +71,11 @@ turnkey = []
openapi = ["dep:utoipa"]

[workspace.dependencies]
masterror-derive = { version = "0.6.6" }
masterror-derive = { version = "0.7.0" }
masterror-template = { version = "0.3.6" }

[dependencies]
masterror-derive = { version = "0.6" }
masterror-derive = { version = "0.7" }
masterror-template = { workspace = true }
tracing = "0.1"

Expand Down
90 changes: 81 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ Core is framework-agnostic; integrations are opt-in via feature flags.
Stable categories, conservative HTTP mapping, no `unsafe`.

- Core types: `AppError`, `AppErrorKind`, `AppResult`, `AppCode`, `ErrorResponse`, `Metadata`
- Derive macros: `#[derive(Error)]`, `#[app_error]`, `#[provide]` for domain
mappings and structured telemetry
- Derive macros: `#[derive(Error)]`, `#[derive(Masterror)]`, `#[app_error]`,
`#[masterror(...)]`, `#[provide]` for domain mappings and structured
telemetry
- Optional Axum/Actix integration and browser/WASM console logging
- Optional OpenAPI schema (via `utoipa`)
- Structured metadata helpers via `field::*` builders
Expand All @@ -37,9 +38,9 @@ guides, comparisons with `thiserror`/`anyhow`, and troubleshooting recipes.

~~~toml
[dependencies]
masterror = { version = "0.12.1", default-features = false }
masterror = { version = "0.13.0", default-features = false }
# or with features:
# masterror = { version = "0.12.1", features = [
# masterror = { version = "0.13.0", features = [
# "axum", "actix", "openapi", "serde_json",
# "sqlx", "sqlx-migrate", "reqwest", "redis",
# "validator", "config", "tokio", "multipart",
Expand Down Expand Up @@ -75,10 +76,10 @@ masterror = { version = "0.12.1", default-features = false }
~~~toml
[dependencies]
# lean core
masterror = { version = "0.12.1", default-features = false }
masterror = { version = "0.13.0", default-features = false }

# with Axum/Actix + JSON + integrations
# masterror = { version = "0.12.1", features = [
# masterror = { version = "0.13.0", features = [
# "axum", "actix", "openapi", "serde_json",
# "sqlx", "sqlx-migrate", "reqwest", "redis",
# "validator", "config", "tokio", "multipart",
Expand Down Expand Up @@ -179,6 +180,77 @@ assert_eq!(wrapped.to_string(), "I/O failed: disk offline");
placeholder, making it easy to branch on the requested rendering behaviour
without manually matching every enum variant.

#### `#[derive(Masterror)]` and `#[masterror(...)]`

`#[derive(Masterror)]` wires a domain error directly into [`masterror::Error`],
augmenting it with metadata, redaction policy and optional transport mappings.
The accompanying `#[masterror(...)]` attribute mirrors the `#[app_error]`
syntax while remaining explicit about telemetry:

~~~rust
use masterror::{
mapping::HttpMapping, AppCode, AppErrorKind, Error, Masterror, MessageEditPolicy
};

#[derive(Debug, Masterror)]
#[error("user {user_id} missing flag {flag}")]
#[masterror(
code = AppCode::NotFound,
category = AppErrorKind::NotFound,
message,
redact(message),
telemetry(
Some(masterror::field::str("user_id", user_id.clone())),
attempt.map(|value| masterror::field::u64("attempt", value))
),
map.grpc = 5,
map.problem = "https://errors.example.com/not-found"
)]
struct MissingFlag {
user_id: String,
flag: &'static str,
attempt: Option<u64>,
#[source]
source: Option<std::io::Error>
}

let err = MissingFlag {
user_id: "alice".into(),
flag: "beta",
attempt: Some(2),
source: None
};
let converted: Error = err.into();
assert_eq!(converted.code, AppCode::NotFound);
assert_eq!(converted.kind, AppErrorKind::NotFound);
assert_eq!(converted.edit_policy, MessageEditPolicy::Redact);
assert!(converted.metadata().get("user_id").is_some());

assert_eq!(
MissingFlag::HTTP_MAPPING,
HttpMapping::new(AppCode::NotFound, AppErrorKind::NotFound)
);
~~~

- `code` / `category` pick the public [`AppCode`] and internal
[`AppErrorKind`].
- `message` forwards the formatted [`Display`] output as the safe public
message. Omit it to keep the message private.
- `redact(message)` flips [`MessageEditPolicy`] to redactable at the transport
boundary.
- `telemetry(...)` accepts expressions that evaluate to
`Option<masterror::Field>`. Each populated field is inserted into the
resulting [`Metadata`]; use `telemetry()` when no fields are attached.
- `map.grpc` / `map.problem` capture optional gRPC status codes (as `i32`) and
RFC 7807 `type` URIs. The derive emits tables such as
`MyError::HTTP_MAPPING`, `MyError::GRPC_MAPPING` and
`MyError::PROBLEM_MAPPING` (or slice variants for enums) for downstream
integrations.

All familiar field-level attributes (`#[from]`, `#[source]`, `#[backtrace]`)
are still honoured. Sources and backtraces are automatically attached to the
generated [`masterror::Error`].

#### Display shorthand projections

`#[error("...")]` supports the same shorthand syntax as `thiserror` for
Expand Down Expand Up @@ -637,13 +709,13 @@ assert_eq!(resp.status, 401);
Minimal core:

~~~toml
masterror = { version = "0.12.1", default-features = false }
masterror = { version = "0.13.0", default-features = false }
~~~

API (Axum + JSON + deps):

~~~toml
masterror = { version = "0.12.1", features = [
masterror = { version = "0.13.0", features = [
"axum", "serde_json", "openapi",
"sqlx", "reqwest", "redis", "validator", "config", "tokio"
] }
Expand All @@ -652,7 +724,7 @@ masterror = { version = "0.12.1", features = [
API (Actix + JSON + deps):

~~~toml
masterror = { version = "0.12.1", features = [
masterror = { version = "0.13.0", features = [
"actix", "serde_json", "openapi",
"sqlx", "reqwest", "redis", "validator", "config", "tokio"
] }
Expand Down
76 changes: 72 additions & 4 deletions README.ru.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@
## Основные возможности

- Базовые типы: `AppError`, `AppErrorKind`, `AppResult`, `AppCode`, `ErrorResponse`, `Metadata`.
- Деривы `#[derive(Error)]`, `#[app_error]`, `#[provide]` для типизированного
телеметрического контекста и прямых конверсий доменных ошибок.
- Деривы `#[derive(Error)]`, `#[derive(Masterror)]`, `#[app_error]`,
`#[masterror(...)]`, `#[provide]` для типизированного телеметрического
контекста и прямых конверсий доменных ошибок.
- Адаптеры для Axum и Actix плюс логирование в браузер/`JsValue` для WASM (по
фичам).
- Генерация схем OpenAPI через `utoipa`.
Expand All @@ -38,9 +39,9 @@
~~~toml
[dependencies]
# минимальное ядро
masterror = { version = "0.12.1", default-features = false }
masterror = { version = "0.13.0", default-features = false }
# или с нужными интеграциями
# masterror = { version = "0.12.1", features = [
# masterror = { version = "0.13.0", features = [
# "axum", "actix", "openapi", "serde_json",
# "sqlx", "sqlx-migrate", "reqwest", "redis",
# "validator", "config", "tokio", "multipart",
Expand Down Expand Up @@ -193,6 +194,73 @@ assert!(matches!(app.kind, AppErrorKind::Service));
информацию и прямой маппинг в `AppError`/`AppCode` без ручных реализаций
`From`.

## `#[derive(Masterror)]` и атрибут `#[masterror(...)]`

Когда нужно сразу получить [`masterror::Error`], используйте `#[derive(Masterror)]`.
Атрибут `#[masterror(...)]` описывает код, категорию, телеметрию, политику
редактирования и транспортные подсказки:

~~~rust
use masterror::{
mapping::HttpMapping, AppCode, AppErrorKind, Error, Masterror, MessageEditPolicy
};

#[derive(Debug, Masterror)]
#[error("пользователь {user_id} без флага {flag}")]
#[masterror(
code = AppCode::NotFound,
category = AppErrorKind::NotFound,
message,
redact(message),
telemetry(
Some(masterror::field::str("user_id", user_id.clone())),
attempt.map(|value| masterror::field::u64("attempt", value))
),
map.grpc = 5,
map.problem = "https://errors.example.com/not-found"
)]
struct MissingFlag {
user_id: String,
flag: &'static str,
attempt: Option<u64>,
#[source]
source: Option<std::io::Error>,
}

let err = MissingFlag {
user_id: "alice".into(),
flag: "beta",
attempt: Some(2),
source: None,
};
let converted: Error = err.into();
assert_eq!(converted.code, AppCode::NotFound);
assert_eq!(converted.kind, AppErrorKind::NotFound);
assert_eq!(converted.edit_policy, MessageEditPolicy::Redact);
assert!(converted.metadata().get("user_id").is_some());

assert_eq!(
MissingFlag::HTTP_MAPPING,
HttpMapping::new(AppCode::NotFound, AppErrorKind::NotFound)
);
~~~

- `code` / `category` задают публичный [`AppCode`] и внутренний
[`AppErrorKind`].
- `message` включает текст, возвращаемый [`Display`], в публичное сообщение.
- `redact(message)` выставляет [`MessageEditPolicy`] в режим редактирования на
транспортной границе.
- `telemetry(...)` принимает выражения, возвращающие
`Option<masterror::Field>`. Каждое присутствующее поле добавляется в
[`Metadata`]; пустые выражения пропускаются.
- `map.grpc` / `map.problem` позволяют зафиксировать код gRPC (целое `i32`) и
URI для problem+json. Дерив генерирует таблицы `TYPE::HTTP_MAPPING`,
`TYPE::GRPC_MAPPING` и `TYPE::PROBLEM_MAPPING` (или срезы для перечислений)
для дальнейшей интеграции.

Атрибуты `#[from]`, `#[source]`, `#[backtrace]` продолжают работать: источники и
бектрейсы автоматически прикрепляются к результирующему [`masterror::Error`].

## Форматирование шаблонов `#[error]`

Шаблон `#[error("...")]` по умолчанию использует `Display`, но любая
Expand Down
76 changes: 74 additions & 2 deletions README.template.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ Core is framework-agnostic; integrations are opt-in via feature flags.
Stable categories, conservative HTTP mapping, no `unsafe`.

- Core types: `AppError`, `AppErrorKind`, `AppResult`, `AppCode`, `ErrorResponse`, `Metadata`
- Derive macros: `#[derive(Error)]`, `#[app_error]`, `#[provide]` for domain
mappings and structured telemetry
- Derive macros: `#[derive(Error)]`, `#[derive(Masterror)]`, `#[app_error]`,
`#[masterror(...)]`, `#[provide]` for domain mappings and structured
telemetry
- Optional Axum/Actix integration and browser/WASM console logging
- Optional OpenAPI schema (via `utoipa`)
- Structured metadata helpers via `field::*` builders
Expand Down Expand Up @@ -173,6 +174,77 @@ assert_eq!(wrapped.to_string(), "I/O failed: disk offline");
placeholder, making it easy to branch on the requested rendering behaviour
without manually matching every enum variant.

#### `#[derive(Masterror)]` and `#[masterror(...)]`

`#[derive(Masterror)]` wires a domain error directly into [`masterror::Error`],
augmenting it with metadata, redaction policy and optional transport mappings.
The accompanying `#[masterror(...)]` attribute mirrors the `#[app_error]`
syntax while remaining explicit about telemetry:

~~~rust
use masterror::{
mapping::HttpMapping, AppCode, AppErrorKind, Error, Masterror, MessageEditPolicy
};

#[derive(Debug, Masterror)]
#[error("user {user_id} missing flag {flag}")]
#[masterror(
code = AppCode::NotFound,
category = AppErrorKind::NotFound,
message,
redact(message),
telemetry(
Some(masterror::field::str("user_id", user_id.clone())),
attempt.map(|value| masterror::field::u64("attempt", value))
),
map.grpc = 5,
map.problem = "https://errors.example.com/not-found"
)]
struct MissingFlag {
user_id: String,
flag: &'static str,
attempt: Option<u64>,
#[source]
source: Option<std::io::Error>
}

let err = MissingFlag {
user_id: "alice".into(),
flag: "beta",
attempt: Some(2),
source: None
};
let converted: Error = err.into();
assert_eq!(converted.code, AppCode::NotFound);
assert_eq!(converted.kind, AppErrorKind::NotFound);
assert_eq!(converted.edit_policy, MessageEditPolicy::Redact);
assert!(converted.metadata().get("user_id").is_some());

assert_eq!(
MissingFlag::HTTP_MAPPING,
HttpMapping::new(AppCode::NotFound, AppErrorKind::NotFound)
);
~~~

- `code` / `category` pick the public [`AppCode`] and internal
[`AppErrorKind`].
- `message` forwards the formatted [`Display`] output as the safe public
message. Omit it to keep the message private.
- `redact(message)` flips [`MessageEditPolicy`] to redactable at the transport
boundary.
- `telemetry(...)` accepts expressions that evaluate to
`Option<masterror::Field>`. Each populated field is inserted into the
resulting [`Metadata`]; use `telemetry()` when no fields are attached.
- `map.grpc` / `map.problem` capture optional gRPC status codes (as `i32`) and
RFC 7807 `type` URIs. The derive emits tables such as
`MyError::HTTP_MAPPING`, `MyError::GRPC_MAPPING` and
`MyError::PROBLEM_MAPPING` (or slice variants for enums) for downstream
integrations.

All familiar field-level attributes (`#[from]`, `#[source]`, `#[backtrace]`)
are still honoured. Sources and backtraces are automatically attached to the
generated [`masterror::Error`].

#### Display shorthand projections

`#[error("...")]` supports the same shorthand syntax as `thiserror` for
Expand Down
Loading
Loading