From 87984f76530163d2746cdadb2b4c110b79c4527d Mon Sep 17 00:00:00 2001 From: RA <70325462+RAprogramm@users.noreply.github.com> Date: Wed, 24 Sep 2025 12:46:36 +0700 Subject: [PATCH] docs: rewrite README for 0.20 workspace --- CHANGELOG.md | 5 + Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 577 +++++++-------------------------------------- README.ru.md | 498 ++++++++++++++++---------------------- README.template.md | 540 ++++++------------------------------------ 6 files changed, 364 insertions(+), 1260 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cf24aca..16e02c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,11 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +## [0.20.5] - 2025-10-05 + +### Changed +- Rewrote the English and Russian READMEs to reflect the matured workspace, feature flags, telemetry flows and transport integrations introduced across the 0.20 releases. + ## [0.20.4] - 2025-10-04 ### Added diff --git a/Cargo.lock b/Cargo.lock index 92e21e8..d6d73a1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1727,7 +1727,7 @@ dependencies = [ [[package]] name = "masterror" -version = "0.20.4" +version = "0.20.5" dependencies = [ "actix-web", "axum 0.8.4", diff --git a/Cargo.toml b/Cargo.toml index 641a46b..9fd4521 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "masterror" -version = "0.20.4" +version = "0.20.5" rust-version = "1.90" edition = "2024" license = "MIT OR Apache-2.0" diff --git a/README.md b/README.md index 9cbec41..13d327c 100644 --- a/README.md +++ b/README.md @@ -14,33 +14,66 @@ > πŸ‡·πŸ‡Ί Π§ΠΈΡ‚Π°ΠΉΡ‚Π΅ README Π½Π° [русском языкС](README.ru.md). -Small, pragmatic error model for API-heavy Rust services with native derives -and typed telemetry. -Core is framework-agnostic; integrations are opt-in via feature flags. -Stable categories, conservative HTTP mapping, no `unsafe`. - -- Core types: `AppError`, `AppErrorKind`, `AppResult`, `AppCode`, `ProblemJson`, `ErrorResponse`, `Metadata` -- 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 -- Conversions from `sqlx`, `reqwest`, `redis`, `validator`, `config`, `tokio` -- Turnkey domain taxonomy and helpers (`turnkey` feature) - -πŸ‘‰ Explore the new [error-handling wiki](docs/wiki/index.md) for step-by-step -guides, comparisons with `thiserror`/`anyhow`, and troubleshooting recipes. - ---- +`masterror` grew from a handful of helpers into a workspace of composable crates for +building consistent, observable error surfaces across Rust services. The core +crate stays framework-agnostic, while feature flags light up transport adapters, +integrations and telemetry without pulling in heavyweight defaults. No +`unsafe`, MSRV is pinned, and the derive macros keep your domain types in charge +of redaction and metadata. + +### Highlights + +- **Unified taxonomy.** `AppError`, `AppErrorKind` and `AppCode` model domain and + transport concerns with conservative HTTP/gRPC mappings, turnkey retry/auth + hints and RFC7807 output via `ProblemJson`. +- **Native derives.** `#[derive(Error)]`, `#[derive(Masterror)]`, `#[app_error]`, + `#[masterror(...)]` and `#[provide]` wire custom types into `AppError` while + forwarding sources, backtraces, telemetry providers and redaction policy. +- **Typed telemetry.** `Metadata` stores structured key/value context with + per-field redaction controls and builders in `field::*`, so logs stay + structured without manual `String` maps. +- **Transport adapters.** Optional features expose Actix/Axum responders, + `tonic::Status` conversions, WASM/browser logging and OpenAPI schema + generation without contaminating the lean default build. +- **Battle-tested integrations.** Enable focused mappings for `sqlx`, + `reqwest`, `redis`, `validator`, `config`, `tokio`, `teloxide`, `multipart`, + Telegram WebApp SDK and more β€” each translating library errors into the + taxonomy with telemetry attached. +- **Turnkey defaults.** The `turnkey` module ships a ready-to-use error catalog, + helper builders and tracing instrumentation for teams that want a consistent + baseline out of the box. + +### Workspace crates + +| Crate | What it provides | When to depend on it | +| --- | --- | --- | +| [`masterror`](https://crates.io/crates/masterror) | Core error types, metadata builders, transports, integrations and the prelude. | Application crates, services and libraries that want a stable error surface. | +| [`masterror-derive`](masterror-derive/README.md) | Proc-macros backing `#[derive(Error)]`, `#[derive(Masterror)]`, `#[app_error]` and `#[provide]`. | Brought in automatically via `masterror`; depend directly only for macro hacking. | +| [`masterror-template`](masterror-template/README.md) | Shared template parser used by the derive macros for formatter analysis. | Internal dependency; reuse when you need the template parser elsewhere. | + +### Feature flags at a glance + +Pick only what you need; everything is off by default. + +- **Web transports:** `axum`, `actix`, `multipart`, `openapi`, `serde_json`. +- **Telemetry & observability:** `tracing`, `metrics`, `backtrace`. +- **Async & IO integrations:** `tokio`, `reqwest`, `sqlx`, `sqlx-migrate`, + `redis`, `validator`, `config`. +- **Messaging & bots:** `teloxide`, `telegram-webapp-sdk`. +- **Front-end tooling:** `frontend` for WASM/browser console logging. +- **gRPC:** `tonic` to emit `tonic::Status` responses. +- **Batteries included:** `turnkey` to adopt the pre-built taxonomy and helpers. + +The build script keeps the full feature snippet below in sync with +`Cargo.toml`. ### TL;DR ~~~toml [dependencies] -masterror = { version = "0.20.4", default-features = false } +masterror = { version = "0.20.5", default-features = false } # or with features: -# masterror = { version = "0.20.4", features = [ +# masterror = { version = "0.20.5", features = [ # "axum", "actix", "openapi", "serde_json", # "tracing", "metrics", "backtrace", "sqlx", # "sqlx-migrate", "reqwest", "redis", "validator", @@ -49,52 +82,8 @@ masterror = { version = "0.20.4", default-features = false } # ] } ~~~ -*Since v0.5.0: derive custom errors via `#[derive(Error)]` (`use masterror::Error;`) and inspect browser logging failures with `BrowserConsoleError::context()`.* -*Since v0.4.0: optional `frontend` feature for WASM/browser console logging.* -*Since v0.3.0: stable `AppCode` enum and extended `ErrorResponse` with retry/authentication metadata.* -*Since v0.15.0: RFC7807 `ProblemJson` responses for HTTP integrations and `tonic::Status` conversion.* - --- -
- Why this crate? - -- **Stable taxonomy.** Small set of `AppErrorKind` categories mapping conservatively to HTTP. -- **Framework-agnostic.** No assumptions, no `unsafe`, MSRV pinned. -- **Opt-in integrations.** Zero default features; you enable what you need. -- **Clean wire contract.** `ProblemJson { type?, title, status, detail?, code, grpc?, metadata? }` with `Retry-After` / `WWW-Authenticate` headers when present. -- **Typed telemetry.** `Metadata` preserves structured key/value context without `String` maps. -- **One log at boundary.** Log once with `tracing`. -- **Less boilerplate.** Built-in conversions, compact prelude, and the - native `masterror::Error` derive with `#[from]` / `#[error(transparent)]` - support. -- **Consistent workspace.** Same error surface across crates. - -
- -
- Installation - -~~~toml -[dependencies] -# lean core -masterror = { version = "0.20.4", default-features = false } - -# with Axum/Actix + JSON + integrations -# masterror = { version = "0.20.4", features = [ -# "axum", "actix", "openapi", "serde_json", -# "tracing", "metrics", "backtrace", "sqlx", -# "sqlx-migrate", "reqwest", "redis", "validator", -# "config", "tokio", "multipart", "teloxide", -# "telegram-webapp-sdk", "tonic", "frontend", "turnkey" -# ] } -~~~ - -**MSRV:** 1.90 -**No unsafe:** forbidden by crate. - -
-
Quick start @@ -126,7 +115,10 @@ fn do_work(flag: bool) -> AppResult<()> {
- Derive custom errors + Derive domain errors and map them to transports + +`masterror` ships native derives so your domain types stay expressive while the +crate handles conversions, telemetry and redaction for you. ~~~rust use std::io; @@ -160,7 +152,7 @@ let wrapped = WrappedDomainError::from(err); assert_eq!(wrapped.to_string(), "I/O failed: disk offline"); ~~~ -- `use masterror::Error;` brings the crate's derive macro into scope. +- `use masterror::Error;` brings the derive macro into scope. - `#[from]` automatically implements `From<...>` while ensuring wrapper shapes are valid. - `#[error(transparent)]` enforces single-field wrappers that forward @@ -183,12 +175,15 @@ 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(...)]` +
+ +
+ Attach telemetry, redaction policy and conversions -`#[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: +`#[derive(Masterror)]` wires a domain error into [`masterror::Error`], adds +metadata, redaction policy and optional transport mappings. The accompanying +`#[masterror(...)]` attribute mirrors the `#[app_error]` syntax while staying +explicit about telemetry and redaction. ~~~rust use masterror::{ @@ -255,103 +250,10 @@ 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 -referencing fields with `.field` or `.0`. The derive now understands chained -segments, so projections like `.limits.lo`, `.0.data` or -`.suggestion.as_ref().map_or_else(...)` keep compiling unchanged. Raw -identifiers and tuple indices are preserved, ensuring keywords such as -`r#type` and tuple fields continue to work even when you call methods on the -projected value. - -~~~rust -use masterror::Error; - -#[derive(Debug)] -struct Limits { - lo: i32, - hi: i32, -} - -#[derive(Debug, Error)] -#[error( - "range {lo}-{hi} suggestion {suggestion}", - lo = .limits.lo, - hi = .limits.hi, - suggestion = .suggestion.as_ref().map_or_else(|| "", |s| s.as_str()) -)] -struct RangeError { - limits: Limits, - suggestion: Option, -} - -#[derive(Debug)] -struct Payload { - data: &'static str, -} - -#[derive(Debug, Error)] -enum UiError { - #[error("tuple data {data}", data = .0.data)] - Tuple(Payload), - #[error( - "named suggestion {value}", - value = .suggestion.as_ref().map_or_else(|| "", |s| s.as_str()) - )] - Named { suggestion: Option }, -} -~~~ - -#### AppError conversions - -Annotating structs or enum variants with `#[app_error(...)]` captures the -metadata required to convert the domain error into `AppError` (and optionally -`AppCode`). Every variant in an enum must provide the mapping when any variant -requests it. - -~~~rust -use masterror::{AppCode, AppError, AppErrorKind, Error}; - -#[derive(Debug, Error)] -#[error("missing flag: {name}")] -#[app_error(kind = AppErrorKind::BadRequest, code = AppCode::BadRequest, message)] -struct MissingFlag { - name: &'static str, -} - -let app: AppError = MissingFlag { name: "feature" }.into(); -assert!(matches!(app.kind, AppErrorKind::BadRequest)); -assert_eq!(app.message.as_deref(), Some("missing flag: feature")); - -let code: AppCode = MissingFlag { name: "feature" }.into(); -assert!(matches!(code, AppCode::BadRequest)); -~~~ - -For enums, each variant specifies the mapping while the derive generates a -single `From` implementation that matches every variant: - -~~~rust -#[derive(Debug, Error)] -enum ApiError { - #[error("missing resource {id}")] - #[app_error( - kind = AppErrorKind::NotFound, - code = AppCode::NotFound, - message - )] - Missing { id: u64 }, - #[error("backend unavailable")] - #[app_error(kind = AppErrorKind::Service, code = AppCode::Service)] - Backend, -} - -let missing = ApiError::Missing { id: 7 }; -let as_app: AppError = missing.into(); -assert_eq!(as_app.message.as_deref(), Some("missing resource 7")); -~~~ +
-#### Structured telemetry providers and AppError mappings +
+ Structured telemetry providers and AppError mappings `#[provide(...)]` exposes typed context through `std::error::Request`, while `#[app_error(...)]` records how your domain error translates into `AppError` @@ -452,173 +354,12 @@ assert!(matches!(app.kind, AppErrorKind::Service)); Compared to `thiserror`, you retain the familiar deriving surface while gaining structured telemetry (`#[provide]`) and first-class conversions into -`AppError`/`AppCode` without writing manual `From` implementations. - -#### Formatter traits - -Placeholders default to `Display` (`{value}`) but can opt into richer -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; - -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, 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, - 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, - &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()); -~~~ - -Display-only format specs (alignment, precision, fill β€” including `#` as a fill -character) are preserved so you can forward them to `write!` without rebuilding -the fragment: - -~~~rust -use masterror::error::template::ErrorTemplate; - -let aligned = ErrorTemplate::parse("{value:>8}").expect("parse"); -let display = aligned.placeholders().next().expect("display placeholder"); -assert_eq!(display.formatter().display_spec(), Some(">8")); -assert_eq!( - display - .formatter() - .format_fragment() - .as_deref(), - Some(">8") -); - -let hashed = ErrorTemplate::parse("{value:#>4}").expect("parse"); -let hash_placeholder = hashed - .placeholders() - .next() - .expect("hash-fill display placeholder"); -assert_eq!(hash_placeholder.formatter().display_spec(), Some("#>4")); -assert_eq!( - hash_placeholder - .formatter() - .format_fragment() - .as_deref(), - Some("#>4") -); -~~~ - -> **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}; - -let template = ErrorTemplate::parse("{code}: {message}").expect("parse"); -let display = template.display_with(|placeholder, f| match placeholder.identifier() { - TemplateIdentifier::Named("code") => write!(f, "{}", 404), - TemplateIdentifier::Named("message") => f.write_str("Not Found"), - _ => Ok(()), -}); - -assert_eq!(display.to_string(), "404: Not Found"); -``` +`AppError`/`AppCode` without manual glue.
- Error response payload + Problem JSON payloads and retry/authentication hints ~~~rust use masterror::{AppError, AppErrorKind, ProblemJson}; @@ -637,172 +378,14 @@ assert_eq!(problem.grpc.expect("grpc").name, "UNAUTHENTICATED");
-
- Web framework integrations - -
- Axum - -~~~rust -// features = ["axum", "serde_json"] -... - assert!(payload.is_object()); - - #[cfg(target_arch = "wasm32")] - { - if let Err(console_err) = err.log_to_browser_console() { - eprintln!( - "failed to log to browser console: {:?}", - console_err.context() - ); - } - } - - Ok(()) -} -~~~ - -- On non-WASM targets `log_to_browser_console` returns - `BrowserConsoleError::UnsupportedTarget`. -- `BrowserConsoleError::context()` exposes optional browser diagnostics for - logging/telemetry when console logging fails. - -
- -
- -
- Feature flags - -- `axum` β€” IntoResponse integration with structured JSON bodies -- `actix` β€” Actix Web ResponseError and Responder implementations -- `openapi` β€” Generate utoipa OpenAPI schema for ErrorResponse -- `serde_json` β€” Attach structured JSON details to AppError -- `tracing` β€” Emit structured tracing events when errors are constructed -- `metrics` β€” Increment `error_total{code,category}` counter for each AppError -- `backtrace` β€” Capture lazy `Backtrace` snapshots when telemetry is flushed -- `sqlx` β€” Classify sqlx_core::Error variants into AppError kinds -- `sqlx-migrate` β€” Map sqlx::migrate::MigrateError into AppError (Database) -- `reqwest` β€” Classify reqwest::Error as timeout/network/external API -- `redis` β€” Map redis::RedisError into cache-aware AppError -- `validator` β€” Convert validator::ValidationErrors into validation failures -- `config` β€” Propagate config::ConfigError as configuration issues -- `tokio` β€” Classify tokio::time::error::Elapsed as timeout -- `multipart` β€” Handle axum multipart extraction errors -- `teloxide` β€” Convert teloxide_core::RequestError into domain errors -- `telegram-webapp-sdk` β€” Surface Telegram WebApp validation failures -- `tonic` β€” Convert AppError into tonic::Status with redaction -- `frontend` β€” Log to the browser console and convert to JsValue on WASM -- `turnkey` β€” Ship Turnkey-specific error taxonomy and conversions - -
- -
- Conversions - -- `std::io::Error` β†’ Internal -- `String` β†’ BadRequest -- `sqlx::Error` β†’ NotFound/Database -- `redis::RedisError` β†’ Cache -- `reqwest::Error` β†’ Timeout/Network/ExternalApi -- `axum::extract::multipart::MultipartError` β†’ BadRequest -- `validator::ValidationErrors` β†’ Validation -- `config::ConfigError` β†’ Config -- `tokio::time::error::Elapsed` β†’ Timeout -- `teloxide_core::RequestError` β†’ RateLimited/Network/ExternalApi/Deserialization/Internal -- `telegram_webapp_sdk::utils::validate_init_data::ValidationError` β†’ TelegramAuth - -
- -
- Typical setups - -Minimal core: - -~~~toml -masterror = { version = "0.20.4", default-features = false } -~~~ - -API (Axum + JSON + deps): - -~~~toml -masterror = { version = "0.20.4", features = [ - "axum", "serde_json", "openapi", - "sqlx", "reqwest", "redis", "validator", "config", "tokio" -] } -~~~ - -API (Actix + JSON + deps): - -~~~toml -masterror = { version = "0.20.4", features = [ - "actix", "serde_json", "openapi", - "sqlx", "reqwest", "redis", "validator", "config", "tokio" -] } -~~~ - -
- -
- Turnkey - -~~~rust -// features = ["turnkey"] -use masterror::turnkey::{classify_turnkey_error, TurnkeyError, TurnkeyErrorKind}; -use masterror::{AppError, AppErrorKind}; - -// Classify a raw SDK/provider error -let kind = classify_turnkey_error("429 Too Many Requests"); -assert!(matches!(kind, TurnkeyErrorKind::RateLimited)); - -// Wrap into AppError -let e = TurnkeyError::new(TurnkeyErrorKind::RateLimited, "throttled upstream"); -let app: AppError = e.into(); -assert_eq!(app.kind, AppErrorKind::RateLimited); -~~~ - -
- -
- Migration 0.2 β†’ 0.3 - -- Use `ErrorResponse::new(status, AppCode::..., "msg")` instead of legacy -- New helpers: `.with_retry_after_secs`, `.with_retry_after_duration`, `.with_www_authenticate` -- `ErrorResponse::new_legacy` is temporary shim - -
- -
- Versioning & MSRV +### Further resources -Semantic versioning. Breaking API/wire contract β†’ major bump. -MSRV = 1.90 (may raise in minor, never in patch). +- Explore the [error-handling wiki](docs/wiki/index.md) for step-by-step guides, + comparisons with `thiserror`/`anyhow`, and troubleshooting recipes. +- Browse the [crate documentation on docs.rs](https://docs.rs/masterror) for API + details, feature-specific guides and transport tables. +- Check [`CHANGELOG.md`](CHANGELOG.md) for release highlights and migration notes. -
- -
- Release checklist - -1. `cargo +nightly fmt --` -1. `cargo clippy -- -D warnings` -1. `cargo test --all` -1. `cargo build` (regenerates README.md from the template) -1. `cargo doc --no-deps` -1. `cargo package --locked` - -
- -
- Non-goals - -- Not a general-purpose error aggregator like `anyhow` -- Not a replacement for your domain errors - -
- -
- License - -Apache-2.0 OR MIT, at your option. +--- -
+MSRV: **1.90** Β· License: **MIT OR Apache-2.0** Β· No `unsafe` diff --git a/README.ru.md b/README.ru.md index d87a385..2c90dfc 100644 --- a/README.ru.md +++ b/README.ru.md @@ -1,6 +1,6 @@ # masterror Β· ΠšΠ°Ρ€ΠΊΠ°Ρ-нСзависимыС Ρ‚ΠΈΠΏΡ‹ ошибок ΠΏΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠΉ -> Π­Ρ‚ΠΎΡ‚ Π΄ΠΎΠΊΡƒΠΌΠ΅Π½Ρ‚ β€” русская вСрсия основной Π΄ΠΎΠΊΡƒΠΌΠ΅Π½Ρ‚Π°Ρ†ΠΈΠΈ. ΠΠ½Π³Π»ΠΈΠΉΡΠΊΡƒΡŽ Π²Π΅Ρ€ΡΠΈΡŽ см. Π² [README.md](README.md). +> Π­Ρ‚Π° страница β€” русская вСрсия основной Π΄ΠΎΠΊΡƒΠΌΠ΅Π½Ρ‚Π°Ρ†ΠΈΠΈ. Английский ΠΎΡ€ΠΈΠ³ΠΈΠ½Π°Π» см. Π² [README.md](README.md). [![Crates.io](https://img.shields.io/crates/v/masterror)](https://crates.io/crates/masterror) [![docs.rs](https://img.shields.io/docsrs/masterror)](https://docs.rs/masterror) @@ -11,47 +11,74 @@ [![Security audit](https://github.com/RAprogramm/masterror/actions/workflows/ci.yml/badge.svg?branch=main&label=Security%20audit)](https://github.com/RAprogramm/masterror/actions/workflows/ci.yml?query=branch%3Amain) [![Cargo Deny](https://img.shields.io/github/actions/workflow/status/RAprogramm/masterror/ci.yml?branch=main&label=Cargo%20Deny)](https://github.com/RAprogramm/masterror/actions/workflows/ci.yml?query=branch%3Amain) -НСбольшая прагматичная модСль ошибок для Rust-сСрвисов с Π²Ρ‹Ρ€Π°ΠΆΠ΅Π½Π½Ρ‹ΠΌ API ΠΈ -встроСнными Π΄Π΅Ρ€ΠΈΠ²Π°ΠΌΠΈ. -Основной ΠΊΡ€Π΅ΠΉΡ‚ Π½Π΅ зависит ΠΎΡ‚ Π²Π΅Π±-Ρ„Ρ€Π΅ΠΉΠΌΠ²ΠΎΡ€ΠΊΠΎΠ², Π° Ρ€Π°ΡΡˆΠΈΡ€Π΅Π½ΠΈΡ Π²ΠΊΠ»ΡŽΡ‡Π°ΡŽΡ‚ΡΡ Ρ‡Π΅Ρ€Π΅Π· -Ρ„ΠΈΡ‡ΠΈ. Ваксономия ошибок ΡΡ‚Π°Π±ΠΈΠ»ΡŒΠ½Π°, соотвСтствиС HTTP-ΠΊΠΎΠ΄Π°ΠΌ консСрвативно, -`unsafe` Π·Π°ΠΏΡ€Π΅Ρ‰Ρ‘Π½. - -## ΠžΡΠ½ΠΎΠ²Π½Ρ‹Π΅ возмоТности - -- Π‘Π°Π·ΠΎΠ²Ρ‹Π΅ Ρ‚ΠΈΠΏΡ‹: `AppError`, `AppErrorKind`, `AppResult`, `AppCode`, `ProblemJson`, `ErrorResponse`, `Metadata`. -- Π”Π΅Ρ€ΠΈΠ²Ρ‹ `#[derive(Error)]`, `#[derive(Masterror)]`, `#[app_error]`, - `#[masterror(...)]`, `#[provide]` для Ρ‚ΠΈΠΏΠΈΠ·ΠΈΡ€ΠΎΠ²Π°Π½Π½ΠΎΠ³ΠΎ тСлСмСтричСского - контСкста ΠΈ прямых конвСрсий Π΄ΠΎΠΌΠ΅Π½Π½Ρ‹Ρ… ошибок. -- АдаптСры для Axum ΠΈ Actix плюс Π»ΠΎΠ³ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ Π² Π±Ρ€Π°ΡƒΠ·Π΅Ρ€/`JsValue` для WASM (ΠΏΠΎ - Ρ„ΠΈΡ‡Π°ΠΌ). -- ГСнСрация схСм OpenAPI Ρ‡Π΅Ρ€Π΅Π· `utoipa`. -- Π‘Ρ‚Ρ€ΡƒΠΊΡ‚ΡƒΡ€ΠΈΡ€ΠΎΠ²Π°Π½Π½Ρ‹Π΅ поля `Metadata` Ρ‡Π΅Ρ€Π΅Π· Π±ΠΈΠ»Π΄Π΅Ρ€Ρ‹ `field::*`. -- ΠšΠΎΠ½Π²Π΅Ρ€ΡΠΈΠΈ ΠΈΠ· распространённых Π±ΠΈΠ±Π»ΠΈΠΎΡ‚Π΅ΠΊ (`sqlx`, `reqwest`, `redis`, - `validator`, `config`, `tokio` ΠΈ Π΄Ρ€.). -- Π“ΠΎΡ‚ΠΎΠ²Ρ‹ΠΉ ΠΏΡ€Π΅Π»ΡŽΠ΄ΠΈΡ-ΠΌΠΎΠ΄ΡƒΠ»ΡŒ ΠΈ Ρ€Π°ΡΡˆΠΈΡ€Π΅Π½ΠΈΠ΅ `turnkey` с собствСнной таксономиСй - ошибок. - -## Установка - -Π”ΠΎΠ±Π°Π²ΡŒΡ‚Π΅ Π·Π°Π²ΠΈΡΠΈΠΌΠΎΡΡ‚ΡŒ Π² `Cargo.toml`: +`masterror` вырос ΠΈΠ· Π½Π°Π±ΠΎΡ€Π° Π²ΡΠΏΠΎΠΌΠΎΠ³Π°Ρ‚Π΅Π»ΡŒΠ½Ρ‹Ρ… Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠΉ Π² ΠΏΠΎΠ»Π½ΠΎΡ†Π΅Π½Π½Ρ‹ΠΉ workspace с +ΠΌΠΎΠ΄ΡƒΠ»ΡŒΠ½Ρ‹ΠΌΠΈ ΠΊΡ€Π΅ΠΉΡ‚Π°ΠΌΠΈ для построСния Π½Π°Π±Π»ΡŽΠ΄Π°Π΅ΠΌΡ‹Ρ… ΠΈ ΠΏΠΎΡΠ»Π΅Π΄ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒΠ½Ρ‹Ρ… ошибок Π² +Rust-сСрвисах. Π‘Π°Π·ΠΎΠ²Ρ‹ΠΉ ΠΊΡ€Π΅ΠΉΡ‚ остаётся нСзависимым ΠΎΡ‚ Π²Π΅Π±-Ρ„Ρ€Π΅ΠΉΠΌΠ²ΠΎΡ€ΠΊΠΎΠ², Π° Ρ„ΠΈΡ‡ΠΈ +Π²ΠΊΠ»ΡŽΡ‡Π°ΡŽΡ‚ Ρ‚ΠΎΠ»ΡŒΠΊΠΎ Π½ΡƒΠΆΠ½Ρ‹Π΅ Π°Π΄Π°ΠΏΡ‚Π΅Ρ€Ρ‹, ΠΈΠ½Ρ‚Π΅Π³Ρ€Π°Ρ†ΠΈΠΈ ΠΈ Ρ‚Π΅Π»Π΅ΠΌΠ΅Ρ‚Ρ€ΠΈΡŽ. `unsafe` Π·Π°ΠΏΡ€Π΅Ρ‰Ρ‘Π½, +MSRV зафиксирован, Π° Ρ€ΠΎΠ΄Π½Ρ‹Π΅ Π΄Π΅Ρ€ΠΈΠ²Ρ‹ ΠΏΠΎΠ·Π²ΠΎΠ»ΡΡŽΡ‚ Π΄ΠΎΠΌΠ΅Π½Π½Ρ‹ΠΌ Ρ‚ΠΈΠΏΠ°ΠΌ ΡƒΠΏΡ€Π°Π²Π»ΡΡ‚ΡŒ +Ρ€Π΅Π΄Π°ΠΊΡ‚ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ΠΌ сообщСний ΠΈ ΠΌΠ΅Ρ‚Π°Π΄Π°Π½Π½Ρ‹ΠΌΠΈ. + +## ΠšΠ»ΡŽΡ‡Π΅Π²Ρ‹Π΅ возмоТности + +- **Единая таксономия.** `AppError`, `AppErrorKind` ΠΈ `AppCode` ΠΎΠΏΠΈΡΡ‹Π²Π°ΡŽΡ‚ + Π΄ΠΎΠΌΠ΅Π½Π½Ρ‹Π΅ ΠΈ транспортныС аспСкты, ΠΈΠΌΠ΅ΡŽΡ‚ консСрвативноС соотвСтствиС HTTP/gRPC, + Π³ΠΎΡ‚ΠΎΠ²Ρ‹Π΅ подсказки retry/auth ΠΈ RFC7807-ΠΎΡ‚Π²Π΅Ρ‚Ρ‹ Ρ‡Π΅Ρ€Π΅Π· `ProblemJson`. +- **Π ΠΎΠ΄Π½Ρ‹Π΅ Π΄Π΅Ρ€ΠΈΠ²Ρ‹.** `#[derive(Error)]`, `#[derive(Masterror)]`, `#[app_error]`, + `#[masterror(...)]` ΠΈ `#[provide]` ΡΠΎΠ΅Π΄ΠΈΠ½ΡΡŽΡ‚ ваши Ρ‚ΠΈΠΏΡ‹ с `AppError`, пробрасывая + источники, бэктрСйсы, Ρ‚Π΅Π»Π΅ΠΌΠ΅Ρ‚Ρ€ΠΈΡŽ ΠΈ ΠΏΠΎΠ»ΠΈΡ‚ΠΈΠΊΡƒ рСдактирования. +- **Випизированная тСлСмСтрия.** `Metadata` Ρ…Ρ€Π°Π½ΠΈΡ‚ структурированныС ΠΊΠ»ΡŽΡ‡ΠΈ ΠΈ + значСния с ΠΈΠ½Π΄ΠΈΠ²ΠΈΠ΄ΡƒΠ°Π»ΡŒΠ½Ρ‹ΠΌΠΈ ΠΏΡ€Π°Π²ΠΈΠ»Π°ΠΌΠΈ маскирования, Π° Π±ΠΈΠ»Π΄Π΅Ρ€Ρ‹ `field::*` + ΠΈΠ·Π±Π°Π²Π»ΡΡŽΡ‚ ΠΎΡ‚ Ρ€ΡƒΡ‡Π½Ρ‹Ρ… `String`-ΠΊΠ°Ρ€Ρ‚. +- **ВранспортныС Π°Π΄Π°ΠΏΡ‚Π΅Ρ€Ρ‹.** ΠžΠΏΡ†ΠΈΠΎΠ½Π°Π»ΡŒΠ½Ρ‹Π΅ Ρ„ΠΈΡ‡ΠΈ Π²ΠΊΠ»ΡŽΡ‡Π°ΡŽΡ‚ рСспондСры для Actix/Axum, + ΠΊΠΎΠ½Π²Π΅Ρ€Ρ‚Π°Ρ†ΠΈΡŽ Π² `tonic::Status`, Π»ΠΎΠ³ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ Π² Π±Ρ€Π°ΡƒΠ·Π΅Ρ€/WASM ΠΈ Π³Π΅Π½Π΅Ρ€Π°Ρ†ΠΈΡŽ схСм + OpenAPI Π±Π΅Π· утяТСлСния Π΄Π΅Ρ„ΠΎΠ»Ρ‚Π½ΠΎΠΉ сборки. +- **Π˜Π½Ρ‚Π΅Π³Ρ€Π°Ρ†ΠΈΠΈ, ΠΏΡ€ΠΎΠ²Π΅Ρ€Π΅Π½Π½Ρ‹Π΅ Π² бою.** АктивируйтС ΠΌΠ°ΠΏΠΏΠΈΠ½Π³ΠΈ для `sqlx`, `reqwest`, + `redis`, `validator`, `config`, `tokio`, `teloxide`, `multipart`, Telegram + WebApp SDK ΠΈ Π΄Ρ€ΡƒΠ³ΠΈΡ… Π±ΠΈΠ±Π»ΠΈΠΎΡ‚Π΅ΠΊ β€” каТдая ΠΏΠ΅Ρ€Π΅Π²ΠΎΠ΄ΠΈΡ‚ ошибки Π² Ρ‚Π°ΠΊΡΠΎΠ½ΠΎΠΌΠΈΡŽ с + ΠΏΡ€ΠΈΠΊΡ€Π΅ΠΏΠ»Ρ‘Π½Π½ΠΎΠΉ Ρ‚Π΅Π»Π΅ΠΌΠ΅Ρ‚Ρ€ΠΈΠ΅ΠΉ. +- **Π“ΠΎΡ‚ΠΎΠ²Ρ‹Π΅ настройки.** ΠœΠΎΠ΄ΡƒΠ»ΡŒ `turnkey` поставляСт Π³ΠΎΡ‚ΠΎΠ²Ρ‹ΠΉ ΠΊΠ°Ρ‚Π°Π»ΠΎΠ³ ошибок, + Π±ΠΈΠ»Π΄Π΅Ρ€Ρ‹ ΠΈ ΠΈΠ½Ρ‚Π΅Π³Ρ€Π°Ρ†ΠΈΡŽ с `tracing` для ΠΊΠΎΠΌΠ°Π½Π΄, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΌ Π½ΡƒΠΆΠ½Π° стартовая + конфигурация Β«ΠΈΠ· ΠΊΠΎΡ€ΠΎΠ±ΠΊΠΈΒ». + +## Бостав workspace + +| ΠšΡ€Π΅ΠΉΡ‚ | Π§Ρ‚ΠΎ содСрТит | Когда ΠΏΠΎΠ΄ΠΊΠ»ΡŽΡ‡Π°Ρ‚ΡŒ | +| --- | --- | --- | +| [`masterror`](https://crates.io/crates/masterror) | ΠžΡΠ½ΠΎΠ²Π½Ρ‹Π΅ Ρ‚ΠΈΠΏΡ‹ ошибок, Π±ΠΈΠ»Π΄Π΅Ρ€Ρ‹ ΠΌΠ΅Ρ‚Π°Π΄Π°Π½Π½Ρ‹Ρ…, транспорты, ΠΈΠ½Ρ‚Π΅Π³Ρ€Π°Ρ†ΠΈΠΈ ΠΈ ΠΏΡ€Π΅Π»ΡŽΠ΄ΠΈΡ. | Π‘ΠΎΠ΅Π²Ρ‹Π΅ сСрвисы ΠΈ Π±ΠΈΠ±Π»ΠΈΠΎΡ‚Π΅ΠΊΠΈ, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΌ Π½ΡƒΠΆΠ½Π° ΡΡ‚Π°Π±ΠΈΠ»ΡŒΠ½Π°Ρ ΠΏΠΎΠ²Π΅Ρ€Ρ…Π½ΠΎΡΡ‚ΡŒ ошибок. | +| [`masterror-derive`](masterror-derive/README.md) | ΠŸΡ€ΠΎΡ†Π΅Π΄ΡƒΡ€Π½Ρ‹Π΅ макросы `#[derive(Error)]`, `#[derive(Masterror)]`, `#[app_error]`, `#[provide]`. | Π£ΠΆΠ΅ ΠΈΠ΄Ρ‘Ρ‚ Ρ‚Ρ€Π°Π½Π·ΠΈΡ‚ΠΈΠ²Π½ΠΎ; ΠΏΠΎΠ΄ΠΊΠ»ΡŽΡ‡Π°ΠΉΡ‚Π΅ Π½Π°ΠΏΡ€ΡΠΌΡƒΡŽ Ρ‚ΠΎΠ»ΡŒΠΊΠΎ для экспСримСнтов с макросами. | +| [`masterror-template`](masterror-template/README.md) | ΠžΠ±Ρ‰ΠΈΠΉ парсСр шаблонов для Π°Π½Π°Π»ΠΈΠ·Π° Ρ„ΠΎΡ€ΠΌΠ°Ρ‚Ρ‚Π΅Ρ€ΠΎΠ² Π² Π΄Π΅Ρ€ΠΈΠ²Π°Ρ…. | Π’Π½ΡƒΡ‚Ρ€Π΅Π½Π½ΠΈΠΉ ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚; ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠΉΡ‚Π΅, Ссли Π½ΡƒΠΆΠ΅Π½ этот парсСр Π² Π΄Ρ€ΡƒΠ³ΠΎΠΌ ΠΊΠΎΠ΄Π΅. | + +## Π€Π»Π°Π³ΠΈ Ρ„ΠΈΡ‡ + +ВсС Ρ„ΠΈΡ‡ΠΈ ΠΎΡ‚ΠΊΠ»ΡŽΡ‡Π΅Π½Ρ‹ ΠΏΠΎ ΡƒΠΌΠΎΠ»Ρ‡Π°Π½ΠΈΡŽ β€” Π²Ρ‹Π±ΠΈΡ€Π°ΠΉΡ‚Π΅ Ρ‚ΠΎΠ»ΡŒΠΊΠΎ Π½ΡƒΠΆΠ½ΠΎΠ΅. + +- **Π’Π΅Π± ΠΈ API:** `axum`, `actix`, `multipart`, `openapi`, `serde_json`. +- **ΠΠ°Π±Π»ΡŽΠ΄Π°Π΅ΠΌΠΎΡΡ‚ΡŒ:** `tracing`, `metrics`, `backtrace`. +- **Async ΠΈ IO:** `tokio`, `reqwest`, `sqlx`, `sqlx-migrate`, `redis`, `validator`, + `config`. +- **Π‘ΠΎΡ‚Ρ‹ ΠΈ мСссСндТСры:** `teloxide`, `telegram-webapp-sdk`. +- **Π€Ρ€ΠΎΠ½Ρ‚Π΅Π½Π΄:** `frontend` для логирования Π² Π±Ρ€Π°ΡƒΠ·Π΅Ρ€Π΅/WASM. +- **gRPC:** `tonic` для Π³Π΅Π½Π΅Ρ€Π°Ρ†ΠΈΠΈ `tonic::Status`. +- **Готовая таксономия:** `turnkey`. + +## TL;DR ~~~toml [dependencies] -# минимальноС ядро -masterror = { version = "0.15.0", default-features = false } -# ΠΈΠ»ΠΈ с Π½ΡƒΠΆΠ½Ρ‹ΠΌΠΈ интСграциями -# masterror = { version = "0.15.0", features = [ +masterror = { version = "0.20.5", default-features = false } +# ΠΈΠ»ΠΈ с Π½ΡƒΠΆΠ½Ρ‹ΠΌΠΈ Ρ„ΠΈΡ‡Π°ΠΌΠΈ: +# masterror = { version = "0.20.5", features = [ # "axum", "actix", "openapi", "serde_json", -# "sqlx", "sqlx-migrate", "reqwest", "redis", -# "validator", "config", "tokio", "multipart", -# "teloxide", "telegram-webapp-sdk", "frontend", "turnkey" +# "tracing", "metrics", "backtrace", "sqlx", +# "sqlx-migrate", "reqwest", "redis", "validator", +# "config", "tokio", "multipart", "teloxide", +# "telegram-webapp-sdk", "tonic", "frontend", "turnkey" # ] } ~~~ -**MSRV:** 1.90 +--- -## Быстрый старт +### Быстрый старт Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ошибки Π²Ρ€ΡƒΡ‡Π½ΡƒΡŽ: @@ -78,29 +105,121 @@ 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`). -- `metadata` β€” Ρ‚ΠΈΠΏΠΈΠ·ΠΈΡ€ΠΎΠ²Π°Π½Π½Ρ‹Π΅ поля `Metadata` Π±Π΅Π· Π°Π»Π»ΠΎΠΊΠ°Ρ†ΠΈΠΉ строк. -- `multipart` β€” ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠ° ошибок извлСчСния multipart Π² Axum. -- `teloxide` β€” ΠΌΠ°ΠΏΠΏΠΈΠ½Π³ `teloxide_core::RequestError` Π² Π΄ΠΎΠΌΠ΅Π½Π½Ρ‹Π΅ ΠΊΠ°Ρ‚Π΅Π³ΠΎΡ€ΠΈΠΈ. -- `telegram-webapp-sdk` β€” ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠ° ошибок Π²Π°Π»ΠΈΠ΄Π°Ρ†ΠΈΠΈ Π΄Π°Π½Π½Ρ‹Ρ… Telegram WebApp. -- `tonic` β€” ΠΏΡ€Π΅ΠΎΠ±Ρ€Π°Π·ΠΎΠ²Π°Π½ΠΈΠ΅ `AppError` Π² `tonic::Status` с ΡƒΡ‡Ρ‘Ρ‚ΠΎΠΌ рСдактирования. -- `frontend` β€” Π»ΠΎΠ³ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ Π² Π±Ρ€Π°ΡƒΠ·Π΅Ρ€Π΅ ΠΈ ΠΏΡ€Π΅ΠΎΠ±Ρ€Π°Π·ΠΎΠ²Π°Π½ΠΈΠ΅ Π² `JsValue` для WASM. -- `turnkey` β€” Ρ€Π°ΡΡˆΠΈΡ€Π΅Π½ΠΈΠ΅ таксономии для Turnkey SDK. - -## Атрибуты `#[provide]` ΠΈ `#[app_error]` - -Атрибут `#[provide(...)]` позволяСт ΠΏΠ΅Ρ€Π΅Π΄Π°Π²Π°Ρ‚ΡŒ ΡΡ‚Ρ€ΡƒΠΊΡ‚ΡƒΡ€ΠΈΡ€ΠΎΠ²Π°Π½Π½ΡƒΡŽ Ρ‚Π΅Π»Π΅ΠΌΠ΅Ρ‚Ρ€ΠΈΡŽ Ρ‡Π΅Ρ€Π΅Π· -`std::error::Request`, Π° `#[app_error(...)]` описываСт прямой ΠΌΠ°ΠΏΠΏΠΈΠ½Π³ Π΄ΠΎΠΌΠ΅Π½Π½ΠΎΠΉ -ошибки Π² `AppError` ΠΈ `AppCode`. Π”Π΅Ρ€ΠΈΠ² сохраняСт синтаксис `thiserror`, Π½ΠΎ -дополняСт Π΅Π³ΠΎ ΠΏΡ€ΠΎΠ²Π°ΠΉΠ΄Π΅Ρ€Π°ΠΌΠΈ Ρ‚Π΅Π»Π΅ΠΌΠ΅Ρ‚Ρ€ΠΈΠΈ ΠΈ Π³ΠΎΡ‚ΠΎΠ²Ρ‹ΠΌΠΈ конвСрсиями Π² Ρ‚ΠΈΠΏΡ‹ `masterror`. +### Π”Π΅Ρ€ΠΈΠ²Ρ‹ для Π΄ΠΎΠΌΠ΅Π½Π½Ρ‹Ρ… ошибок ΠΈ транспорта + +`masterror` прСдоставляСт Ρ€ΠΎΠ΄Π½Ρ‹Π΅ Π΄Π΅Ρ€ΠΈΠ²Ρ‹, Ρ‡Ρ‚ΠΎΠ±Ρ‹ Ρ‚ΠΈΠΏΡ‹ ΠΎΡΡ‚Π°Π²Π°Π»ΠΈΡΡŒ Π²Ρ‹Ρ€Π°Π·ΠΈΡ‚Π΅Π»ΡŒΠ½Ρ‹ΠΌΠΈ, Π° +crate ΠΎΡ‚Π²Π΅Ρ‡Π°Π» Π·Π° конвСрсии, Ρ‚Π΅Π»Π΅ΠΌΠ΅Ρ‚Ρ€ΠΈΡŽ ΠΈ Ρ€Π΅Π΄Π°ΠΊΡ‚ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ сообщСний. + +~~~rust +use std::io; + +use masterror::Error; + +#[derive(Debug, Error)] +#[error("I/O failed: {source}")] +pub struct DomainError { + #[from] + #[source] + source: io::Error, +} + +#[derive(Debug, Error)] +#[error(transparent)] +pub struct WrappedDomainError( + #[from] + #[source] + DomainError +); + +fn load() -> Result<(), DomainError> { + Err(io::Error::other("disk offline").into()) +} + +let err = load().unwrap_err(); +assert_eq!(err.to_string(), "I/O failed: disk offline"); + +let wrapped = WrappedDomainError::from(err); +assert_eq!(wrapped.to_string(), "I/O failed: disk offline"); +~~~ + +- `use masterror::Error;` ΠΏΠΎΠ΄ΠΊΠ»ΡŽΡ‡Π°Π΅Ρ‚ макрос Π΄Π΅Ρ€ΠΈΠ²Π°. +- `#[from]` автоматичСски Ρ€Π΅Π°Π»ΠΈΠ·ΡƒΠ΅Ρ‚ `From<...>` ΠΈ провСряСт Ρ„ΠΎΡ€ΠΌΡƒ Π²Ρ€Π°ΠΏΠΏΠ΅Ρ€Π°. +- `#[error(transparent)]` Π³Π°Ρ€Π°Π½Ρ‚ΠΈΡ€ΡƒΠ΅Ρ‚ ΠΊΠΎΡ€Ρ€Π΅ΠΊΡ‚Π½ΡƒΡŽ ΠΏΡ€ΠΎΠΊΠ»Π°Π΄ΠΊΡƒ `Display`/`source`. +- `#[app_error(kind = ..., code = ..., message)]` сопоставляСт ΠΎΡˆΠΈΠ±ΠΊΡƒ с + `AppError`/`AppCode`; `code = ...` добавляСт `From for AppCode`, Π° + `message` ΠΏΡƒΠ±Π»ΠΈΠΊΡƒΠ΅Ρ‚ Ρ„ΠΎΡ€ΠΌΠ°Ρ‚ΠΈΡ€ΠΎΠ²Π°Π½Π½ΡƒΡŽ строку вмСсто ΠΎΠ±Π΅Π·Π»ΠΈΡ‡Π΅Π½Π½ΠΎΠ³ΠΎ тСкста. +- `masterror::error::template::ErrorTemplate` Ρ€Π°Π·Π±ΠΈΡ€Π°Π΅Ρ‚ строки Ρ„ΠΎΡ€ΠΌΠ°Ρ‚Π°, позволяя + ΡΡ‚Ρ€ΠΎΠΈΡ‚ΡŒ собствСнныС Π΄Π΅Ρ€ΠΈΠ²Ρ‹ Π±Π΅Π· зависимости ΠΎΡ‚ `thiserror`. + +### ВСлСмСтрия, Ρ€Π΅Π΄Π°ΠΊΡ‚ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ ΠΈ ΠΌΠ°ΠΏΠΏΠΈΠ½Π³ΠΈ транспортов + +`#[derive(Masterror)]` ΠΏΡ€Π΅ΠΎΠ±Ρ€Π°Π·ΡƒΠ΅Ρ‚ Π΄ΠΎΠΌΠ΅Π½Π½ΡƒΡŽ ΠΎΡˆΠΈΠ±ΠΊΡƒ Π² [`masterror::Error`], +прикрСпляя ΠΌΠ΅Ρ‚Π°Π΄Π°Π½Π½Ρ‹Π΅, ΠΏΠΎΠ»ΠΈΡ‚ΠΈΠΊΡƒ рСдактирования ΠΈ ΠΌΠ°ΠΏΠΏΠΈΠ½Π³ΠΈ для HTTP/gRPC/RFC7807. + +~~~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, fields("user_id" = hash)), + 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, + #[source] + source: Option +} + +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` ΠΏΡƒΠ±Π»ΠΈΠΊΡƒΠ΅Ρ‚ Ρ„ΠΎΡ€ΠΌΠ°Ρ‚ΠΈΡ€ΠΎΠ²Π°Π½Π½ΡƒΡŽ строку ΠΊΠ°ΠΊ бСзопасноС сообщСниС. +- `redact(message)` Π²ΠΊΠ»ΡŽΡ‡Π°Π΅Ρ‚ Ρ€Π΅Π΄Π°ΠΊΡ‚ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅, Π° `fields("name" = hash)` Π·Π°Π΄Π°Ρ‘Ρ‚ + ΠΏΡ€Π°Π²ΠΈΠ»Π° маскирования для ΠΌΠ΅Ρ‚Π°Π΄Π°Π½Π½Ρ‹Ρ…. +- `telemetry(...)` ΠΏΡ€ΠΈΠ½ΠΈΠΌΠ°Π΅Ρ‚ выраТСния, Π΄Π°ΡŽΡ‰ΠΈΠ΅ `Option`; Π·Π°ΠΏΠΎΠ»Π½Π΅Π½Π½Ρ‹Π΅ поля + ΠΏΠΎΠΏΠ°Π΄Π°ΡŽΡ‚ Π² [`Metadata`]. +- `map.grpc` / `map.problem` Π΄ΠΎΠ±Π°Π²Π»ΡΡŽΡ‚ gRPC-ΠΊΠΎΠ΄ ΠΈ RFC7807 `type` URI. Π”Π΅Ρ€ΠΈΠ² + Π³Π΅Π½Π΅Ρ€ΠΈΡ€ΡƒΠ΅Ρ‚ Ρ‚Π°Π±Π»ΠΈΡ†Ρ‹ `HTTP_MAPPING`, `GRPC_MAPPING`, `PROBLEM_MAPPING`. + +ВсС Π°Ρ‚Ρ€ΠΈΠ±ΡƒΡ‚Ρ‹ уровня ΠΏΠΎΠ»Π΅ΠΉ (`#[from]`, `#[source]`, `#[backtrace]`) ΠΏΡ€ΠΎΠ΄ΠΎΠ»ΠΆΠ°ΡŽΡ‚ +Ρ€Π°Π±ΠΎΡ‚Π°Ρ‚ΡŒ. Π˜ΡΡ‚ΠΎΡ‡Π½ΠΈΠΊΠΈ ΠΈ бэктрСйсы автоматичСски ΠΏΡ€ΠΈΠΊΡ€Π΅ΠΏΠ»ΡΡŽΡ‚ΡΡ ΠΊ +[`masterror::Error`]. + +### ΠŸΡ€ΠΎΠ²Π°ΠΉΠ΄Π΅Ρ€Ρ‹ Ρ‚Π΅Π»Π΅ΠΌΠ΅Ρ‚Ρ€ΠΈΠΈ ΠΈ `AppError` + +`#[provide(...)]` раскрываСт ΡΡ‚Ρ€ΡƒΠΊΡ‚ΡƒΡ€ΠΈΡ€ΠΎΠ²Π°Π½Π½ΡƒΡŽ Ρ‚Π΅Π»Π΅ΠΌΠ΅Ρ‚Ρ€ΠΈΡŽ Ρ‡Π΅Ρ€Π΅Π· +`std::error::Request`, Π° `#[app_error(...)]` описываСт ΠΊΠΎΠ½Π²Π΅Ρ€ΡΠΈΡŽ Π² `AppError` ΠΈ +`AppCode`. ~~~rust use std::error::request_ref; @@ -136,8 +255,8 @@ let via_app = request_ref::(&app).expect("telemetry"); assert_eq!(via_app.name, "db.query"); ~~~ -ΠžΠΏΡ†ΠΈΠΎΠ½Π°Π»ΡŒΠ½Ρ‹Π΅ поля автоматичСски ΠΏΡ€ΠΎΠΏΡƒΡΠΊΠ°ΡŽΡ‚ΡΡ, Ссли значСния Π½Π΅Ρ‚. ΠŸΡ€ΠΈ запросС -значСния `Option` ΠΌΠΎΠΆΠ½ΠΎ Π²Π΅Ρ€Π½ΡƒΡ‚ΡŒ ΠΊΠ°ΠΊ ΠΏΠΎ ссылкС, Ρ‚Π°ΠΊ ΠΈ ΠΏΠ΅Ρ€Π΅Π΄Π°Ρ‚ΡŒ Π²Π»Π°Π΄Π΅Π½ΠΈΠ΅: +ΠžΠΏΡ†ΠΈΠΎΠ½Π°Π»ΡŒΠ½Π°Ρ тСлСмСтрия Π½Π΅ рСгистрируСт ΠΏΡ€ΠΎΠ²Π°ΠΉΠ΄Π΅Ρ€, Ссли Π·Π½Π°Ρ‡Π΅Π½ΠΈΠ΅ `None`, Π° +Π²Π»Π°Π΄Π΅Π½ΠΈΠ΅ ΠΌΠΎΠΆΠ½ΠΎ ΠΏΠ΅Ρ€Π΅Π΄Π°Ρ‚ΡŒ Ρ‡Π΅Ρ€Π΅Π· `value = ...`. ~~~rust use masterror::{AppCode, AppErrorKind, Error}; @@ -162,8 +281,7 @@ assert!(request_ref::(&noisy).is_some()); assert!(request_ref::(&silent).is_none()); ~~~ -Для пСрСчислСний каТдая Π²Π΅Ρ‚ΠΊΠ° ΠΌΠΎΠΆΠ΅Ρ‚ Π·Π°Π΄Π°Π²Π°Ρ‚ΡŒ ΡΠΎΠ±ΡΡ‚Π²Π΅Π½Π½ΡƒΡŽ Ρ‚Π΅Π»Π΅ΠΌΠ΅Ρ‚Ρ€ΠΈΡŽ ΠΈ -ΠΊΠΎΠ½Π²Π΅Ρ€ΡΠΈΡŽ. Π”Π΅Ρ€ΠΈΠ² сгСнСрируСт Π΅Π΄ΠΈΠ½Ρ‹ΠΉ `From` для `AppError`/`AppCode`: +ΠŸΠ΅Ρ€Π΅Ρ‡ΠΈΡΠ»Π΅Π½ΠΈΡ ΠΏΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΈΠ²Π°ΡŽΡ‚ собствСнныС ΠΌΠ°ΠΏΠΏΠΈΠ½Π³ΠΈ ΠΈ ΠΏΡ€ΠΎΠ²Π°ΠΉΠ΄Π΅Ρ€Ρ‹ Π½Π° Π²Π°Ρ€ΠΈΠ°Π½Ρ‚: ~~~rust #[derive(Debug, Error)] @@ -191,252 +309,34 @@ let app: AppError = owned.into(); assert!(matches!(app.kind, AppErrorKind::Service)); ~~~ -Π’ ΠΎΡ‚Π»ΠΈΡ‡ΠΈΠ΅ ΠΎΡ‚ `thiserror`, Π²Ρ‹ ΠΏΠΎΠ»ΡƒΡ‡Π°Π΅Ρ‚Π΅ Π΄ΠΎΠΏΠΎΠ»Π½ΠΈΡ‚Π΅Π»ΡŒΠ½ΡƒΡŽ ΡΡ‚Ρ€ΡƒΠΊΡ‚ΡƒΡ€ΠΈΡ€ΠΎΠ²Π°Π½Π½ΡƒΡŽ -ΠΈΠ½Ρ„ΠΎΡ€ΠΌΠ°Ρ†ΠΈΡŽ ΠΈ прямой ΠΌΠ°ΠΏΠΏΠΈΠ½Π³ Π² `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, fields("user_id" = hash)), - 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, - #[source] - source: Option, -} - -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`] Π² Ρ€Π΅ΠΆΠΈΠΌ рСдактирования Π½Π° - транспортной Π³Ρ€Π°Π½ΠΈΡ†Π΅, `fields("name" = hash, "card" = last4)` пСрСопрСдСляСт - ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΡƒ ΠΌΠ΅Ρ‚Π°Π΄Π°Π½Π½Ρ‹Ρ… (`hash`, `last4`, `redact`, `none`). -- `telemetry(...)` ΠΏΡ€ΠΈΠ½ΠΈΠΌΠ°Π΅Ρ‚ выраТСния, Π²ΠΎΠ·Π²Ρ€Π°Ρ‰Π°ΡŽΡ‰ΠΈΠ΅ - `Option`. КаТдоС ΠΏΡ€ΠΈΡΡƒΡ‚ΡΡ‚Π²ΡƒΡŽΡ‰Π΅Π΅ ΠΏΠΎΠ»Π΅ добавляСтся Π² - [`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`, Π½ΠΎ любая -подстановка ΠΌΠΎΠΆΠ΅Ρ‚ Π·Π°ΠΏΡ€ΠΎΡΠΈΡ‚ΡŒ Π΄Ρ€ΡƒΠ³ΠΎΠΉ Ρ„ΠΎΡ€ΠΌΠ°Ρ‚Ρ‚Π΅Ρ€. -`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`, позволяя ΠΏΡ€ΠΎΠ³Ρ€Π°ΠΌΠΌΠ½ΠΎ ΠΏΠ΅Ρ€Π΅ΠΊΠ»ΡŽΡ‡Π°Ρ‚ΡŒ Ρ„Π»Π°Π³ `#`. -- Display-плСйсхолдСры ΡΠΎΡ…Ρ€Π°Π½ΡΡŽΡ‚ исходныС ΠΏΠ°Ρ€Π°ΠΌΠ΅Ρ‚Ρ€Ρ‹ форматирования: - ΠΌΠ΅Ρ‚ΠΎΠ΄Ρ‹ `TemplateFormatter::display_spec()` ΠΈ - `TemplateFormatter::format_fragment()` Π²ΠΎΠ·Π²Ρ€Π°Ρ‰Π°ΡŽΡ‚ `:>8`, `:.3` ΠΈ Π΄Ρ€ΡƒΠ³ΠΈΠ΅ - Π²Π°Ρ€ΠΈΠ°Π½Ρ‚Ρ‹ Π±Π΅Π· нСобходимости ΡΠΎΠ±ΠΈΡ€Π°Ρ‚ΡŒ строку Π²Ρ€ΡƒΡ‡Π½ΡƒΡŽ. - -~~~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")); -~~~ +Π’Π°ΠΊ Π²Ρ‹ сохраняСтС Π·Π½Π°ΠΊΠΎΠΌΡ‹ΠΉ интСрфСйс `thiserror`, Π½ΠΎ ΠΏΠΎΠ»ΡƒΡ‡Π°Π΅Ρ‚Π΅ Ρ‚Π΅Π»Π΅ΠΌΠ΅Ρ‚Ρ€ΠΈΡŽ ΠΈ +Π³ΠΎΡ‚ΠΎΠ²Ρ‹Π΅ конвСрсии Π² `AppError`/`AppCode` Π±Π΅Π· Ρ€ΡƒΡ‡Π½ΠΎΠ³ΠΎ ΠΊΠΎΠ΄Π°. -`masterror::error::template::ErrorTemplate` позволяСт Ρ€Π°Π·ΠΎΠ±Ρ€Π°Ρ‚ΡŒ шаблон ΠΈ -ΠΏΡ€ΠΎΠ³Ρ€Π°ΠΌΠΌΠ½ΠΎ ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΈΡ‚ΡŒ Π·Π°ΠΏΡ€ΠΎΡˆΠ΅Π½Π½Ρ‹Π΅ Ρ„ΠΎΡ€ΠΌΠ°Ρ‚Ρ‚Π΅Ρ€Ρ‹; пСрСчислСниС -`TemplateFormatterKind` Π²ΠΎΠ·Π²Ρ€Π°Ρ‰Π°Π΅Ρ‚ Π½Π°Π·Π²Π°Π½ΠΈΠ΅ Ρ‚Ρ€Π΅ΠΉΡ‚Π° для ΠΊΠ°ΠΆΠ΄ΠΎΠ³ΠΎ плСйсхолдСра: +### Problem JSON ΠΈ подсказки retry/auth ~~~rust -use masterror::error::template::{ - ErrorTemplate, TemplateFormatter, TemplateFormatterKind -}; +use masterror::{AppError, AppErrorKind, ProblemJson}; +use std::time::Duration; -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, - 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, - &TemplateFormatter::Debug { alternate: false } +let problem = ProblemJson::from_app_error( + AppError::new(AppErrorKind::Unauthorized, "Token expired") + .with_retry_after_duration(Duration::from_secs(30)) + .with_www_authenticate(r#"Bearer realm="api", error="invalid_token""#) ); -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()); -~~~ -ΠžΠΏΡ†ΠΈΠΈ выравнивания, точности ΠΈ заполнСния для `Display` ΡΠΎΡ…Ρ€Π°Π½ΡΡŽΡ‚ΡΡ ΠΈ доступны -для прямой ΠΏΠ΅Ρ€Π΅Π΄Π°Ρ‡ΠΈ Π² `write!`: - -~~~rust -use masterror::error::template::ErrorTemplate; - -let aligned = ErrorTemplate::parse("{value:>8}").expect("parse"); -let display = aligned.placeholders().next().expect("display placeholder"); -assert_eq!(display.formatter().display_spec(), Some(">8")); -assert_eq!( - display - .formatter() - .format_fragment() - .as_deref(), - Some(">8") -); +assert_eq!(problem.status, 401); +assert_eq!(problem.retry_after, Some(30)); +assert_eq!(problem.grpc.expect("grpc").name, "UNAUTHENTICATED"); ~~~ -ДинамичСскиС ΡˆΠΈΡ€ΠΈΠ½Π° ΠΈ Ρ‚ΠΎΡ‡Π½ΠΎΡΡ‚ΡŒ (`{value:>width$}`, `{value:.precision$}`) -Ρ‚ΠΎΠΆΠ΅ доходят Π΄ΠΎ Π²Ρ‹Π·ΠΎΠ²Π° `write!`, Ссли ΠΎΠ±ΡŠΡΠ²ΠΈΡ‚ΡŒ ΡΠΎΠΎΡ‚Π²Π΅Ρ‚ΡΡ‚Π²ΡƒΡŽΡ‰ΠΈΠ΅ Π°Ρ€Π³ΡƒΠΌΠ΅Π½Ρ‚Ρ‹ Π² -Π°Ρ‚Ρ€ΠΈΠ±ΡƒΡ‚Π΅ `#[error(...)]`: - -~~~rust -use masterror::Error; - -#[derive(Debug, Error)] -#[error("{value:>width$}", value = .value, width = .width)] -struct DynamicWidth { - value: &'static str, - width: usize, -} - -#[derive(Debug, Error)] -#[error("{value:.precision$}", value = .value, precision = .precision)] -struct DynamicPrecision { - value: f64, - precision: usize, -} - -let width = DynamicWidth { - value: "x", - width: 5, -}; -let precision = DynamicPrecision { - value: 123.456_f64, - precision: 4, -}; - -assert_eq!(width.to_string(), format!("{value:>width$}", value = "x", width = 5)); -assert_eq!( - precision.to_string(), - format!("{value:.precision$}", value = 123.456_f64, precision = 4) -); -~~~ +### Π”ΠΎΠΏΠΎΠ»Π½ΠΈΡ‚Π΅Π»ΡŒΠ½Ρ‹Π΅ ΠΌΠ°Ρ‚Π΅Ρ€ΠΈΠ°Π»Ρ‹ -> **Π‘ΠΎΠ²ΠΌΠ΅ΡΡ‚ΠΈΠΌΠΎΡΡ‚ΡŒ с `thiserror` v2.** ДоступныС спСцификаторы, сообщСния ΠΎΠ± -> ΠΎΡˆΠΈΠ±ΠΊΠ°Ρ… ΠΈ ΠΏΠΎΠ²Π΅Π΄Π΅Π½ΠΈΠ΅ ΡΠΎΠ²ΠΏΠ°Π΄Π°ΡŽΡ‚ с `thiserror` 2.x, поэтому миграция с -> `thiserror::Error` Π½Π° `masterror::Error` Π½Π΅ Ρ‚Ρ€Π΅Π±ΡƒΠ΅Ρ‚ ΠΏΠ΅Ρ€Π΅ΠΏΠΈΡΡ‹Π²Π°Ρ‚ΡŒ ΡˆΠ°Π±Π»ΠΎΠ½Ρ‹. +- [Π’ΠΈΠΊΠΈ ΠΏΠΎ ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠ΅ ошибок](docs/wiki/index.md) с ΠΏΠΎΡˆΠ°Π³ΠΎΠ²Ρ‹ΠΌΠΈ руководствами, + сравнСниСм `thiserror`/`anyhow` ΠΈ Ρ€Π΅Ρ†Π΅ΠΏΡ‚Π°ΠΌΠΈ устранСния ΠΏΡ€ΠΎΠ±Π»Π΅ΠΌ. +- [ДокумСнтация Π½Π° docs.rs](https://docs.rs/masterror) с ΠΏΠΎΠ΄Ρ€ΠΎΠ±Π½Ρ‹ΠΌΠΈ Ρ‚Π°Π±Π»ΠΈΡ†Π°ΠΌΠΈ ΠΏΠΎ + Ρ„ΠΈΡ‡Π°ΠΌ ΠΈ транспортам. +- [`CHANGELOG.md`](CHANGELOG.md) для истории Ρ€Π΅Π»ΠΈΠ·ΠΎΠ² ΠΈ ΠΌΠΈΠ³Ρ€Π°Ρ†ΠΈΠΉ. -## ЛицСнзия +--- -ΠŸΡ€ΠΎΠ΅ΠΊΡ‚ распространяСтся ΠΏΠΎ Π»ΠΈΡ†Π΅Π½Π·ΠΈΠΈ Apache-2.0 ΠΈΠ»ΠΈ MIT Π½Π° ваш Π²Ρ‹Π±ΠΎΡ€. +MSRV: **1.90** Β· ЛицСнзия: **MIT OR Apache-2.0** Β· Π‘Π΅Π· `unsafe` diff --git a/README.template.md b/README.template.md index 1095ee8..1f743b3 100644 --- a/README.template.md +++ b/README.template.md @@ -14,25 +14,58 @@ > πŸ‡·πŸ‡Ί Π§ΠΈΡ‚Π°ΠΉΡ‚Π΅ README Π½Π° [русском языкС](README.ru.md). -Small, pragmatic error model for API-heavy Rust services with native derives -and typed telemetry. -Core is framework-agnostic; integrations are opt-in via feature flags. -Stable categories, conservative HTTP mapping, no `unsafe`. - -- Core types: `AppError`, `AppErrorKind`, `AppResult`, `AppCode`, `ProblemJson`, `ErrorResponse`, `Metadata` -- 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 -- Conversions from `sqlx`, `reqwest`, `redis`, `validator`, `config`, `tokio` -- Turnkey domain taxonomy and helpers (`turnkey` feature) - -πŸ‘‰ Explore the new [error-handling wiki](docs/wiki/index.md) for step-by-step -guides, comparisons with `thiserror`/`anyhow`, and troubleshooting recipes. - ---- +`masterror` grew from a handful of helpers into a workspace of composable crates for +building consistent, observable error surfaces across Rust services. The core +crate stays framework-agnostic, while feature flags light up transport adapters, +integrations and telemetry without pulling in heavyweight defaults. No +`unsafe`, MSRV is pinned, and the derive macros keep your domain types in charge +of redaction and metadata. + +### Highlights + +- **Unified taxonomy.** `AppError`, `AppErrorKind` and `AppCode` model domain and + transport concerns with conservative HTTP/gRPC mappings, turnkey retry/auth + hints and RFC7807 output via `ProblemJson`. +- **Native derives.** `#[derive(Error)]`, `#[derive(Masterror)]`, `#[app_error]`, + `#[masterror(...)]` and `#[provide]` wire custom types into `AppError` while + forwarding sources, backtraces, telemetry providers and redaction policy. +- **Typed telemetry.** `Metadata` stores structured key/value context with + per-field redaction controls and builders in `field::*`, so logs stay + structured without manual `String` maps. +- **Transport adapters.** Optional features expose Actix/Axum responders, + `tonic::Status` conversions, WASM/browser logging and OpenAPI schema + generation without contaminating the lean default build. +- **Battle-tested integrations.** Enable focused mappings for `sqlx`, + `reqwest`, `redis`, `validator`, `config`, `tokio`, `teloxide`, `multipart`, + Telegram WebApp SDK and more β€” each translating library errors into the + taxonomy with telemetry attached. +- **Turnkey defaults.** The `turnkey` module ships a ready-to-use error catalog, + helper builders and tracing instrumentation for teams that want a consistent + baseline out of the box. + +### Workspace crates + +| Crate | What it provides | When to depend on it | +| --- | --- | --- | +| [`masterror`](https://crates.io/crates/masterror) | Core error types, metadata builders, transports, integrations and the prelude. | Application crates, services and libraries that want a stable error surface. | +| [`masterror-derive`](masterror-derive/README.md) | Proc-macros backing `#[derive(Error)]`, `#[derive(Masterror)]`, `#[app_error]` and `#[provide]`. | Brought in automatically via `masterror`; depend directly only for macro hacking. | +| [`masterror-template`](masterror-template/README.md) | Shared template parser used by the derive macros for formatter analysis. | Internal dependency; reuse when you need the template parser elsewhere. | + +### Feature flags at a glance + +Pick only what you need; everything is off by default. + +- **Web transports:** `axum`, `actix`, `multipart`, `openapi`, `serde_json`. +- **Telemetry & observability:** `tracing`, `metrics`, `backtrace`. +- **Async & IO integrations:** `tokio`, `reqwest`, `sqlx`, `sqlx-migrate`, + `redis`, `validator`, `config`. +- **Messaging & bots:** `teloxide`, `telegram-webapp-sdk`. +- **Front-end tooling:** `frontend` for WASM/browser console logging. +- **gRPC:** `tonic` to emit `tonic::Status` responses. +- **Batteries included:** `turnkey` to adopt the pre-built taxonomy and helpers. + +The build script keeps the full feature snippet below in sync with +`Cargo.toml`. ### TL;DR @@ -45,48 +78,8 @@ masterror = { version = "{{CRATE_VERSION}}", default-features = false } # ] } ~~~ -*Since v0.5.0: derive custom errors via `#[derive(Error)]` (`use masterror::Error;`) and inspect browser logging failures with `BrowserConsoleError::context()`.* -*Since v0.4.0: optional `frontend` feature for WASM/browser console logging.* -*Since v0.3.0: stable `AppCode` enum and extended `ErrorResponse` with retry/authentication metadata.* -*Since v0.15.0: RFC7807 `ProblemJson` responses for HTTP integrations and `tonic::Status` conversion.* - --- -
- Why this crate? - -- **Stable taxonomy.** Small set of `AppErrorKind` categories mapping conservatively to HTTP. -- **Framework-agnostic.** No assumptions, no `unsafe`, MSRV pinned. -- **Opt-in integrations.** Zero default features; you enable what you need. -- **Clean wire contract.** `ProblemJson { type?, title, status, detail?, code, grpc?, metadata? }` with `Retry-After` / `WWW-Authenticate` headers when present. -- **Typed telemetry.** `Metadata` preserves structured key/value context without `String` maps. -- **One log at boundary.** Log once with `tracing`. -- **Less boilerplate.** Built-in conversions, compact prelude, and the - native `masterror::Error` derive with `#[from]` / `#[error(transparent)]` - support. -- **Consistent workspace.** Same error surface across crates. - -
- -
- Installation - -~~~toml -[dependencies] -# lean core -masterror = { version = "{{CRATE_VERSION}}", default-features = false } - -# with Axum/Actix + JSON + integrations -# masterror = { version = "{{CRATE_VERSION}}", features = [ -{{FEATURE_SNIPPET}} -# ] } -~~~ - -**MSRV:** {{MSRV}} -**No unsafe:** forbidden by crate. - -
-
Quick start @@ -118,7 +111,10 @@ fn do_work(flag: bool) -> AppResult<()> {
- Derive custom errors + Derive domain errors and map them to transports + +`masterror` ships native derives so your domain types stay expressive while the +crate handles conversions, telemetry and redaction for you. ~~~rust use std::io; @@ -152,7 +148,7 @@ let wrapped = WrappedDomainError::from(err); assert_eq!(wrapped.to_string(), "I/O failed: disk offline"); ~~~ -- `use masterror::Error;` brings the crate's derive macro into scope. +- `use masterror::Error;` brings the derive macro into scope. - `#[from]` automatically implements `From<...>` while ensuring wrapper shapes are valid. - `#[error(transparent)]` enforces single-field wrappers that forward @@ -175,12 +171,15 @@ 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(...)]` +
+ +
+ Attach telemetry, redaction policy and conversions -`#[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: +`#[derive(Masterror)]` wires a domain error into [`masterror::Error`], adds +metadata, redaction policy and optional transport mappings. The accompanying +`#[masterror(...)]` attribute mirrors the `#[app_error]` syntax while staying +explicit about telemetry and redaction. ~~~rust use masterror::{ @@ -247,103 +246,10 @@ 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 -referencing fields with `.field` or `.0`. The derive now understands chained -segments, so projections like `.limits.lo`, `.0.data` or -`.suggestion.as_ref().map_or_else(...)` keep compiling unchanged. Raw -identifiers and tuple indices are preserved, ensuring keywords such as -`r#type` and tuple fields continue to work even when you call methods on the -projected value. - -~~~rust -use masterror::Error; - -#[derive(Debug)] -struct Limits { - lo: i32, - hi: i32, -} - -#[derive(Debug, Error)] -#[error( - "range {lo}-{hi} suggestion {suggestion}", - lo = .limits.lo, - hi = .limits.hi, - suggestion = .suggestion.as_ref().map_or_else(|| "", |s| s.as_str()) -)] -struct RangeError { - limits: Limits, - suggestion: Option, -} - -#[derive(Debug)] -struct Payload { - data: &'static str, -} - -#[derive(Debug, Error)] -enum UiError { - #[error("tuple data {data}", data = .0.data)] - Tuple(Payload), - #[error( - "named suggestion {value}", - value = .suggestion.as_ref().map_or_else(|| "", |s| s.as_str()) - )] - Named { suggestion: Option }, -} -~~~ - -#### AppError conversions - -Annotating structs or enum variants with `#[app_error(...)]` captures the -metadata required to convert the domain error into `AppError` (and optionally -`AppCode`). Every variant in an enum must provide the mapping when any variant -requests it. - -~~~rust -use masterror::{AppCode, AppError, AppErrorKind, Error}; - -#[derive(Debug, Error)] -#[error("missing flag: {name}")] -#[app_error(kind = AppErrorKind::BadRequest, code = AppCode::BadRequest, message)] -struct MissingFlag { - name: &'static str, -} - -let app: AppError = MissingFlag { name: "feature" }.into(); -assert!(matches!(app.kind, AppErrorKind::BadRequest)); -assert_eq!(app.message.as_deref(), Some("missing flag: feature")); - -let code: AppCode = MissingFlag { name: "feature" }.into(); -assert!(matches!(code, AppCode::BadRequest)); -~~~ - -For enums, each variant specifies the mapping while the derive generates a -single `From` implementation that matches every variant: - -~~~rust -#[derive(Debug, Error)] -enum ApiError { - #[error("missing resource {id}")] - #[app_error( - kind = AppErrorKind::NotFound, - code = AppCode::NotFound, - message - )] - Missing { id: u64 }, - #[error("backend unavailable")] - #[app_error(kind = AppErrorKind::Service, code = AppCode::Service)] - Backend, -} - -let missing = ApiError::Missing { id: 7 }; -let as_app: AppError = missing.into(); -assert_eq!(as_app.message.as_deref(), Some("missing resource 7")); -~~~ +
-#### Structured telemetry providers and AppError mappings +
+ Structured telemetry providers and AppError mappings `#[provide(...)]` exposes typed context through `std::error::Request`, while `#[app_error(...)]` records how your domain error translates into `AppError` @@ -444,173 +350,12 @@ assert!(matches!(app.kind, AppErrorKind::Service)); Compared to `thiserror`, you retain the familiar deriving surface while gaining structured telemetry (`#[provide]`) and first-class conversions into -`AppError`/`AppCode` without writing manual `From` implementations. - -#### Formatter traits - -Placeholders default to `Display` (`{value}`) but can opt into richer -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; - -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, 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, - 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, - &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()); -~~~ - -Display-only format specs (alignment, precision, fill β€” including `#` as a fill -character) are preserved so you can forward them to `write!` without rebuilding -the fragment: - -~~~rust -use masterror::error::template::ErrorTemplate; - -let aligned = ErrorTemplate::parse("{value:>8}").expect("parse"); -let display = aligned.placeholders().next().expect("display placeholder"); -assert_eq!(display.formatter().display_spec(), Some(">8")); -assert_eq!( - display - .formatter() - .format_fragment() - .as_deref(), - Some(">8") -); - -let hashed = ErrorTemplate::parse("{value:#>4}").expect("parse"); -let hash_placeholder = hashed - .placeholders() - .next() - .expect("hash-fill display placeholder"); -assert_eq!(hash_placeholder.formatter().display_spec(), Some("#>4")); -assert_eq!( - hash_placeholder - .formatter() - .format_fragment() - .as_deref(), - Some("#>4") -); -~~~ - -> **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}; - -let template = ErrorTemplate::parse("{code}: {message}").expect("parse"); -let display = template.display_with(|placeholder, f| match placeholder.identifier() { - TemplateIdentifier::Named("code") => write!(f, "{}", 404), - TemplateIdentifier::Named("message") => f.write_str("Not Found"), - _ => Ok(()), -}); - -assert_eq!(display.to_string(), "404: Not Found"); -``` +`AppError`/`AppCode` without manual glue.
- Error response payload + Problem JSON payloads and retry/authentication hints ~~~rust use masterror::{AppError, AppErrorKind, ProblemJson}; @@ -629,143 +374,14 @@ assert_eq!(problem.grpc.expect("grpc").name, "UNAUTHENTICATED");
-
- Web framework integrations - -
- Axum - -~~~rust -// features = ["axum", "serde_json"] -... - assert!(payload.is_object()); - - #[cfg(target_arch = "wasm32")] - { - if let Err(console_err) = err.log_to_browser_console() { - eprintln!( - "failed to log to browser console: {:?}", - console_err.context() - ); - } - } - - Ok(()) -} -~~~ - -- On non-WASM targets `log_to_browser_console` returns - `BrowserConsoleError::UnsupportedTarget`. -- `BrowserConsoleError::context()` exposes optional browser diagnostics for - logging/telemetry when console logging fails. - -
- -
- -
- Feature flags - -{{FEATURE_BULLETS}} - -
- -
- Conversions - -{{CONVERSION_BULLETS}} - -
- -
- Typical setups - -Minimal core: - -~~~toml -masterror = { version = "{{CRATE_VERSION}}", default-features = false } -~~~ - -API (Axum + JSON + deps): - -~~~toml -masterror = { version = "{{CRATE_VERSION}}", features = [ - "axum", "serde_json", "openapi", - "sqlx", "reqwest", "redis", "validator", "config", "tokio" -] } -~~~ - -API (Actix + JSON + deps): - -~~~toml -masterror = { version = "{{CRATE_VERSION}}", features = [ - "actix", "serde_json", "openapi", - "sqlx", "reqwest", "redis", "validator", "config", "tokio" -] } -~~~ - -
- -
- Turnkey - -~~~rust -// features = ["turnkey"] -use masterror::turnkey::{classify_turnkey_error, TurnkeyError, TurnkeyErrorKind}; -use masterror::{AppError, AppErrorKind}; - -// Classify a raw SDK/provider error -let kind = classify_turnkey_error("429 Too Many Requests"); -assert!(matches!(kind, TurnkeyErrorKind::RateLimited)); - -// Wrap into AppError -let e = TurnkeyError::new(TurnkeyErrorKind::RateLimited, "throttled upstream"); -let app: AppError = e.into(); -assert_eq!(app.kind, AppErrorKind::RateLimited); -~~~ - -
- -
- Migration 0.2 β†’ 0.3 - -- Use `ErrorResponse::new(status, AppCode::..., "msg")` instead of legacy -- New helpers: `.with_retry_after_secs`, `.with_retry_after_duration`, `.with_www_authenticate` -- `ErrorResponse::new_legacy` is temporary shim - -
- -
- Versioning & MSRV +### Further resources -Semantic versioning. Breaking API/wire contract β†’ major bump. -MSRV = {{MSRV}} (may raise in minor, never in patch). +- Explore the [error-handling wiki](docs/wiki/index.md) for step-by-step guides, + comparisons with `thiserror`/`anyhow`, and troubleshooting recipes. +- Browse the [crate documentation on docs.rs](https://docs.rs/masterror) for API + details, feature-specific guides and transport tables. +- Check [`CHANGELOG.md`](CHANGELOG.md) for release highlights and migration notes. -
- -
- Release checklist - -1. `cargo +nightly fmt --` -1. `cargo clippy -- -D warnings` -1. `cargo test --all` -1. `cargo build` (regenerates README.md from the template) -1. `cargo doc --no-deps` -1. `cargo package --locked` - -
- -
- Non-goals - -- Not a general-purpose error aggregator like `anyhow` -- Not a replacement for your domain errors - -
- -
- License - -Apache-2.0 OR MIT, at your option. +--- -
+MSRV: **{{MSRV}}** Β· License: **MIT OR Apache-2.0** Β· No `unsafe`