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).
[](https://crates.io/crates/masterror)
[](https://docs.rs/masterror)
@@ -11,47 +11,74 @@
[](https://github.com/RAprogramm/masterror/actions/workflows/ci.yml?query=branch%3Amain)
[](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`