From d87be1aa361a7c82aaf5520ee334d41dbdda10ef Mon Sep 17 00:00:00 2001 From: RAprogramm Date: Sun, 5 Jul 2026 12:57:12 +0700 Subject: [PATCH 1/2] #449 docs: fix fabricated and stale claims in rustdoc and README --- README.md | 108 ++++++++++---------------- README.template.md | 108 ++++++++++---------------- src/app_error.rs | 10 ++- src/app_error/core.rs | 11 +-- src/app_error/core/backtrace.rs | 25 +----- src/app_error/core/display.rs | 114 +++++++++------------------- src/app_error/core/introspection.rs | 26 ++++--- src/code/app_code.rs | 5 +- src/convert.rs | 23 +++--- src/convert/actix.rs | 16 ++-- src/convert/init_data.rs | 11 +-- src/convert/multipart.rs | 5 +- src/convert/redis.rs | 28 ++++--- src/convert/reqwest.rs | 3 +- src/convert/serde_json.rs | 9 ++- src/convert/teloxide.rs | 4 +- src/convert/tokio.rs | 9 ++- src/lib.rs | 22 ++++-- src/response.rs | 2 +- src/response/actix_impl.rs | 5 +- src/response/axum_impl.rs | 5 +- src/result_ext.rs | 8 +- 22 files changed, 243 insertions(+), 314 deletions(-) diff --git a/README.md b/README.md index f5feb03..b0621d7 100644 --- a/README.md +++ b/README.md @@ -74,9 +74,12 @@ of redaction and metadata. - **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. +- **Native derives.** `#[derive(Error)]` and `#[derive(Masterror)]` wire custom + types into the runtime types. `#[derive(Masterror)]` with `#[masterror(...)]` + forwards sources, backtraces, telemetry fields and redaction policy; + `#[app_error]` maps a derived error to an `AppErrorKind`/`AppCode` (optionally + exposing its `Display` message) and `#[provide]` registers typed telemetry + providers on the domain error itself. - **Typed telemetry.** `Metadata` stores structured key/value context (strings, integers, floats, durations, IP addresses and optional JSON) with per-field redaction controls and builders in `field::*`, so logs stay structured without @@ -86,11 +89,11 @@ of redaction and metadata. 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. + Telegram init-data validation 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. + a heuristic classifier and conservative mappings into the canonical taxonomy + for teams that want a consistent baseline out of the box. - **Typed control-flow macros.** `ensure!` and `fail!` short-circuit functions with your domain errors without allocating or formatting on the happy path. @@ -128,14 +131,16 @@ of redaction and metadata. ## Feature Flags -Pick only what you need; everything is off by default. +Pick only what you need; the default feature set is just `std`, everything +else is opt-in. - **Web transports:** `axum`, `actix`, `multipart`, `openapi`, `serde_json`. - **Telemetry & observability:** `tracing`, `metrics`, `backtrace`, `colored` for colored terminal output. - **Async & IO integrations:** `tokio`, `reqwest`, `sqlx`, `sqlx-migrate`, `redis`, `validator`, `config`. -- **Messaging & bots:** `teloxide`, `telegram-webapp-sdk`. +- **Messaging & bots:** `teloxide`, `init-data` for Telegram Mini App + init-data validation via `init-data-rs`. - **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. @@ -195,7 +200,7 @@ cargo bench -F benchmarks --bench error_paths The suite emits two groups: - `context_into_error/*` promotes a dummy source error with representative - metadata (strings, counters, durations, IPs) through `Context::into_error` in + metadata (strings, counters, durations, IPs) through `ResultExt::ctx` in both redacted and non-redacted modes. - `problem_json_from_app_error/*` consumes the resulting `AppError` values to build RFC 7807 payloads via `ProblemJson::from_app_error`, showing how message @@ -473,9 +478,17 @@ generated [`masterror::Error`]. `#[provide(...)]` exposes typed context through `std::error::Request`, while `#[app_error(...)]` records how your domain error translates into `AppError` -and `AppCode`. The derive mirrors `thiserror`'s syntax and extends it with -optional telemetry propagation and direct conversions into the `masterror` -runtime types. +and `AppCode`. The derive mirrors `thiserror`'s syntax. Note that the +generated `From` conversions produce an `AppError` carrying only the mapped +kind and code (plus the `Display` output as public message when the `message` +flag is set) — the original domain error is dropped, so its sources and +telemetry providers are not forwarded. Request telemetry from the domain +error before converting. + +`request_ref`/`request_value` and the `std::error::Request` machinery require +a nightly toolchain (`error_generic_member_access`); the crate detects +compiler support at build time and only enables provider integration when +available. ~~~rust use std::error::request_ref; @@ -507,8 +520,7 @@ let snapshot = request_ref::(&err).expect("telemetry"); assert_eq!(snapshot.value, 42); let app: AppError = err.into(); -let via_app = request_ref::(&app).expect("telemetry"); -assert_eq!(via_app.name, "db.query"); +assert!(matches!(app.kind, AppErrorKind::Service)); ~~~ Optional telemetry only surfaces when present, so `None` does not register a @@ -579,11 +591,10 @@ structured telemetry (`#[provide]`) and first-class conversions into ~~~rust use masterror::{AppError, AppErrorKind, ProblemJson}; -use std::time::Duration; let problem = ProblemJson::from_app_error( AppError::new(AppErrorKind::Unauthorized, "Token expired") - .with_retry_after_duration(Duration::from_secs(30)) + .with_retry_after_secs(30) .with_www_authenticate(r#"Bearer realm="api", error="invalid_token""#) ); @@ -595,36 +606,19 @@ assert_eq!(problem.grpc.expect("grpc").name, "UNAUTHENTICATED");
- Environment-aware error formatting with DisplayMode - -The `DisplayMode` API lets you control error output formatting based on deployment -environment without changing your error handling code. Three modes are available: - -- **`DisplayMode::Prod`** — Lightweight JSON output with minimal fields, optimized - for production logs. Only includes `kind`, `code`, and `message` (if not redacted). - Filters sensitive metadata automatically. - -- **`DisplayMode::Local`** — Human-readable multi-line output with full context. - Shows error details, complete source chain, all metadata, and backtrace (if enabled). - Best for local development and debugging. + Environment detection with DisplayMode -- **`DisplayMode::Staging`** — JSON output with additional context. Includes - `kind`, `code`, `message`, limited `source_chain`, and filtered metadata. - Useful for staging environments where you need structured logs with more detail. +`DisplayMode` detects the deployment environment (`Prod`, `Local` or +`Staging`) so your code can branch on it. `DisplayMode::current()` resolves +the mode in this order and caches the result on first access: -**Automatic Environment Detection:** - -The mode is auto-detected in this order: 1. `MASTERROR_ENV` environment variable (`prod`, `local`, or `staging`) -2. `KUBERNETES_SERVICE_HOST` presence (triggers `Prod` mode) +2. `KUBERNETES_SERVICE_HOST` presence (selects `Prod`) 3. Build configuration (`debug_assertions` → `Local`, release → `Prod`) -The result is cached on first access for zero-cost subsequent calls. - ~~~rust use masterror::DisplayMode; -// Query the current mode (cached after first call) let mode = DisplayMode::current(); match mode { @@ -634,41 +628,26 @@ match mode { } ~~~ +Note: `Display` for `AppError` does not consult `DisplayMode` yet — the +output is identical in all modes. The only formatting branch today is the +`colored` feature described below; mode-aware formatting is not wired up. + **Colored Terminal Output:** -Enable the `colored` feature for enhanced terminal output in local mode: +Enable the `colored` feature for enhanced terminal output. It applies +whenever the feature is enabled, regardless of the detected mode: ~~~toml [dependencies] masterror = { version = "0.28.0", features = ["colored"] } ~~~ -With `colored` enabled, errors display with syntax highlighting: -- Error kind and code in bold -- Error messages in color -- Source chain with indentation -- Metadata keys highlighted - -~~~rust -use masterror::{AppError, field}; - -let error = AppError::not_found("User not found") - .with_field(field::str("user_id", "12345")) - .with_field(field::str("request_id", "abc-def")); - -// Without 'colored': plain text -// With 'colored': color-coded output in terminals -println!("{}", error); -~~~ - -**Production vs Development Output:** - -Without `colored` feature, errors display their `AppErrorKind` label: +Without the `colored` feature, errors display their `AppErrorKind` label: ~~~ NotFound ~~~ -With `colored` feature, full multi-line format with context: +With `colored`, a full multi-line format with context: ~~~ Error: NotFound Code: NOT_FOUND @@ -679,9 +658,6 @@ Context: request_id: abc-def ~~~ -This separation keeps production logs clean while giving developers rich context -during local debugging sessions. -
@@ -707,7 +683,7 @@ Comprehensive real-world examples demonstrating masterror integration with popul | [**custom-domain-errors**](examples/custom-domain-errors/) | Payment processing domain errors | Derive macro, error conversion, structured errors | | [**basic-async**](examples/basic-async/) | Async error handling with tokio | Error propagation, timeout handling, Result types | -All examples are runnable and include comprehensive tests. See the [`examples/`](examples/) directory for complete source code and documentation. +All examples are runnable; the axum-rest-api example additionally ships integration tests. See the [`examples/`](examples/) directory for complete source code and documentation.
diff --git a/README.template.md b/README.template.md index 381a842..b1ec72b 100644 --- a/README.template.md +++ b/README.template.md @@ -74,9 +74,12 @@ of redaction and metadata. - **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. +- **Native derives.** `#[derive(Error)]` and `#[derive(Masterror)]` wire custom + types into the runtime types. `#[derive(Masterror)]` with `#[masterror(...)]` + forwards sources, backtraces, telemetry fields and redaction policy; + `#[app_error]` maps a derived error to an `AppErrorKind`/`AppCode` (optionally + exposing its `Display` message) and `#[provide]` registers typed telemetry + providers on the domain error itself. - **Typed telemetry.** `Metadata` stores structured key/value context (strings, integers, floats, durations, IP addresses and optional JSON) with per-field redaction controls and builders in `field::*`, so logs stay structured without @@ -86,11 +89,11 @@ of redaction and metadata. 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. + Telegram init-data validation 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. + a heuristic classifier and conservative mappings into the canonical taxonomy + for teams that want a consistent baseline out of the box. - **Typed control-flow macros.** `ensure!` and `fail!` short-circuit functions with your domain errors without allocating or formatting on the happy path. @@ -128,14 +131,16 @@ of redaction and metadata. ## Feature Flags -Pick only what you need; everything is off by default. +Pick only what you need; the default feature set is just `std`, everything +else is opt-in. - **Web transports:** `axum`, `actix`, `multipart`, `openapi`, `serde_json`. - **Telemetry & observability:** `tracing`, `metrics`, `backtrace`, `colored` for colored terminal output. - **Async & IO integrations:** `tokio`, `reqwest`, `sqlx`, `sqlx-migrate`, `redis`, `validator`, `config`. -- **Messaging & bots:** `teloxide`, `telegram-webapp-sdk`. +- **Messaging & bots:** `teloxide`, `init-data` for Telegram Mini App + init-data validation via `init-data-rs`. - **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. @@ -190,7 +195,7 @@ cargo bench -F benchmarks --bench error_paths The suite emits two groups: - `context_into_error/*` promotes a dummy source error with representative - metadata (strings, counters, durations, IPs) through `Context::into_error` in + metadata (strings, counters, durations, IPs) through `ResultExt::ctx` in both redacted and non-redacted modes. - `problem_json_from_app_error/*` consumes the resulting `AppError` values to build RFC 7807 payloads via `ProblemJson::from_app_error`, showing how message @@ -468,9 +473,17 @@ generated [`masterror::Error`]. `#[provide(...)]` exposes typed context through `std::error::Request`, while `#[app_error(...)]` records how your domain error translates into `AppError` -and `AppCode`. The derive mirrors `thiserror`'s syntax and extends it with -optional telemetry propagation and direct conversions into the `masterror` -runtime types. +and `AppCode`. The derive mirrors `thiserror`'s syntax. Note that the +generated `From` conversions produce an `AppError` carrying only the mapped +kind and code (plus the `Display` output as public message when the `message` +flag is set) — the original domain error is dropped, so its sources and +telemetry providers are not forwarded. Request telemetry from the domain +error before converting. + +`request_ref`/`request_value` and the `std::error::Request` machinery require +a nightly toolchain (`error_generic_member_access`); the crate detects +compiler support at build time and only enables provider integration when +available. ~~~rust use std::error::request_ref; @@ -502,8 +515,7 @@ let snapshot = request_ref::(&err).expect("telemetry"); assert_eq!(snapshot.value, 42); let app: AppError = err.into(); -let via_app = request_ref::(&app).expect("telemetry"); -assert_eq!(via_app.name, "db.query"); +assert!(matches!(app.kind, AppErrorKind::Service)); ~~~ Optional telemetry only surfaces when present, so `None` does not register a @@ -574,11 +586,10 @@ structured telemetry (`#[provide]`) and first-class conversions into ~~~rust use masterror::{AppError, AppErrorKind, ProblemJson}; -use std::time::Duration; let problem = ProblemJson::from_app_error( AppError::new(AppErrorKind::Unauthorized, "Token expired") - .with_retry_after_duration(Duration::from_secs(30)) + .with_retry_after_secs(30) .with_www_authenticate(r#"Bearer realm="api", error="invalid_token""#) ); @@ -590,36 +601,19 @@ assert_eq!(problem.grpc.expect("grpc").name, "UNAUTHENTICATED");
- Environment-aware error formatting with DisplayMode - -The `DisplayMode` API lets you control error output formatting based on deployment -environment without changing your error handling code. Three modes are available: - -- **`DisplayMode::Prod`** — Lightweight JSON output with minimal fields, optimized - for production logs. Only includes `kind`, `code`, and `message` (if not redacted). - Filters sensitive metadata automatically. - -- **`DisplayMode::Local`** — Human-readable multi-line output with full context. - Shows error details, complete source chain, all metadata, and backtrace (if enabled). - Best for local development and debugging. + Environment detection with DisplayMode -- **`DisplayMode::Staging`** — JSON output with additional context. Includes - `kind`, `code`, `message`, limited `source_chain`, and filtered metadata. - Useful for staging environments where you need structured logs with more detail. +`DisplayMode` detects the deployment environment (`Prod`, `Local` or +`Staging`) so your code can branch on it. `DisplayMode::current()` resolves +the mode in this order and caches the result on first access: -**Automatic Environment Detection:** - -The mode is auto-detected in this order: 1. `MASTERROR_ENV` environment variable (`prod`, `local`, or `staging`) -2. `KUBERNETES_SERVICE_HOST` presence (triggers `Prod` mode) +2. `KUBERNETES_SERVICE_HOST` presence (selects `Prod`) 3. Build configuration (`debug_assertions` → `Local`, release → `Prod`) -The result is cached on first access for zero-cost subsequent calls. - ~~~rust use masterror::DisplayMode; -// Query the current mode (cached after first call) let mode = DisplayMode::current(); match mode { @@ -629,41 +623,26 @@ match mode { } ~~~ +Note: `Display` for `AppError` does not consult `DisplayMode` yet — the +output is identical in all modes. The only formatting branch today is the +`colored` feature described below; mode-aware formatting is not wired up. + **Colored Terminal Output:** -Enable the `colored` feature for enhanced terminal output in local mode: +Enable the `colored` feature for enhanced terminal output. It applies +whenever the feature is enabled, regardless of the detected mode: ~~~toml [dependencies] masterror = { version = "{{CRATE_VERSION}}", features = ["colored"] } ~~~ -With `colored` enabled, errors display with syntax highlighting: -- Error kind and code in bold -- Error messages in color -- Source chain with indentation -- Metadata keys highlighted - -~~~rust -use masterror::{AppError, field}; - -let error = AppError::not_found("User not found") - .with_field(field::str("user_id", "12345")) - .with_field(field::str("request_id", "abc-def")); - -// Without 'colored': plain text -// With 'colored': color-coded output in terminals -println!("{}", error); -~~~ - -**Production vs Development Output:** - -Without `colored` feature, errors display their `AppErrorKind` label: +Without the `colored` feature, errors display their `AppErrorKind` label: ~~~ NotFound ~~~ -With `colored` feature, full multi-line format with context: +With `colored`, a full multi-line format with context: ~~~ Error: NotFound Code: NOT_FOUND @@ -674,9 +653,6 @@ Context: request_id: abc-def ~~~ -This separation keeps production logs clean while giving developers rich context -during local debugging sessions. -
@@ -702,7 +678,7 @@ Comprehensive real-world examples demonstrating masterror integration with popul | [**custom-domain-errors**](examples/custom-domain-errors/) | Payment processing domain errors | Derive macro, error conversion, structured errors | | [**basic-async**](examples/basic-async/) | Async error handling with tokio | Error propagation, timeout handling, Result types | -All examples are runnable and include comprehensive tests. See the [`examples/`](examples/) directory for complete source code and documentation. +All examples are runnable; the axum-rest-api example additionally ships integration tests. See the [`examples/`](examples/) directory for complete source code and documentation.
diff --git a/src/app_error.rs b/src/app_error.rs index fffd18a..4b74b60 100644 --- a/src/app_error.rs +++ b/src/app_error.rs @@ -6,8 +6,10 @@ //! //! [`AppError`] is a thin, framework-agnostic wrapper around a canonical //! error taxonomy [`AppErrorKind`] plus an optional public-facing message. -//! The `Display` for `AppError` prints only the kind, not the message, to keep -//! logs and errors concise by default. +//! Without the `colored` feature, the `Display` for `AppError` prints only +//! the kind, not the message, to keep logs and errors concise by default. +//! With `colored` enabled, `Display` renders a multi-line report (kind, code, +//! message, source chain, metadata). //! //! ## Design //! @@ -17,7 +19,9 @@ //! secrets here. //! - **Structured metadata:** attach typed key/value pairs for diagnostics via //! [`Metadata`]. -//! - **No panics:** all helpers avoid `unwrap/expect`. +//! - **No panics on user input:** helpers never panic on caller-provided data; +//! the only internal `expect` relies on the invariant that an error chain +//! always contains at least one element. //! - **Transport-agnostic:** mapping to HTTP lives in `kind.rs` and //! `convert/*`. //! diff --git a/src/app_error/core.rs b/src/app_error/core.rs index 2aef940..83c02ed 100644 --- a/src/app_error/core.rs +++ b/src/app_error/core.rs @@ -52,12 +52,13 @@ pub mod telemetry; /// - `CapturedBacktrace` type alias pub mod types; -/// Display formatting and environment-based output modes. +/// Environment detection and alternative formatting helpers. /// -/// Provides environment-aware error formatting with three modes: -/// - Production: lightweight JSON, minimal fields -/// - Local: human-readable with full context -/// - Staging: JSON with additional context +/// Provides [`DisplayMode`], which detects the deployment environment from +/// `MASTERROR_ENV`, `KUBERNETES_SERVICE_HOST` or build configuration and +/// caches the result. The `Display` implementation for `Error` does not +/// consult this mode; it is a detection API for callers to choose their own +/// formatting. pub mod display; #[cfg(all(test, feature = "backtrace"))] diff --git a/src/app_error/core/backtrace.rs b/src/app_error/core/backtrace.rs index 10bc4bf..ede4933 100644 --- a/src/app_error/core/backtrace.rs +++ b/src/app_error/core/backtrace.rs @@ -91,18 +91,8 @@ fn detect_backtrace_preference() -> bool { /// /// This function is only available in test builds with the `backtrace` feature. /// It clears both the global state and test override, forcing the next -/// backtrace capture to re-read configuration. -/// -/// # Examples -/// -/// ```rust -/// # #[cfg(all(test, feature = "backtrace"))] -/// # { -/// use masterror::app_error::core::backtrace::reset_backtrace_preference; -/// -/// reset_backtrace_preference(); -/// # } -/// ``` +/// backtrace capture to re-read configuration. Tests call it directly before +/// and after overriding the preference. #[cfg(all(test, feature = "backtrace"))] pub fn reset_backtrace_preference() { BACKTRACE_STATE.store(BACKTRACE_STATE_UNSET, AtomicOrdering::Release); @@ -119,17 +109,6 @@ pub fn reset_backtrace_preference() { /// /// * `value` - `Some(true)` to force enable, `Some(false)` to force disable, /// `None` to clear override -/// -/// # Examples -/// -/// ```rust -/// # #[cfg(all(test, feature = "backtrace"))] -/// # { -/// use masterror::app_error::core::backtrace::set_backtrace_preference_override; -/// -/// set_backtrace_preference_override(Some(true)); -/// # } -/// ``` #[cfg(all(test, feature = "backtrace"))] pub fn set_backtrace_preference_override(value: Option) { test_backtrace_override::set(value); diff --git a/src/app_error/core/display.rs b/src/app_error/core/display.rs index cc92573..3943c2d 100644 --- a/src/app_error/core/display.rs +++ b/src/app_error/core/display.rs @@ -12,12 +12,15 @@ use core::{ use super::error::Error; use crate::{FieldRedaction, FieldValue, MessageEditPolicy}; -/// Display mode for error output. +/// Detected deployment environment. /// -/// Controls the structure and verbosity of error messages based on -/// the deployment environment. The mode is determined by the -/// `MASTERROR_ENV` environment variable or auto-detected based on -/// build configuration and runtime environment. +/// [`DisplayMode::current`] identifies the environment the process runs in, +/// based on the `MASTERROR_ENV` environment variable, Kubernetes detection +/// (`KUBERNETES_SERVICE_HOST`) or build configuration, and caches the result. +/// +/// Note: the `Display` implementation for [`struct@crate::Error`] does not +/// currently switch its output based on this mode; the mode is a detection +/// API that callers can consult to choose their own formatting. /// /// # Examples /// @@ -26,62 +29,33 @@ use crate::{FieldRedaction, FieldValue, MessageEditPolicy}; /// /// let mode = DisplayMode::current(); /// match mode { -/// DisplayMode::Prod => println!("Production mode: JSON output"), -/// DisplayMode::Local => println!("Local mode: Human-readable output"), -/// DisplayMode::Staging => println!("Staging mode: JSON with context") +/// DisplayMode::Prod => println!("Production environment"), +/// DisplayMode::Local => println!("Local development environment"), +/// DisplayMode::Staging => println!("Staging environment") /// } /// ``` #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum DisplayMode { - /// Production mode: lightweight JSON, minimal fields, no sensitive data. - /// - /// Output includes only: `kind`, `code`, `message` (if not redacted). - /// Metadata is filtered to exclude sensitive fields. - /// Source chain and backtrace are excluded. - /// - /// # Example Output + /// Production environment. /// - /// ```json - /// {"kind":"NotFound","code":"NOT_FOUND","message":"User not found"} - /// ``` + /// Selected when `MASTERROR_ENV` is `prod`/`production`, when + /// `KUBERNETES_SERVICE_HOST` is set, or for release builds by default. Prod = 0, - /// Development mode: human-readable, full context. - /// - /// Output includes: error details, full source chain, complete metadata, - /// and backtrace (if enabled). Supports colored output when the `colored` - /// feature is enabled and output is a TTY. + /// Local development environment. /// - /// # Example Output - /// - /// ```text - /// Error: NotFound - /// Code: NOT_FOUND - /// Message: User not found - /// - /// Caused by: database query failed - /// Caused by: connection timeout - /// - /// Context: - /// user_id: 12345 - /// ``` + /// Selected when `MASTERROR_ENV` is `local`/`dev`/`development`, or for + /// debug builds by default. Local = 1, - /// Staging mode: JSON with additional context. + /// Staging environment. /// - /// Output includes: `kind`, `code`, `message`, limited `source_chain`, - /// and filtered metadata. No backtrace. - /// - /// # Example Output - /// - /// ```json - /// {"kind":"NotFound","code":"NOT_FOUND","message":"User not found","source_chain":["database error"],"metadata":{"user_id":12345}} - /// ``` + /// Selected when `MASTERROR_ENV` is `staging`/`stage`. Staging = 2 } impl DisplayMode { - /// Returns the current display mode based on environment configuration. + /// Returns the detected environment based on configuration. /// /// The mode is determined by checking (in order): /// 1. `MASTERROR_ENV` environment variable (`prod`, `local`, or `staging`) @@ -152,21 +126,15 @@ impl DisplayMode { #[allow(dead_code)] impl Error { - /// Formats error in production mode (compact JSON). + /// Formats the error as compact JSON (`kind`, `code`, optional `message`, + /// public metadata). + /// + /// Not wired into the `Display` implementation; callers that want this + /// layout must invoke it explicitly. /// /// # Arguments /// /// * `f` - Formatter to write output to - /// - /// # Examples - /// - /// ``` - /// use masterror::AppError; - /// - /// let error = AppError::not_found("User not found"); - /// let output = format!("{}", error); - /// // In prod mode: {"kind":"NotFound","code":"NOT_FOUND","message":"User not found"} - /// ``` #[cfg(not(test))] pub(crate) fn fmt_prod(&self, f: &mut Formatter<'_>) -> FmtResult { self.fmt_prod_impl(f) @@ -212,21 +180,15 @@ impl Error { write!(f, "}}") } - /// Formats error in local/development mode (human-readable). + /// Formats the error as a multi-line human-readable report (kind, code, + /// message, source chain, metadata). + /// + /// Not wired into the `Display` implementation; callers that want this + /// layout must invoke it explicitly. /// /// # Arguments /// /// * `f` - Formatter to write output to - /// - /// # Examples - /// - /// ``` - /// use masterror::AppError; - /// - /// let error = AppError::internal("Database error"); - /// let output = format!("{}", error); - /// // In local mode: multi-line human-readable format with full context - /// ``` #[cfg(not(test))] pub(crate) fn fmt_local(&self, f: &mut Formatter<'_>) -> FmtResult { self.fmt_local_impl(f) @@ -307,21 +269,15 @@ impl Error { } } - /// Formats error in staging mode (JSON with context). + /// Formats the error as JSON with additional context (`source_chain` and + /// public metadata). + /// + /// Not wired into the `Display` implementation; callers that want this + /// layout must invoke it explicitly. /// /// # Arguments /// /// * `f` - Formatter to write output to - /// - /// # Examples - /// - /// ``` - /// use masterror::AppError; - /// - /// let error = AppError::service("Service unavailable"); - /// let output = format!("{}", error); - /// // In staging mode: JSON with source_chain and metadata - /// ``` #[cfg(not(test))] pub(crate) fn fmt_staging(&self, f: &mut Formatter<'_>) -> FmtResult { self.fmt_staging_impl(f) diff --git a/src/app_error/core/introspection.rs b/src/app_error/core/introspection.rs index f27c6d8..da6ccec 100644 --- a/src/app_error/core/introspection.rs +++ b/src/app_error/core/introspection.rs @@ -185,10 +185,10 @@ impl Error { .expect("chain always has at least one error") } - /// Attempts to downcast the error source to a concrete type. + /// Check whether the attached source error is of a concrete type. /// - /// Returns `true` if the error source is of type `E`, `false` otherwise. - /// This only checks the immediate source, not the entire chain. + /// Returns `true` if the immediate source (not this error itself, and not + /// the entire chain) is of type `E`, `false` otherwise. /// /// # Examples /// @@ -215,7 +215,7 @@ impl Error { self.source_ref().is_some_and(|source| source.is::()) } - /// Attempt to downcast the error source to a concrete type by value. + /// Attempt to take ownership of the source error as a concrete type. /// /// **Note:** This method is currently a stub and always returns /// `Err(Self)`. @@ -243,10 +243,11 @@ impl Error { Err(self) } - /// Attempt to downcast the error to a concrete type by immutable - /// reference. + /// Attempt to downcast the attached source error to a concrete type by + /// immutable reference. /// - /// Returns `Some(&E)` if this error is of type `E`, `None` otherwise. + /// Returns `Some(&E)` if the immediate source is of type `E`, `None` + /// otherwise (including when no source is attached). /// /// # Examples /// @@ -272,9 +273,12 @@ impl Error { self.source_ref()?.downcast_ref::() } - /// Attempt to downcast the error to a concrete type by mutable reference. + /// Attempt to downcast the attached source error to a concrete type by + /// mutable reference. /// - /// Returns `Some(&mut E)` if this error is of type `E`, `None` otherwise. + /// **Note:** This method is currently a stub and always returns `None`. + /// + /// Use [`downcast_ref`](Self::downcast_ref) for inspecting error sources. /// /// # Examples /// @@ -287,9 +291,7 @@ impl Error { /// let io_err = IoError::other("disk offline"); /// let mut err = AppError::internal("boom").with_context(io_err); /// - /// if let Some(_io) = err.downcast_mut::() { - /// // Can modify the IoError if needed - /// } + /// assert!(err.downcast_mut::().is_none()); /// # } /// ``` #[must_use] diff --git a/src/code/app_code.rs b/src/code/app_code.rs index d46b585..be89529 100644 --- a/src/code/app_code.rs +++ b/src/code/app_code.rs @@ -149,8 +149,9 @@ impl AppCode { /// /// # Errors /// - /// Returns [`ParseAppCodeError`] when the string is empty or contains - /// characters outside of `A-Z`, `0-9`, and `_`. + /// Returns [`ParseAppCodeError`] when the string is empty, contains + /// characters outside of `A-Z`, `0-9`, and `_`, or has leading, trailing + /// or consecutive underscores. /// /// # Examples /// ``` diff --git a/src/convert.rs b/src/convert.rs index 50de21f..d780602 100644 --- a/src/convert.rs +++ b/src/convert.rs @@ -9,10 +9,11 @@ //! the crate’s [`AppError`]. It also conditionally enables transport adapters //! (Axum/Actix) to turn [`AppError`] into HTTP responses. //! -//! ## Always-on mappings +//! ## Base mappings //! -//! - [`std::io::Error`] → `AppErrorKind::Internal` Infrastructure-level -//! failure; message preserved for logs only. +//! - [`std::io::Error`] → `AppErrorKind::Internal` (requires the default `std` +//! feature) Infrastructure-level failure; the error text becomes the public +//! message. //! - [`String`] → `AppErrorKind::BadRequest` Lightweight validation helper //! when you don’t pull in `validator`. //! @@ -24,12 +25,15 @@ //! - `axum` + `multipart`: Axum multipart parsing errors //! - `actix`: Actix `ResponseError` integration (not a mapping, but transport) //! - `config`: configuration loader errors +//! - `init-data`: Telegram Mini Apps init-data validation errors //! - `redis`: Redis client errors //! - `reqwest`: HTTP client errors -//! - `serde_json`: JSON conversion helpers (if you attach JSON details -//! manually) -//! - `sqlx`: database driver errors +//! - `serde_json`: `serde_json::Error` classification into +//! `Serialization`/`Deserialization` +//! - `sqlx`: database driver errors (`sqlx-core`) +//! - `sqlx-migrate`: `sqlx::migrate::MigrateError` mapping //! - `tokio`: timeouts from `tokio::time::error::Elapsed` +//! - `tonic`: conversions between [`struct@crate::Error`] and `tonic::Status` //! - `teloxide`: Telegram request errors //! - `validator`: input DTO validation errors //! @@ -144,9 +148,10 @@ pub use self::tonic::StatusConversionError; /// Map `std::io::Error` to an internal application error. /// -/// Rationale: I/O failures are infrastructure-level and should not leak -/// driver-specific details to clients. The message is preserved for -/// observability, but the public-facing kind is always `Internal`. +/// Rationale: I/O failures are infrastructure-level, so the public-facing +/// kind is always `Internal`. The error text becomes the public message, so +/// avoid embedding sensitive details in I/O error messages that cross this +/// boundary. /// /// ```rust /// use std::io::{self, ErrorKind}; diff --git a/src/convert/actix.rs b/src/convert/actix.rs index 677080f..28d28c1 100644 --- a/src/convert/actix.rs +++ b/src/convert/actix.rs @@ -10,27 +10,24 @@ //! - Implements `actix_web::ResponseError` for [`AppError`]. //! - This lets you `return AppResult<_>` from Actix handlers. //! - On error, Actix automatically builds an `HttpResponse` with the right -//! status code and RFC7807 JSON body (when the `serde_json` feature is -//! enabled). +//! status code and an RFC7807 JSON body. //! - Provides stable mapping from [`AppErrorKind`] to //! `actix_web::http::StatusCode`. //! - Ensures that only safe, public-facing fields are returned to the client -//! (`type`, `title`, `status`, `detail?`, `metadata?`). +//! (`type`, `title`, `status`, `code`, `detail?`, `metadata?`, `grpc?`). //! //! ## Wire payload //! -//! When the `serde_json` feature is enabled, the body is [`ProblemJson`] with: +//! The body is always [`ProblemJson`] with: //! - `type`: canonical URI describing the problem class //! - `title`: short summary derived from [`AppErrorKind`] //! - `status`: numeric HTTP status (e.g. 404, 422, 500) +//! - `code`: stable machine-readable [`AppCode`](crate::AppCode) //! - `detail?`: public message (redacted when the error is private) //! - `metadata?`: sanitized structured fields carried from //! [`Metadata`](crate::Metadata) //! - `grpc?`: optional gRPC mapping for multi-protocol clients //! -//! Without `serde_json`, Actix still returns a response with the correct status -//! but with an empty body. -//! //! ## Example //! //! ```rust,ignore @@ -70,10 +67,9 @@ //! - Do not duplicate this `ResponseError` implementation elsewhere. //! - Internal error sources (`std::error::Error` chain) are logged only; they //! are never leaked to the HTTP response. -//! - You typically want both `actix` and `serde_json` features enabled for -//! proper JSON payloads. //! -//! See also: Axum integration in [`convert::axum`]. +//! See also: the Axum integration provided by the `axum` feature +//! (`convert/axum.rs`). #[cfg(feature = "actix")] use actix_web::{HttpResponse, ResponseError, http::StatusCode as ActixStatus}; diff --git a/src/convert/init_data.rs b/src/convert/init_data.rs index a7db318..51bb15b 100644 --- a/src/convert/init_data.rs +++ b/src/convert/init_data.rs @@ -8,8 +8,10 @@ //! //! ## Mapping //! -//! All [`InitDataError`] variants are mapped to `AppErrorKind::TelegramAuth` -//! and the original error text is preserved in the message. +//! All [`InitDataError`] variants are mapped to `AppErrorKind::TelegramAuth`. +//! The public message is left unset; the failure reason and details are +//! captured as `telegram_init_data.*` metadata fields and the original error +//! is retained in the source chain. //! //! ## Rationale //! @@ -20,10 +22,9 @@ //! //! ## Example //! -//! '''rust +//! ```rust //! # #[cfg(feature = "init-data")] //! # { -//! '''rust,ignore //! use init_data_rs::InitDataError; //! use masterror::{AppErrorKind, Error}; //! @@ -34,7 +35,7 @@ //! let e = convert(InitDataError::HashMissing); //! assert!(matches!(e.kind, AppErrorKind::TelegramAuth)); //! # } -//! ''' +//! ``` #[cfg(feature = "init-data")] use init_data_rs::InitDataError; diff --git a/src/convert/multipart.rs b/src/convert/multipart.rs index 251e668..d8f243e 100644 --- a/src/convert/multipart.rs +++ b/src/convert/multipart.rs @@ -3,7 +3,10 @@ // SPDX-License-Identifier: MIT //! Maps [`MultipartError`] into [`Error`] with -//! [`AppErrorKind::BadRequest`], preserving the original message. +//! [`AppErrorKind::BadRequest`]. The public message is left unset; the +//! multipart body text and HTTP status are captured as metadata fields +//! (`multipart.reason`, `http.*`) and the original error is retained in the +//! source chain. //! //! Intended for Axum multipart form parsing so that client mistakes are //! surfaced as bad requests. diff --git a/src/convert/redis.rs b/src/convert/redis.rs index bce2e3c..b771293 100644 --- a/src/convert/redis.rs +++ b/src/convert/redis.rs @@ -21,19 +21,19 @@ //! //! ## Example //! -//! ```rust,ignore +//! An IO-level failure is promoted from `Cache` to `DependencyUnavailable`: +//! +//! ```rust +//! # #[cfg(feature = "redis")] +//! # { //! use masterror::{AppErrorKind, Error}; //! use redis::RedisError; //! -//! fn handle_cache_error(e: RedisError) -> Error { -//! e.into() -//! } -//! -//! // In production code, this would come from a Redis client operation -//! let dummy = RedisError::from((redis::ErrorKind::Io, "connection lost")); -//! let app_err = handle_cache_error(dummy); +//! let err = RedisError::from((redis::ErrorKind::Io, "connection lost")); +//! let app_err: Error = err.into(); //! -//! assert!(matches!(app_err.kind, AppErrorKind::Cache)); +//! assert!(matches!(app_err.kind, AppErrorKind::DependencyUnavailable)); +//! # } //! ``` #[cfg(feature = "redis")] @@ -42,10 +42,14 @@ use redis::{ErrorKind, RedisError, RetryMethod, ServerErrorKind}; #[cfg(feature = "redis")] use crate::{AppErrorKind, Context, Error, field}; -/// Map any [`redis::RedisError`] into an [`crate::AppError`] with kind `Cache`. +/// Map a [`redis::RedisError`] into an [`crate::AppError`]. /// -/// Rationale: Redis is treated as a backend cache dependency. -/// Detailed driver errors are kept in the message for diagnostics. +/// Rationale: Redis is treated as a backend cache dependency, so the default +/// kind is `Cache`. Timeouts are promoted to `Timeout`; IO, connection, +/// cluster and busy-loading failures are promoted to `DependencyUnavailable`. +/// The public message is left unset; driver details are captured as structured +/// metadata fields (`redis.*`) and the original error is retained in the +/// source chain for diagnostics. #[cfg(feature = "redis")] #[cfg_attr(docsrs, doc(cfg(feature = "redis")))] impl From for Error { diff --git a/src/convert/reqwest.rs b/src/convert/reqwest.rs index 7b73c31..2f433a5 100644 --- a/src/convert/reqwest.rs +++ b/src/convert/reqwest.rs @@ -62,7 +62,8 @@ use crate::{AppErrorKind, Context, Error, FieldRedaction, field}; /// /// - Timeout → `Timeout` /// - Connect or request build error → `Network` -/// - Upstream returned HTTP error status → `ExternalApi` +/// - Upstream HTTP error status: `429` → `RateLimited`, `408` → `Timeout`, +/// `5xx` → `DependencyUnavailable`, others → `ExternalApi` /// - Fallback for other cases → `ExternalApi` /// /// # Example diff --git a/src/convert/serde_json.rs b/src/convert/serde_json.rs index c7a0ed6..6531cc8 100644 --- a/src/convert/serde_json.rs +++ b/src/convert/serde_json.rs @@ -10,8 +10,9 @@ //! //! Errors are classified using [`serde_json::Error::classify`]. I/O failures //! are mapped to [`AppErrorKind::Serialization`]; syntax, data and EOF errors -//! map to [`AppErrorKind::Deserialization`]. The original error string is -//! preserved in `message` for observability. +//! map to [`AppErrorKind::Deserialization`]. The public message is left unset; +//! category and position details are captured as `serde_json.*` metadata +//! fields and the original error is retained in the source chain. //! //! ## Rationale //! @@ -46,8 +47,8 @@ use crate::{AppErrorKind, Context, Error, field}; /// Map a [`serde_json::Error`] into an [`crate::AppError`]. /// /// Errors are classified to `Serialization` or `Deserialization` using -/// [`serde_json::Error::classify`]. The original error string is preserved for -/// logs and optional JSON payloads. +/// [`serde_json::Error::classify`]. Category and position details go into +/// metadata; the original error stays available via the source chain. #[cfg(feature = "serde_json")] #[cfg_attr(docsrs, doc(cfg(feature = "serde_json")))] impl From for Error { diff --git a/src/convert/teloxide.rs b/src/convert/teloxide.rs index 291aef1..a8cdf1b 100644 --- a/src/convert/teloxide.rs +++ b/src/convert/teloxide.rs @@ -16,7 +16,9 @@ //! - [`RequestError::InvalidJson`] → `AppErrorKind::Deserialization` //! - [`RequestError::Io`] → `AppErrorKind::Internal` //! -//! The original error string is preserved in the `message` for observability. +//! The public message is left unset; error details are captured as +//! `telegram.*` metadata fields and the original error is retained in the +//! source chain for observability. //! //! ## Example //! diff --git a/src/convert/tokio.rs b/src/convert/tokio.rs index 03713cb..865b76c 100644 --- a/src/convert/tokio.rs +++ b/src/convert/tokio.rs @@ -8,8 +8,10 @@ //! //! ## Mapping //! -//! All elapsed-time errors are mapped to `AppErrorKind::Timeout` with a fixed, -//! public-facing message `"Operation timed out"`. +//! All elapsed-time errors are mapped to `AppErrorKind::Timeout`. The +//! conversion sets no public message, so clients see the kind's fallback +//! title; a `timeout.source` metadata field records the timeout origin and +//! the original error is retained in the source chain. //! //! ## Rationale //! @@ -45,7 +47,8 @@ use crate::{AppErrorKind, Context, Error, field}; /// Map a [`tokio::time::error::Elapsed`] into an [`crate::AppError`] with kind /// `Timeout`. /// -/// Message is fixed to avoid leaking timing specifics to the client. +/// No public message is set, which avoids leaking timing specifics to the +/// client; a `timeout.source` metadata field is attached instead. /// /// # Example /// diff --git a/src/lib.rs b/src/lib.rs index e086186..ec0a9f2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -46,7 +46,7 @@ //! //! # Minimum Supported Rust Version (MSRV) //! -//! MSRV is **1.90**. New minor releases may increase MSRV with a changelog +//! MSRV is **1.96**. New minor releases may increase MSRV with a changelog //! note, but never in a patch release. //! //! # Feature flags @@ -60,21 +60,29 @@ //! - `tonic` — converts [`struct@Error`] into `tonic::Status` with sanitized //! metadata //! - `openapi` — derives an OpenAPI schema for [`ErrorResponse`] (via `utoipa`) -//! - `sqlx` — `From` mapping +//! - `sqlx` — `From` mapping (pulls only `sqlx-core`) +//! - `sqlx-migrate` — `From` mapping (pulls full +//! `sqlx`) //! - `redis` — `From` mapping //! - `validator` — `From` mapping //! - `config` — `From` mapping //! - `tokio` — `From` mapping //! - `reqwest` — `From` mapping //! - `teloxide` — `From` mapping -//! - `telegram-webapp-sdk` — -//! `From` -//! mapping +//! - `init-data` — `From` mapping for Telegram +//! Mini Apps init-data validation //! - `frontend` — convert errors into `wasm_bindgen::JsValue` and emit //! `console.error` logs in WASM/browser contexts //! - `serde_json` — support for structured JSON details in [`ErrorResponse`] -//! and [`ProblemJson`]; also pulled transitively by `axum` -//! - `multipart` — compatibility flag for Axum multipart +//! and [`ProblemJson`] plus a `From` mapping. Note: +//! `axum`/`actix` enable the `serde_json` *dependency* for their own bodies, +//! but not this feature — enable it explicitly to get the JSON-gated APIs +//! - `multipart` — `From` mapping to +//! `BadRequest` (implies `axum`) +//! - `colored` — colored, multi-line `Display` output for [`struct@Error`] +//! - `tracing` — emit structured `tracing` events when errors are constructed +//! - `metrics` — increment an `error_total{code,category}` counter per error +//! - `backtrace` — capture backtraces (controlled by `RUST_BACKTRACE`) //! - `turnkey` — domain taxonomy and conversions for Turnkey errors, exposed in //! the `turnkey` module //! diff --git a/src/response.rs b/src/response.rs index 86979a5..4308bdb 100644 --- a/src/response.rs +++ b/src/response.rs @@ -12,7 +12,7 @@ //! //! - canonical problem `type` URIs derived from [`AppCode`] //! - a `title` computed from [`AppErrorKind`] -//! - the stable machine code plus optional gRPC mapping (`grpc.code`, +//! - the stable machine code plus optional gRPC mapping (`grpc.name`, //! `grpc.value`) //! - retry/authentication hints surfaced via the `Retry-After` and //! `WWW-Authenticate` headers diff --git a/src/response/actix_impl.rs b/src/response/actix_impl.rs index 8fc6f8d..303bdbd 100644 --- a/src/response/actix_impl.rs +++ b/src/response/actix_impl.rs @@ -9,7 +9,10 @@ //! - Serializes the response as RFC7807 `application/problem+json`. //! - Adds `Retry-After` when retry advice is present. //! - Adds `WWW-Authenticate` when an authentication challenge is provided. -//! - Redacts message and metadata when the error is marked private. +//! - Redaction itself happens earlier, in +//! [`ProblemJson::from_app_error`](crate::ProblemJson::from_app_error), which +//! applies the error's message and metadata redaction policies before the +//! payload reaches this adapter. use actix_web::{ HttpRequest, HttpResponse, Responder, diff --git a/src/response/axum_impl.rs b/src/response/axum_impl.rs index b939c22..b3cbba3 100644 --- a/src/response/axum_impl.rs +++ b/src/response/axum_impl.rs @@ -10,7 +10,10 @@ //! status. //! - Adds `Retry-After` if retry advice is present. //! - Adds `WWW-Authenticate` if an authentication challenge is present. -//! - Redacts the message and metadata when the error is marked as private. +//! - Redaction itself happens earlier, in +//! [`ProblemJson::from_app_error`](crate::ProblemJson::from_app_error), which +//! applies the error's message and metadata redaction policies before the +//! payload reaches this adapter. use axum::{ Json, diff --git a/src/result_ext.rs b/src/result_ext.rs index afe9b65..ccc6777 100644 --- a/src/result_ext.rs +++ b/src/result_ext.rs @@ -40,8 +40,12 @@ pub trait ResultExt { /// Wrap the error with a simple context message. /// - /// This is a convenience method equivalent to anyhow's `.context()`. - /// For more control, use [`ctx`](ResultExt::ctx). + /// This is a convenience method in the spirit of anyhow's `.context()`, + /// with one difference: when the source is not already a masterror + /// [`Error`], the result is classified as `Internal` (whereas anyhow + /// keeps errors unclassified). If the source is already an [`Error`], + /// its kind, code and metadata are preserved and only the message is + /// replaced. For control over the category, use [`ctx`](ResultExt::ctx). /// /// # Examples /// From 7f7cbd56ca6e1df9648f888b9ab1c186d476f868 Mon Sep 17 00:00:00 2001 From: RAprogramm Date: Sun, 5 Jul 2026 13:01:45 +0700 Subject: [PATCH 2/2] #449 docs: sync Russian and Korean READMEs with English --- README.ko.md | 129 +++++++++++++++++++++++------------------------- README.ru.md | 135 +++++++++++++++++++++++++-------------------------- 2 files changed, 128 insertions(+), 136 deletions(-) diff --git a/README.ko.md b/README.ko.md index 0fe7d7d..85824be 100644 --- a/README.ko.md +++ b/README.ko.md @@ -14,12 +14,13 @@ SPDX-License-Identifier: MIT [![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) [![Downloads](https://img.shields.io/crates/d/masterror)](https://crates.io/crates/masterror) - ![MSRV](https://img.shields.io/badge/MSRV-1.90-blue) - ![License](https://img.shields.io/badge/License-MIT%20or%20Apache--2.0-informational) + ![MSRV](https://img.shields.io/badge/MSRV-1.96-blue) + ![License](https://img.shields.io/badge/License-MIT-informational) + [![REUSE status](https://api.reuse.software/badge/github.com/RAprogramm/masterror)](https://api.reuse.software/info/github.com/RAprogramm/masterror) [![codecov](https://codecov.io/gh/RAprogramm/masterror/graph/badge.svg?token=V9JQDTZLXH)](https://codecov.io/gh/RAprogramm/masterror) [![CI](https://github.com/RAprogramm/masterror/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/RAprogramm/masterror/actions/workflows/ci.yml?query=branch%3Amain) - [![Hits-of-Code](https://hitsofcode.com/github/RAprogramm/masterror?branch=main)](https://hitsofcode.com/github/RAprogramm/masterror/view?branch=main) + [![Hits-of-Code](https://hitsofcode.com/github/RAprogramm/masterror?branch=main&exclude=Cargo.lock,.gitignore,CHANGELOG.md)](https://hitsofcode.com/github/RAprogramm/masterror/view?branch=main&exclude=Cargo.lock,.gitignore,CHANGELOG.md) [![IMIR](https://raw.githubusercontent.com/RAprogramm/infra-metrics-insight-renderer/main/assets/badges/imir-badge-simple-public.svg)](https://github.com/RAprogramm/infra-metrics-insight-renderer) > 🇬🇧 [Read README in English](README.md) @@ -43,6 +44,7 @@ SPDX-License-Identifier: MIT - [코드 커버리지](#코드-커버리지) - [빠른 시작](#빠른-시작) - [고급 사용법](#고급-사용법) +- [예제](#예제) - [리소스](#리소스) - [메트릭](#메트릭) - [라이선스](#라이선스) @@ -68,11 +70,11 @@ SPDX-License-Identifier: MIT ## 주요 특징 - **통합된 분류 체계.** `AppError`, `AppErrorKind` 및 `AppCode`는 보수적인 HTTP/gRPC 매핑, 즉시 사용 가능한 재시도/인증 힌트 및 `ProblemJson`을 통한 RFC7807 출력과 함께 도메인 및 전송 관련 사항을 모델링합니다. -- **네이티브 파생.** `#[derive(Error)]`, `#[derive(Masterror)]`, `#[app_error]`, `#[masterror(...)]` 및 `#[provide]`는 소스, 백트레이스, 텔레메트리 프로바이더 및 리덕션 정책을 전달하면서 커스텀 타입을 `AppError`에 연결합니다. +- **네이티브 파생.** `#[derive(Error)]` 및 `#[derive(Masterror)]`는 커스텀 타입을 런타임 타입에 연결합니다. `#[masterror(...)]`와 함께 사용하는 `#[derive(Masterror)]`는 소스, 백트레이스, 텔레메트리 필드 및 리덕션 정책을 전달하고, `#[app_error]`는 파생된 오류를 `AppErrorKind`/`AppCode`에 매핑하며(선택적으로 해당 `Display` 메시지를 노출), `#[provide]`는 도메인 오류 자체에 타입 기반 텔레메트리 프로바이더를 등록합니다. - **타입 기반 텔레메트리.** `Metadata`는 필드별 리덕션 제어 및 `field::*`의 빌더와 함께 구조화된 키/값 컨텍스트(문자열, 정수, 부동 소수점, 기간, IP 주소 및 선택적 JSON)를 저장하므로 수동 `String` 맵 없이 로그를 구조화할 수 있습니다. - **전송 어댑터.** 선택적 기능은 린 기본 빌드를 오염시키지 않고 Actix/Axum 응답자, `tonic::Status` 변환, WASM/브라우저 로깅 및 OpenAPI 스키마 생성을 제공합니다. -- **실전 검증된 통합.** `sqlx`, `reqwest`, `redis`, `validator`, `config`, `tokio`, `teloxide`, `multipart`, Telegram WebApp SDK 등을 위한 집중적인 매핑을 활성화하세요. 각각은 텔레메트리가 첨부된 분류 체계로 라이브러리 오류를 변환합니다. -- **즉시 사용 가능한 기본값.** `turnkey` 모듈은 박스에서 꺼내자마자 일관된 기준선을 원하는 팀을 위해 즉시 사용 가능한 오류 카탈로그, 헬퍼 빌더 및 추적 계측을 제공합니다. +- **실전 검증된 통합.** `sqlx`, `reqwest`, `redis`, `validator`, `config`, `tokio`, `teloxide`, `multipart`, Telegram init-data 검증 등을 위한 집중적인 매핑을 활성화하세요. 각각은 텔레메트리가 첨부된 분류 체계로 라이브러리 오류를 변환합니다. +- **즉시 사용 가능한 기본값.** `turnkey` 모듈은 박스에서 꺼내자마자 일관된 기준선을 원하는 팀을 위해 즉시 사용 가능한 오류 카탈로그, 휴리스틱 분류기 및 정규 분류 체계로의 보수적인 매핑을 제공합니다. - **타입 기반 제어 흐름 매크로.** `ensure!` 및 `fail!`은 해피 패스에서 할당이나 포매팅 없이 도메인 오류로 함수를 단락합니다.
@@ -109,12 +111,12 @@ SPDX-License-Identifier: MIT ## 기능 플래그 -필요한 것만 선택하세요; 모든 것이 기본적으로 비활성화되어 있습니다. +필요한 것만 선택하세요; 기본 기능 세트는 `std`뿐이며, 나머지는 모두 옵트인입니다. - **웹 전송:** `axum`, `actix`, `multipart`, `openapi`, `serde_json`. - **텔레메트리 및 관찰성:** `tracing`, `metrics`, `backtrace`, 컬러 터미널 출력을 위한 `colored`. - **비동기 및 IO 통합:** `tokio`, `reqwest`, `sqlx`, `sqlx-migrate`, `redis`, `validator`, `config`. -- **메시징 및 봇:** `teloxide`, `telegram-webapp-sdk`. +- **메시징 및 봇:** `teloxide`, `init-data-rs`를 통한 Telegram Mini App init-data 검증을 위한 `init-data`. - **프론트엔드 도구:** WASM/브라우저 콘솔 로깅을 위한 `frontend`. - **gRPC:** `tonic::Status` 응답을 발행하기 위한 `tonic`. - **배터리 포함:** 사전 구축된 분류 체계와 헬퍼를 채택하기 위한 `turnkey`. @@ -137,15 +139,15 @@ SPDX-License-Identifier: MIT ~~~toml [dependencies] -masterror = { version = "0.24.19", default-features = false } -# 또는 기능과 함께: -# masterror = { version = "0.24.19", features = [ +masterror = { version = "0.28.0", default-features = false } +# or with features: +# masterror = { version = "0.28.0", features = [ # "std", "axum", "actix", "openapi", # "serde_json", "tracing", "metrics", "backtrace", -# "sqlx", "sqlx-migrate", "reqwest", "redis", -# "validator", "config", "tokio", "multipart", -# "teloxide", "telegram-webapp-sdk", "tonic", "frontend", -# "turnkey", "benchmarks" +# "colored", "sqlx", "sqlx-migrate", "reqwest", +# "redis", "validator", "config", "tokio", +# "multipart", "teloxide", "init-data", "tonic", +# "frontend", "turnkey", "benchmarks" # ] } ~~~ @@ -171,7 +173,7 @@ cargo bench -F benchmarks --bench error_paths 이 스위트는 두 그룹을 발행합니다: -- `context_into_error/*`는 리덕션 모드와 비리덕션 모드 모두에서 대표적인 메타데이터(문자열, 카운터, 기간, IP)가 포함된 더미 소스 오류를 `Context::into_error`를 통해 승격합니다. +- `context_into_error/*`는 리덕션 모드와 비리덕션 모드 모두에서 대표적인 메타데이터(문자열, 카운터, 기간, IP)가 포함된 더미 소스 오류를 `ResultExt::ctx`를 통해 승격합니다. - `problem_json_from_app_error/*`는 결과 `AppError` 값을 소비하여 `ProblemJson::from_app_error`를 통해 RFC 7807 페이로드를 빌드하며, 메시지 리덕션 및 필드 정책이 직렬화에 미치는 영향을 보여줍니다. 변경 사항을 조사할 때 처리량과 더 엄격한 신뢰 구간 간의 균형을 맞추기 위해 `--` 이후에 Criterion CLI 플래그(예: `--sample-size 200` 또는 `--save-baseline local`)를 조정하세요. @@ -411,7 +413,9 @@ assert_eq!(
구조화된 텔레메트리 프로바이더 및 AppError 매핑 -`#[provide(...)]`는 `std::error::Request`를 통해 타입 기반 컨텍스트를 노출하고, `#[app_error(...)]`는 도메인 오류가 `AppError` 및 `AppCode`로 변환되는 방법을 기록합니다. 파생은 `thiserror`의 구문을 미러링하고 선택적 텔레메트리 전파 및 `masterror` 런타임 타입으로의 직접 변환으로 확장합니다. +`#[provide(...)]`는 `std::error::Request`를 통해 타입 기반 컨텍스트를 노출하고, `#[app_error(...)]`는 도메인 오류가 `AppError` 및 `AppCode`로 변환되는 방법을 기록합니다. 파생은 `thiserror`의 구문을 미러링합니다. 생성된 `From` 변환은 매핑된 kind와 code만 담은 `AppError`를 생성하며(`message` 플래그가 설정된 경우 `Display` 출력을 공개 메시지로 포함), 원본 도메인 오류는 폐기되므로 그 소스와 텔레메트리 프로바이더는 전달되지 않습니다. 변환하기 전에 도메인 오류에서 텔레메트리를 요청하세요. + +`request_ref`/`request_value` 및 `std::error::Request` 메커니즘에는 nightly 툴체인(`error_generic_member_access`)이 필요합니다; 크레이트는 빌드 시점에 컴파일러 지원을 감지하여 사용 가능한 경우에만 프로바이더 통합을 활성화합니다. ~~~rust use std::error::request_ref; @@ -443,8 +447,7 @@ let snapshot = request_ref::(&err).expect("telemetry"); assert_eq!(snapshot.value, 42); let app: AppError = err.into(); -let via_app = request_ref::(&app).expect("telemetry"); -assert_eq!(via_app.name, "db.query"); +assert!(matches!(app.kind, AppErrorKind::Service)); ~~~ 선택적 텔레메트리는 존재할 때만 표시되므로 `None`은 프로바이더를 등록하지 않습니다. 호출자가 소유권을 요청할 때 소유된 스냅샷을 여전히 값으로 제공할 수 있습니다: @@ -509,11 +512,10 @@ assert!(matches!(app.kind, AppErrorKind::Service)); ~~~rust use masterror::{AppError, AppErrorKind, ProblemJson}; -use std::time::Duration; let problem = ProblemJson::from_app_error( AppError::new(AppErrorKind::Unauthorized, "Token expired") - .with_retry_after_duration(Duration::from_secs(30)) + .with_retry_after_secs(30) .with_www_authenticate(r#"Bearer realm="api", error="invalid_token""#) ); @@ -525,85 +527,53 @@ assert_eq!(problem.grpc.expect("grpc").name, "UNAUTHENTICATED");
- DisplayMode를 통한 환경 인식 오류 포매팅 - -`DisplayMode` API를 사용하면 오류 처리 코드를 변경하지 않고도 배포 환경에 따라 오류 출력 포매팅을 제어할 수 있습니다. 세 가지 모드를 사용할 수 있습니다: - -- **`DisplayMode::Prod`** — 프로덕션 로그에 최적화된 최소 필드가 포함된 경량 JSON 출력. `kind`, `code` 및 `message`(리덕션되지 않은 경우)만 포함합니다. 민감한 메타데이터를 자동으로 필터링합니다. - -- **`DisplayMode::Local`** — 전체 컨텍스트가 포함된 사람이 읽을 수 있는 여러 줄 출력. 오류 세부 정보, 전체 소스 체인, 모든 메타데이터 및 백트레이스(활성화된 경우)를 표시합니다. 로컬 개발 및 디버깅에 가장 적합합니다. + DisplayMode를 통한 환경 감지 -- **`DisplayMode::Staging`** — 추가 컨텍스트가 포함된 JSON 출력. `kind`, `code`, `message`, 제한된 `source_chain` 및 필터링된 메타데이터를 포함합니다. 더 많은 세부 정보가 포함된 구조화된 로그가 필요한 스테이징 환경에 유용합니다. +`DisplayMode`는 배포 환경(`Prod`, `Local` 또는 `Staging`)을 감지하여 코드가 이에 따라 분기할 수 있도록 합니다. `DisplayMode::current()`는 다음 순서로 모드를 결정하고 첫 액세스 시 결과를 캐시합니다: -**자동 환경 감지:** - -모드는 다음 순서로 자동 감지됩니다: 1. `MASTERROR_ENV` 환경 변수 (`prod`, `local` 또는 `staging`) -2. `KUBERNETES_SERVICE_HOST` 존재 여부 (`Prod` 모드 트리거) +2. `KUBERNETES_SERVICE_HOST` 존재 여부 (`Prod` 선택) 3. 빌드 구성 (`debug_assertions` → `Local`, 릴리스 → `Prod`) -결과는 첫 번째 액세스 시 캐시되어 후속 호출에서 비용이 전혀 발생하지 않습니다. - ~~~rust use masterror::DisplayMode; -// 현재 모드 쿼리 (첫 호출 후 캐시됨) let mode = DisplayMode::current(); match mode { - DisplayMode::Prod => println!("프로덕션 모드에서 실행 중"), - DisplayMode::Local => println!("로컬 개발 모드에서 실행 중"), - DisplayMode::Staging => println!("스테이징 모드에서 실행 중"), + DisplayMode::Prod => println!("Running in production mode"), + DisplayMode::Local => println!("Running in local development mode"), + DisplayMode::Staging => println!("Running in staging mode"), } ~~~ +참고: `AppError`의 `Display`는 아직 `DisplayMode`를 참조하지 않습니다 — 출력은 모든 모드에서 동일합니다. 현재 유일한 포매팅 분기는 아래에 설명된 `colored` 기능이며, 모드 인식 포매팅은 아직 연결되어 있지 않습니다. + **컬러 터미널 출력:** -로컬 모드에서 향상된 터미널 출력을 위해 `colored` 기능을 활성화하세요: +향상된 터미널 출력을 위해 `colored` 기능을 활성화하세요. 이 기능이 활성화되어 있으면 감지된 모드와 관계없이 항상 적용됩니다: ~~~toml [dependencies] -masterror = { version = "0.24.19", features = ["colored"] } +masterror = { version = "0.28.0", features = ["colored"] } ~~~ -`colored`가 활성화되면 오류가 구문 강조 표시와 함께 표시됩니다: -- 굵은 글씨로 표시되는 오류 종류 및 코드 -- 색상으로 표시되는 오류 메시지 -- 들여쓰기된 소스 체인 -- 강조 표시된 메타데이터 키 - -~~~rust -use masterror::{AppError, field}; - -let error = AppError::not_found("사용자를 찾을 수 없음") - .with_field(field::str("user_id", "12345")) - .with_field(field::str("request_id", "abc-def")); - -// 'colored' 없이: 일반 텍스트 -// 'colored' 사용 시: 터미널에서 색상 코딩된 출력 -println!("{}", error); -~~~ - -**프로덕션 vs 개발 출력:** - `colored` 기능 없이 오류는 `AppErrorKind` 레이블을 표시합니다: ~~~ NotFound ~~~ -`colored` 기능 사용 시 컨텍스트가 포함된 전체 여러 줄 형식: +`colored` 사용 시 컨텍스트가 포함된 전체 여러 줄 형식: ~~~ Error: NotFound Code: NOT_FOUND -Message: 사용자를 찾을 수 없음 +Message: User not found Context: user_id: 12345 request_id: abc-def ~~~ -이러한 구분은 프로덕션 로그를 깔끔하게 유지하면서 로컬 디버깅 세션 중에 개발자에게 풍부한 컨텍스트를 제공합니다. -
@@ -618,6 +588,31 @@ Context: --- +## 예제 + +인기 있는 프레임워크와의 masterror 통합을 보여주는 포괄적인 실전 예제: + +| 예제 | 설명 | 기능 | +|---------|-------------|----------| +| [**axum-rest-api**](examples/axum-rest-api/) | RFC 7807 Problem Details를 사용하는 REST API | HTTP 엔드포인트, 도메인 오류, 통합 테스트 | +| [**sqlx-database**](examples/sqlx-database/) | SQLx를 사용한 데이터베이스 오류 처리 | 연결 오류, 제약 조건 위반, 트랜잭션 | +| [**custom-domain-errors**](examples/custom-domain-errors/) | 결제 처리 도메인 오류 | 파생 매크로, 오류 변환, 구조화된 오류 | +| [**basic-async**](examples/basic-async/) | tokio를 사용한 비동기 오류 처리 | 오류 전파, 타임아웃 처리, Result 타입 | + +모든 예제는 실행 가능합니다; axum-rest-api 예제는 추가로 통합 테스트를 함께 제공합니다. 전체 소스 코드와 문서는 [`examples/`](examples/) 디렉터리를 참조하세요. + +
+ + + +
+ +--- + ## 리소스 - 단계별 가이드, `thiserror`/`anyhow`와의 비교 및 문제 해결 레시피는 [오류 처리 위키](https://github.com/RAprogramm/masterror/wiki)를 참조하세요. @@ -655,6 +650,4 @@ Context: ## 라이선스 -MSRV: **1.90** · License: **MIT OR Apache-2.0** · `unsafe` 없음 - - +MSRV: **1.96** · License: **MIT** · `unsafe` 없음 diff --git a/README.ru.md b/README.ru.md index a1c46b9..7ee1b28 100644 --- a/README.ru.md +++ b/README.ru.md @@ -14,12 +14,13 @@ SPDX-License-Identifier: MIT [![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) [![Downloads](https://img.shields.io/crates/d/masterror)](https://crates.io/crates/masterror) - ![MSRV](https://img.shields.io/badge/MSRV-1.90-blue) - ![License](https://img.shields.io/badge/License-MIT%20or%20Apache--2.0-informational) + ![MSRV](https://img.shields.io/badge/MSRV-1.96-blue) + ![License](https://img.shields.io/badge/License-MIT-informational) + [![REUSE status](https://api.reuse.software/badge/github.com/RAprogramm/masterror)](https://api.reuse.software/info/github.com/RAprogramm/masterror) [![codecov](https://codecov.io/gh/RAprogramm/masterror/graph/badge.svg?token=V9JQDTZLXH)](https://codecov.io/gh/RAprogramm/masterror) [![CI](https://github.com/RAprogramm/masterror/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/RAprogramm/masterror/actions/workflows/ci.yml?query=branch%3Amain) - [![Hits-of-Code](https://hitsofcode.com/github/RAprogramm/masterror?branch=main)](https://hitsofcode.com/github/RAprogramm/masterror/view?branch=main) + [![Hits-of-Code](https://hitsofcode.com/github/RAprogramm/masterror?branch=main&exclude=Cargo.lock,.gitignore,CHANGELOG.md)](https://hitsofcode.com/github/RAprogramm/masterror/view?branch=main&exclude=Cargo.lock,.gitignore,CHANGELOG.md) [![IMIR](https://raw.githubusercontent.com/RAprogramm/infra-metrics-insight-renderer/main/assets/badges/imir-badge-simple-public.svg)](https://github.com/RAprogramm/infra-metrics-insight-renderer) > 🇬🇧 [Read README in English](README.md) @@ -43,6 +44,7 @@ SPDX-License-Identifier: MIT - [Покрытие кода](#покрытие-кода) - [Быстрый старт](#быстрый-старт) - [Расширенное использование](#расширенное-использование) +- [Примеры](#примеры) - [Ресурсы](#ресурсы) - [Метрики](#метрики) - [Лицензия](#лицензия) @@ -67,12 +69,12 @@ SPDX-License-Identifier: MIT ## Особенности -- **Унифицированная таксономия.** `AppError`, `AppErrorKind` и `AppCode` моделируют доменные и транспортные задачи с консервативными отображениями HTTP/gRPC, готовыми подсказками для повторных попыток и аутентификации, а также вывода RFC7807 через `ProblemJson`. -- **Нативные derive-макросы.** `#[derive(Error)]`, `#[derive(Masterror)]`, `#[app_error]`, `#[masterror(...)]` и `#[provide]` встраивают пользовательские типы в `AppError`, передавая источники, бэктрейсы, провайдеры телеметрии и политики редактирования. +- **Унифицированная таксономия.** `AppError`, `AppErrorKind` и `AppCode` моделируют доменные и транспортные задачи с консервативными отображениями HTTP/gRPC, готовыми подсказками для повторных попыток и аутентификации, а также выводом RFC7807 через `ProblemJson`. +- **Нативные derive-макросы.** `#[derive(Error)]` и `#[derive(Masterror)]` встраивают пользовательские типы в типы времени выполнения. `#[derive(Masterror)]` с `#[masterror(...)]` пробрасывает источники, бэктрейсы, поля телеметрии и политику редактирования; `#[app_error]` отображает произведенную ошибку в `AppErrorKind`/`AppCode` (опционально раскрывая её сообщение `Display`), а `#[provide]` регистрирует типизированные провайдеры телеметрии на самой доменной ошибке. - **Типизированная телеметрия.** `Metadata` хранит структурированный контекст в формате ключ/значение (строки, целые числа, числа с плавающей точкой, длительности, IP-адреса и опциональный JSON) с контролем редактирования для каждого поля и конструкторами в `field::*`, чтобы логи оставались структурированными без ручного создания `String`-карт. - **Транспортные адаптеры.** Опциональные функции предоставляют респондеры Actix/Axum, конверсии в `tonic::Status`, логирование WASM/браузера и генерацию схемы OpenAPI без загрязнения компактной сборки по умолчанию. -- **Проверенные интеграции.** Включите точечные отображения для `sqlx`, `reqwest`, `redis`, `validator`, `config`, `tokio`, `teloxide`, `multipart`, Telegram WebApp SDK и других — каждое из них транслирует библиотечные ошибки в таксономию с присоединенной телеметрией. -- **Готовые значения по умолчанию.** Модуль `turnkey` поставляет готовый к использованию каталог ошибок, вспомогательные конструкторы и инструментарий трассировки для команд, которые хотят получить согласованную базовую линию из коробки. +- **Проверенные интеграции.** Включите точечные отображения для `sqlx`, `reqwest`, `redis`, `validator`, `config`, `tokio`, `teloxide`, `multipart`, валидации Telegram init-data и других — каждое из них транслирует библиотечные ошибки в таксономию с присоединенной телеметрией. +- **Готовые значения по умолчанию.** Модуль `turnkey` поставляет готовый к использованию каталог ошибок, эвристический классификатор и консервативные отображения в каноническую таксономию для команд, которые хотят получить согласованную базовую линию из коробки. - **Типизированные макросы управления потоком.** `ensure!` и `fail!` прерывают выполнение функций с доменными ошибками без выделения памяти или форматирования на успешном пути.
@@ -109,13 +111,13 @@ SPDX-License-Identifier: MIT ## Флаги функций -Выбирайте только то, что вам нужно; по умолчанию все отключено. +Выбирайте только то, что вам нужно; по умолчанию включён только `std`, всё остальное подключается явно. - **Веб-транспорты:** `axum`, `actix`, `multipart`, `openapi`, `serde_json`. - **Телеметрия и наблюдаемость:** `tracing`, `metrics`, `backtrace`, `colored` для цветного вывода в терминале. - **Асинхронные интеграции и ввод/вывод:** `tokio`, `reqwest`, `sqlx`, `sqlx-migrate`, `redis`, `validator`, `config`. -- **Обмен сообщениями и боты:** `teloxide`, `telegram-webapp-sdk`. +- **Обмен сообщениями и боты:** `teloxide`, `init-data` для валидации init-data Telegram Mini App через `init-data-rs`. - **Инструменты фронтенда:** `frontend` для логирования WASM/консоли браузера. - **gRPC:** `tonic` для отправки ответов `tonic::Status`. - **Всё включено:** `turnkey` для принятия готовой таксономии и вспомогательных функций. @@ -138,15 +140,15 @@ SPDX-License-Identifier: MIT ~~~toml [dependencies] -masterror = { version = "0.24.19", default-features = false } -# или с функциями: -# masterror = { version = "0.24.19", features = [ +masterror = { version = "0.28.0", default-features = false } +# or with features: +# masterror = { version = "0.28.0", features = [ # "std", "axum", "actix", "openapi", # "serde_json", "tracing", "metrics", "backtrace", -# "sqlx", "sqlx-migrate", "reqwest", "redis", -# "validator", "config", "tokio", "multipart", -# "teloxide", "telegram-webapp-sdk", "tonic", "frontend", -# "turnkey", "benchmarks" +# "colored", "sqlx", "sqlx-migrate", "reqwest", +# "redis", "validator", "config", "tokio", +# "multipart", "teloxide", "init-data", "tonic", +# "frontend", "turnkey", "benchmarks" # ] } ~~~ @@ -172,7 +174,7 @@ cargo bench -F benchmarks --bench error_paths Набор тестов выдает две группы: -- `context_into_error/*` продвигает фиктивную исходную ошибку с репрезентативными метаданными (строки, счетчики, длительности, IP-адреса) через `Context::into_error` в режимах с редактированием и без редактирования. +- `context_into_error/*` продвигает фиктивную исходную ошибку с репрезентативными метаданными (строки, счетчики, длительности, IP-адреса) через `ResultExt::ctx` в режимах с редактированием и без редактирования. - `problem_json_from_app_error/*` использует результирующие значения `AppError` для построения полезных нагрузок RFC 7807 через `ProblemJson::from_app_error`, показывая, как редактирование сообщений и политики полей влияют на сериализацию. Настройте флаги командной строки Criterion (например, `--sample-size 200` или `--save-baseline local`) после `--` для обмена пропускной способности на более точные доверительные интервалы при исследовании изменений. @@ -412,7 +414,9 @@ assert_eq!(
Структурированные провайдеры телеметрии и отображения AppError -`#[provide(...)]` предоставляет типизированный контекст через `std::error::Request`, в то время как `#[app_error(...)]` записывает, как ваша доменная ошибка транслируется в `AppError` и `AppCode`. Derive отражает синтаксис `thiserror` и расширяет его опциональным распространением телеметрии и прямыми конверсиями в типы времени выполнения `masterror`. +`#[provide(...)]` предоставляет типизированный контекст через `std::error::Request`, в то время как `#[app_error(...)]` записывает, как ваша доменная ошибка транслируется в `AppError` и `AppCode`. Derive отражает синтаксис `thiserror`. Обратите внимание, что сгенерированные конверсии `From` создают `AppError`, несущий только отображенные kind и code (плюс вывод `Display` как публичное сообщение, если установлен флаг `message`) — исходная доменная ошибка отбрасывается, поэтому её источники и провайдеры телеметрии не пробрасываются. Запрашивайте телеметрию у доменной ошибки до конверсии. + +`request_ref`/`request_value` и механизм `std::error::Request` требуют nightly-тулчейна (`error_generic_member_access`); крейт определяет поддержку компилятора во время сборки и включает интеграцию провайдеров только когда она доступна. ~~~rust use std::error::request_ref; @@ -444,8 +448,7 @@ let snapshot = request_ref::(&err).expect("telemetry"); assert_eq!(snapshot.value, 42); let app: AppError = err.into(); -let via_app = request_ref::(&app).expect("telemetry"); -assert_eq!(via_app.name, "db.query"); +assert!(matches!(app.kind, AppErrorKind::Service)); ~~~ Опциональная телеметрия появляется только когда присутствует, поэтому `None` не регистрирует провайдера. Собственные снимки все еще могут быть предоставлены как значения, когда вызывающая сторона запрашивает владение: @@ -510,11 +513,10 @@ assert!(matches!(app.kind, AppErrorKind::Service)); ~~~rust use masterror::{AppError, AppErrorKind, ProblemJson}; -use std::time::Duration; let problem = ProblemJson::from_app_error( AppError::new(AppErrorKind::Unauthorized, "Token expired") - .with_retry_after_duration(Duration::from_secs(30)) + .with_retry_after_secs(30) .with_www_authenticate(r#"Bearer realm="api", error="invalid_token""#) ); @@ -526,85 +528,58 @@ assert_eq!(problem.grpc.expect("grpc").name, "UNAUTHENTICATED");
- Форматирование ошибок с учётом окружения через DisplayMode - -API `DisplayMode` позволяет контролировать форматирование вывода ошибок в зависимости от окружения развертывания без изменения кода обработки ошибок. Доступны три режима: - -- **`DisplayMode::Prod`** — Компактный JSON-вывод с минимальным набором полей, оптимизированный для production логов. Включает только `kind`, `code` и `message` (если не скрыто). Автоматически фильтрует чувствительные метаданные. - -- **`DisplayMode::Local`** — Человекочитаемый многострочный вывод с полным контекстом. Показывает детали ошибки, полную цепочку источников, все метаданные и бэктрейс (если включен). Лучший вариант для локальной разработки и отладки. + Определение окружения через DisplayMode -- **`DisplayMode::Staging`** — JSON-вывод с дополнительным контекстом. Включает `kind`, `code`, `message`, ограниченную `source_chain` и отфильтрованные метаданные. Полезен для staging окружений, где нужны структурированные логи с большей детализацией. +`DisplayMode` определяет окружение развертывания (`Prod`, `Local` или +`Staging`), чтобы ваш код мог ветвиться по нему. `DisplayMode::current()` +разрешает режим в следующем порядке и кэширует результат при первом обращении: -**Автоматическое определение окружения:** - -Режим определяется автоматически в следующем порядке: 1. Переменная окружения `MASTERROR_ENV` (`prod`, `local` или `staging`) -2. Наличие `KUBERNETES_SERVICE_HOST` (активирует режим `Prod`) +2. Наличие `KUBERNETES_SERVICE_HOST` (выбирает `Prod`) 3. Конфигурация сборки (`debug_assertions` → `Local`, release → `Prod`) -Результат кэшируется при первом обращении для нулевой стоимости последующих вызовов. - ~~~rust use masterror::DisplayMode; -// Запрос текущего режима (кэшируется после первого вызова) let mode = DisplayMode::current(); match mode { - DisplayMode::Prod => println!("Работа в production режиме"), - DisplayMode::Local => println!("Работа в режиме локальной разработки"), - DisplayMode::Staging => println!("Работа в staging режиме"), + DisplayMode::Prod => println!("Running in production mode"), + DisplayMode::Local => println!("Running in local development mode"), + DisplayMode::Staging => println!("Running in staging mode"), } ~~~ +Примечание: `Display` для `AppError` пока не учитывает `DisplayMode` — вывод +идентичен во всех режимах. Единственная ветка форматирования сегодня — функция +`colored`, описанная ниже; форматирование с учётом режима не подключено. + **Цветной вывод в терминале:** -Включите функцию `colored` для улучшенного вывода в терминале в локальном режиме: +Включите функцию `colored` для улучшенного вывода в терминале. Она применяется +всегда, когда функция включена, независимо от определённого режима: ~~~toml [dependencies] -masterror = { version = "0.24.19", features = ["colored"] } +masterror = { version = "0.28.0", features = ["colored"] } ~~~ -С включённой `colored`, ошибки отображаются с подсветкой синтаксиса: -- Тип и код ошибки выделены жирным шрифтом -- Сообщения об ошибках в цвете -- Цепочка источников с отступами -- Выделенные ключи метаданных - -~~~rust -use masterror::{AppError, field}; - -let error = AppError::not_found("Пользователь не найден") - .with_field(field::str("user_id", "12345")) - .with_field(field::str("request_id", "abc-def")); - -// Без 'colored': обычный текст -// С 'colored': цветной вывод в терминалах -println!("{}", error); -~~~ - -**Вывод в Production vs Development:** - Без функции `colored` ошибки отображают метку `AppErrorKind`: ~~~ NotFound ~~~ -С функцией `colored` полный многострочный формат с контекстом: +С функцией `colored` — полный многострочный формат с контекстом: ~~~ Error: NotFound Code: NOT_FOUND -Message: Пользователь не найден +Message: User not found Context: user_id: 12345 request_id: abc-def ~~~ -Такое разделение сохраняет production логи чистыми, предоставляя разработчикам богатый контекст во время локальных сессий отладки. -
@@ -619,6 +594,31 @@ Context: --- +## Примеры + +Полноценные примеры из реальной практики, демонстрирующие интеграцию masterror с популярными фреймворками: + +| Пример | Описание | Возможности | +|---------|-------------|----------| +| [**axum-rest-api**](examples/axum-rest-api/) | REST API с RFC 7807 Problem Details | HTTP-эндпоинты, доменные ошибки, интеграционные тесты | +| [**sqlx-database**](examples/sqlx-database/) | Обработка ошибок базы данных с SQLx | Ошибки подключения, нарушения ограничений, транзакции | +| [**custom-domain-errors**](examples/custom-domain-errors/) | Доменные ошибки обработки платежей | Derive-макрос, конверсия ошибок, структурированные ошибки | +| [**basic-async**](examples/basic-async/) | Асинхронная обработка ошибок с tokio | Распространение ошибок, обработка таймаутов, типы Result | + +Все примеры запускаемы; пример axum-rest-api дополнительно поставляется с интеграционными тестами. Смотрите директорию [`examples/`](examples/) для полного исходного кода и документации. + +
+ + + +
+ +--- + ## Ресурсы - Изучите [вики по обработке ошибок](https://github.com/RAprogramm/masterror/wiki) для пошаговых руководств, сравнений с `thiserror`/`anyhow` и рецептов решения проблем. @@ -656,5 +656,4 @@ Context: ## Лицензия -MSRV: **1.90** · Лицензия: **MIT OR Apache-2.0** · Без `unsafe` - +MSRV: **1.96** · Лицензия: **MIT** · Без `unsafe`