From 4f539c97c830639a73f3bf41a3cfeeb6535860b8 Mon Sep 17 00:00:00 2001 From: RA <70325462+RAprogramm@users.noreply.github.com> Date: Sun, 21 Sep 2025 09:15:20 +0700 Subject: [PATCH 1/2] docs: add error-handling wiki --- CHANGELOG.md | 9 ++ Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 18 +-- README.template.md | 3 + docs/wiki/error-crate-comparison.md | 144 +++++++++++++++++++ docs/wiki/index.md | 35 +++++ docs/wiki/masterror-application-guide.md | 163 ++++++++++++++++++++++ docs/wiki/patterns-and-troubleshooting.md | 126 +++++++++++++++++ docs/wiki/rust-error-handling-basics.md | 108 ++++++++++++++ 10 files changed, 600 insertions(+), 10 deletions(-) create mode 100644 docs/wiki/error-crate-comparison.md create mode 100644 docs/wiki/index.md create mode 100644 docs/wiki/masterror-application-guide.md create mode 100644 docs/wiki/patterns-and-troubleshooting.md create mode 100644 docs/wiki/rust-error-handling-basics.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 51e36e1..f0ade18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,15 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +## [0.11.1] - 2025-10-27 + +### Documentation +- Added a multi-page error-handling wiki (`docs/wiki`) with beginner-friendly + walkthroughs, framework patterns, and comparisons against `thiserror` and + `anyhow`. +- Linked the wiki from the README template so crate consumers can discover it + directly on crates.io and docs.rs. + ## [0.11.0] - 2025-10-26 ### Changed diff --git a/Cargo.lock b/Cargo.lock index 78d4eb7..907611e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1606,7 +1606,7 @@ dependencies = [ [[package]] name = "masterror" -version = "0.11.0" +version = "0.11.1" dependencies = [ "actix-web", "axum", diff --git a/Cargo.toml b/Cargo.toml index e539f93..beb989f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "masterror" -version = "0.11.0" +version = "0.11.1" rust-version = "1.90" edition = "2024" license = "MIT OR Apache-2.0" diff --git a/README.md b/README.md index bd3f8f8..bea52d7 100644 --- a/README.md +++ b/README.md @@ -23,15 +23,18 @@ Stable categories, conservative HTTP mapping, no `unsafe`. - Optional OpenAPI schema (via `utoipa`) - Conversions from `sqlx`, `reqwest`, `redis`, `validator`, `config`, `tokio` +πŸ‘‰ Explore the new [error-handling wiki](docs/wiki/index.md) for step-by-step +guides, comparisons with `thiserror`/`anyhow`, and troubleshooting recipes. + --- ### TL;DR ~~~toml [dependencies] -masterror = { version = "0.11.0", default-features = false } +masterror = { version = "0.11.1", default-features = false } # or with features: -# masterror = { version = "0.11.0", features = [ +# masterror = { version = "0.11.1", features = [ # "axum", "actix", "openapi", "serde_json", # "sqlx", "sqlx-migrate", "reqwest", "redis", # "validator", "config", "tokio", "multipart", @@ -66,10 +69,10 @@ masterror = { version = "0.11.0", default-features = false } ~~~toml [dependencies] # lean core -masterror = { version = "0.11.0", default-features = false } +masterror = { version = "0.11.1", default-features = false } # with Axum/Actix + JSON + integrations -# masterror = { version = "0.11.0", features = [ +# masterror = { version = "0.11.1", features = [ # "axum", "actix", "openapi", "serde_json", # "sqlx", "sqlx-migrate", "reqwest", "redis", # "validator", "config", "tokio", "multipart", @@ -625,13 +628,13 @@ assert_eq!(resp.status, 401); Minimal core: ~~~toml -masterror = { version = "0.11.0", default-features = false } +masterror = { version = "0.11.1", default-features = false } ~~~ API (Axum + JSON + deps): ~~~toml -masterror = { version = "0.11.0", features = [ +masterror = { version = "0.11.1", features = [ "axum", "serde_json", "openapi", "sqlx", "reqwest", "redis", "validator", "config", "tokio" ] } @@ -640,7 +643,7 @@ masterror = { version = "0.11.0", features = [ API (Actix + JSON + deps): ~~~toml -masterror = { version = "0.11.0", features = [ +masterror = { version = "0.11.1", features = [ "actix", "serde_json", "openapi", "sqlx", "reqwest", "redis", "validator", "config", "tokio" ] } @@ -711,4 +714,3 @@ MSRV = 1.90 (may raise in minor, never in patch). Apache-2.0 OR MIT, at your option. - diff --git a/README.template.md b/README.template.md index e071ddd..6fbc1a8 100644 --- a/README.template.md +++ b/README.template.md @@ -23,6 +23,9 @@ Stable categories, conservative HTTP mapping, no `unsafe`. - Optional OpenAPI schema (via `utoipa`) - Conversions from `sqlx`, `reqwest`, `redis`, `validator`, `config`, `tokio` +πŸ‘‰ Explore the new [error-handling wiki](docs/wiki/index.md) for step-by-step +guides, comparisons with `thiserror`/`anyhow`, and troubleshooting recipes. + --- ### TL;DR diff --git a/docs/wiki/error-crate-comparison.md b/docs/wiki/error-crate-comparison.md new file mode 100644 index 0000000..2fb3c37 --- /dev/null +++ b/docs/wiki/error-crate-comparison.md @@ -0,0 +1,144 @@ +# When to use `thiserror`, `anyhow`, or `masterror` + +Rust gives you multiple complementary error crates. This page compares how they +behave and shows concrete examples. + +## Quick summary + +| Crate | Primary goal | Typical usage stage | +|--------------|--------------------------------------------------|---------------------------------| +| `thiserror` | Define strongly typed domain errors with `derive`| Library and boundary layers | +| `anyhow` | Prototype quickly with dynamic context | CLI tools, experiments, glue | +| `masterror` | Ship stable API responses with rich metadata | Web backends, public interfaces | + +You can mix them: `masterror` re-exports the derive macro so you keep using +`#[derive(Error)]`, and you can attach `anyhow::Error` as context on +`AppError`. + +## Example: modelling a domain error + +The following snippet derives a typed error using each crate. + +```rust +use masterror::{AppCode, AppError, AppErrorKind, Error}; +use serde::Deserialize; +use thiserror::Error as ThisError; + +#[derive(Debug, Deserialize)] +struct Payload { + flag: bool, +} + +#[derive(Debug, Error)] +#[error("invalid payload: {source}")] +#[app_error(kind = AppErrorKind::BadRequest, code = AppCode::new("INVALID_PAYLOAD"))] +struct PayloadError { + #[from] + #[source] + source: serde_json::Error, +} + +fn parse_with_masterror(input: &str) -> masterror::AppResult { + let payload: Payload = serde_json::from_str(input).map_err(PayloadError::from)?; + Ok(payload) +} + +#[derive(Debug, ThisError)] +#[error("invalid payload: {source}")] +struct PlainPayloadError { + #[from] + source: serde_json::Error, +} + +fn parse_with_thiserror(input: &str) -> Result { + let payload = serde_json::from_str(input)?; + Ok(payload) +} + +fn parse_with_anyhow(input: &str) -> Result { + let payload = serde_json::from_str::(input) + .map_err(|err| anyhow::anyhow!("invalid payload: {err}"))?; + Ok(payload) +} + +fn convert_anyhow_into_masterror(err: anyhow::Error) -> AppError { + AppError::internal("unexpected parser failure").with_context(err) +} +``` + +Observations: + +- `thiserror` focuses on ergonomic derives and string formatting, but it does + not impose how callers expose the error to clients. +- `anyhow` stores a dynamic error with a backtrace. It is ideal for prototyping + and small CLIs, but it does not convey HTTP status codes or machine-readable + metadata. +- `masterror` is opinionated about API boundaries. By using `#[app_error]`, the + domain error maps to a stable `AppErrorKind` and `AppCode` automatically. + +## Mapping errors at service boundaries + +Imagine an Axum handler that validates JSON, queries a database, and reaches an +external API. Each crate offers different trade-offs. + +```rust +async fn handler_with_anyhow() -> Result { + let payload = parse_with_anyhow("{ }")?; + Ok(format!("flag: {}", payload.flag)) +} + +async fn handler_with_thiserror() -> Result { + let payload = parse_with_thiserror("{ }")?; + Ok(format!("flag: {}", payload.flag)) +} + +async fn handler_with_masterror() -> masterror::AppResult { + let payload = parse_with_masterror("{ }")?; + Ok(format!("flag: {}", payload.flag)) +} +``` + +- The `anyhow` version surfaces a stringified error and a backtrace. Clients + receive HTTP 500 unless you write custom mapping logic. +- The `thiserror` version returns a typed error, but you still have to convert it + into an HTTP response yourself. +- The `masterror` version already contains the HTTP 400 classification, a stable + `AppCode`, and optional JSON details. + +## Attaching context across crates + +You can combine the strengths of each crate. Keep `thiserror` derives for rich +messages, wrap the result in `AppError`, and use `anyhow` for debug traces when +needed. + +```rust +fn load_configuration(path: &std::path::Path) -> masterror::AppResult { + let contents = std::fs::read_to_string(path).map_err(|err| { + AppError::internal("failed to read configuration") + .with_code(AppCode::new("CONFIG_IO")) + .with_context(anyhow::Error::from(err)) + })?; + Ok(contents) +} +``` + +`AppError` stores the `anyhow::Error` internally without exposing it to clients. +You still emit clean JSON responses, while logs retain the full diagnostic +payload. + +## Why choose `masterror` + +1. **Stable contract.** `AppErrorKind` and `ErrorResponse` stay consistent across + services, making cross-team collaboration easier. +2. **Framework adapters.** Ready-to-use integrations with Axum, Actix Web, + `utoipa`, `serde_json`, and others remove boilerplate. +3. **Structured metadata.** Attach retry hints, authentication challenges, and + JSON details without building ad-hoc enums. +4. **Derive support.** Reuse the familiar `thiserror` syntax via + `masterror::Error` and augment it with `#[app_error]` rules. +5. **Context preservation.** Store source errors (including `anyhow::Error`) for + logging, while presenting sanitized messages externally. + +Use `anyhow` when speed matters more than structure, `thiserror` when crafting +libraries, and `masterror` when you need predictable, well-documented API +responses. diff --git a/docs/wiki/index.md b/docs/wiki/index.md new file mode 100644 index 0000000..a482f71 --- /dev/null +++ b/docs/wiki/index.md @@ -0,0 +1,35 @@ +# Masterror error-handling wiki + +This wiki collects step-by-step guides for building reliable error handling in Rust services. +Each page is intentionally short and focused so you can jump straight to the +section that matches your experience level. + +- [Rust error handling basics](rust-error-handling-basics.md) +- [Building applications with `masterror`](masterror-application-guide.md) +- [When to reach for `thiserror`, `anyhow`, or `masterror`](error-crate-comparison.md) +- [Patterns and troubleshooting](patterns-and-troubleshooting.md) + +## How the wiki is organised + +1. **Start with the basics** if you are new to `Result` and the `?` operator. +2. **Follow the application guide** to design domain-specific error types with + consistent wire responses. +3. **Read the comparison** to understand how `masterror` complements `thiserror` + and `anyhow` instead of replacing them outright. +4. **Review patterns and troubleshooting** when you need concrete recipes for + mapping third-party errors, logging, and testing. + +Each page contains runnable examples. Copy them into a new binary crate or an +`examples/` folder, run `cargo run`, and experiment. + +## Related documentation + +- [`README.md`](../README.md) and [`docs.rs/masterror`](https://docs.rs/masterror) + for API reference and feature lists. +- [`masterror-derive`](../../masterror-derive/README.md) to explore the derive + macro internals and advanced formatting capabilities. +- [`masterror-template`](../../masterror-template/README.md) for the shared + template parser used by the derive macros. + +Feedback and suggestions are welcome β€” open an issue or discussion on +[GitHub](https://github.com/RAprogramm/masterror). diff --git a/docs/wiki/masterror-application-guide.md b/docs/wiki/masterror-application-guide.md new file mode 100644 index 0000000..d936189 --- /dev/null +++ b/docs/wiki/masterror-application-guide.md @@ -0,0 +1,163 @@ +# Building applications with `masterror` + +`masterror` provides a stable taxonomy for API-driven services. This page shows +how to define domain errors, propagate them through business logic, and surface +them as structured responses. + +## Core types at a glance + +- `AppErrorKind` categorises a failure (`BadRequest`, `Unauthorized`, + `Validation`, `Internal`, ...). Each kind maps to a conservative HTTP status. +- `AppCode` is an optional machine-readable identifier for your API clients. +- `AppError` bundles a kind, developer message, optional `AppCode`, optional + structured details, and retry/authentication hints. +- `AppResult` is a convenient alias for `Result`. + +Use the helpers to construct errors without allocating intermediate `String`s. + +```rust +use masterror::{AppError, AppErrorKind, AppResult}; + +pub fn ensure_flag(flag: bool) -> AppResult<()> { + if !flag { + return Err(AppError::bad_request("flag must be enabled")); + } + Ok(()) +} + +pub fn get_secret(flag: bool) -> AppResult<&'static str> { + ensure_flag(flag)?; + Ok("swordfish") +} +``` + +`AppError::bad_request` returns an HTTP 400 response. Other helpers include +`AppError::internal`, `AppError::timeout`, `AppError::unauthorized`, and more. + +## Attaching codes and structured details + +Attach machine-friendly metadata so clients can branch on errors without parsing +text. + +```rust +use masterror::{AppCode, AppError}; +use serde::Serialize; + +#[derive(Debug, Serialize)] +struct MissingField<'a> { + field: &'a str, +} + +pub fn parse_payload(json: &str) -> masterror::AppResult<&str> { + let payload: serde_json::Value = serde_json::from_str(json).map_err(|err| { + AppError::bad_request("payload must be valid JSON") + .with_code(AppCode::new("INVALID_JSON")) + .with_details(&MissingField { field: "feature_flag" }) + .with_context(err) + })?; + + payload + .get("feature_flag") + .and_then(|value| value.as_str()) + .ok_or_else(|| { + AppError::bad_request("feature_flag string is required") + .with_code(AppCode::new("MISSING_FIELD")) + }) +} +``` + +`with_context` stores the original `serde_json::Error` for logging; clients only +see the sanitized message, code, and JSON details. + +## Deriving domain errors + +Combine `masterror::Error` derive macros with `#[app_error]` to convert domain +errors into `AppError` automatically. + +```rust +use masterror::{AppCode, AppErrorKind, Error}; + +#[derive(Debug, Error)] +#[error("database query failed: {source}")] +#[app_error(kind = AppErrorKind::Database, code = AppCode::new("DB_FAILURE"))] +pub struct DatabaseFailure { + #[from] + #[source] + source: sqlx_core::Error, +} + +pub async fn load_user(pool: &sqlx_core::pool::PoolConnection<'_, sqlx_core::Postgres>) + -> Result<(), DatabaseFailure> +{ + Err(sqlx_core::Error::RowNotFound)?; + Ok(()) +} +``` + +Whenever `DatabaseFailure` is converted into `AppError`, the derived impl picks +`AppErrorKind::Database` and attaches the `DB_FAILURE` code. No manual mapping is +required in handlers. + +## Framework integrations + +Enable the relevant feature to integrate with HTTP frameworks: + +- `axum`: `AppError` implements `IntoResponse` to emit JSON bodies that follow + `ErrorResponse` (status, code, message, optional details/retry info). +- `actix`: `AppError` implements `ResponseError` with the same JSON schema. +- `openapi`: `ErrorResponse` gains `utoipa::ToSchema` so your OpenAPI spec stays + in sync. + +Example Axum handler: + +```rust +use axum::{routing::get, Router}; +use masterror::AppError; + +async fn handler() -> masterror::AppResult<&'static str> { + Err(AppError::unauthorized("missing token")) +} + +fn app() -> Router { + Router::new().route("/", get(handler)) +} +``` + +Axum automatically converts the error into an HTTP 401 JSON payload. + +## Logging and telemetry + +`AppError` implements `std::error::Error`. Use `tracing` to log errors once, at +module boundaries (e.g., HTTP middleware or background task entry points). + +```rust +fn log_error(err: &masterror::AppError) { + tracing::error!(kind = ?err.kind, code = ?err.code, "request failed"); + if let Some(context) = err.context() { + tracing::debug!(?context, "captured error context"); + } +} +``` + +Avoid logging the same error multiple times β€” the structured data already +contains everything needed for observability dashboards. + +## Testing error behaviour + +Write unit tests that assert on the `AppErrorKind`, optional `AppCode`, and the +serialised `ErrorResponse` payload. + +```rust +#[test] +fn missing_field_is_bad_request() { + let err = parse_payload("{}").unwrap_err(); + assert!(matches!(err.kind, AppErrorKind::BadRequest)); + assert_eq!(err.code.unwrap().as_str(), "MISSING_FIELD"); + + let response: masterror::ErrorResponse = err.clone().into(); + assert_eq!(response.status.as_u16(), 400); +} +``` + +Cloning is cheap because `AppError` stores data on the stack and shares context +via `Arc` under the hood. Use these assertions to guarantee stable APIs. diff --git a/docs/wiki/patterns-and-troubleshooting.md b/docs/wiki/patterns-and-troubleshooting.md new file mode 100644 index 0000000..87a2249 --- /dev/null +++ b/docs/wiki/patterns-and-troubleshooting.md @@ -0,0 +1,126 @@ +# Patterns and troubleshooting + +This page collects recipes for common error-handling tasks along with debugging +strategies. + +## Mapping third-party errors + +Prefer typed conversions over string formatting. `masterror` exposes helper +constructors and feature-gated conversions. + +```rust +use masterror::{AppCode, AppError}; + +pub async fn fetch_user(client: &reqwest::Client) -> masterror::AppResult { + let response = client.get("https://example.com/user").send().await.map_err(|err| { + AppError::external_api("failed to reach user service") + .with_code(AppCode::new("UPSTREAM_HTTP")) + .with_context(err) + })?; + + response.text().await.map_err(|err| { + AppError::external_api("failed to decode response body").with_context(err) + }) +} +``` + +Enable the `reqwest` feature to classify timeouts and HTTP status codes +automatically. Similar conversions exist for `sqlx`, `redis`, `validator`, +`config`, and more. + +## Validating inputs + +Surface validation failures as structured data so clients can highlight fields. + +```rust +use masterror::{AppCode, AppError}; +use serde::Deserialize; +use validator::Validate; + +#[derive(Debug, Deserialize, Validate)] +struct CreateUser { + #[validate(length(min = 3))] + username: String, + + #[validate(email)] + email: String, +} + +pub fn validate(payload: &CreateUser) -> masterror::AppResult<()> { + payload.validate().map_err(|err| { + AppError::validation("invalid user payload") + .with_code(AppCode::new("VALIDATION_ERROR")) + .with_details(&err) + }) +} +``` + +`validator::ValidationErrors` implements `Serialize`, so it plugs directly into +`with_details`. + +## Emitting HTTP responses manually + +Sometimes you need to control the HTTP layer yourself (e.g., custom middleware). +Convert `AppError` into `ErrorResponse` and format it however you need. + +```rust +fn to_json(err: &masterror::AppError) -> serde_json::Value { + let response: masterror::ErrorResponse = err.clone().into(); + serde_json::json!({ + "status": response.status.as_u16(), + "code": response.code, + "message": response.message, + "details": response.details, + }) +} +``` + +The clone is cheap because `AppError` uses shared references for heavy context +objects. + +## Capturing reproducible logs + +1. Log errors at the boundary with `tracing::error!`, including `kind`, + `code`, and `retry` metadata. +2. Attach upstream errors via `with_context`. When you need additional metadata, + derive your error type with fields annotated using `#[provide]` from + `masterror::Error`. + +```rust +#[tracing::instrument(skip(err))] +fn log_for_support(err: &masterror::AppError) { + tracing::error!( + kind = ?err.kind, + code = ?err.code, + retry = ?err.retry, + auth = ?err.www_authenticate, + "request failed", + ); +} +``` + +`#[tracing::instrument]` captures spans automatically, so support teams can +reconstruct what happened. + +## Debugging common issues + +| Symptom | Checklist | +|---------|-----------| +| Validation failures return HTTP 500 | Enable the `validator` feature and expose handlers as `AppResult`. | +| JSON response lacks `code` | Call `.with_code(AppCode::new("..."))` or derive it via `#[app_error(code = ...)]`. | +| Logs show duplicated errors | Log once per request at the boundary; do not log again inside helpers. | +| `with_details` fails to compile | Ensure the value implements `Serialize` (derive or implement it manually). | +| Need to inspect nested errors | Call `err.context()` to retrieve captured sources, including `anyhow::Error`. | + +## Testing strategies + +- Unit-test constructors: assert on `AppErrorKind`, `AppCode`, retry hints, and + JSON serialisation. Use `serde_json::to_value` for comparisons. +- Integration-test HTTP handlers: send requests using `axum::Router` or + `actix_web::test::TestServer` and assert on status codes plus JSON bodies. +- Property-based tests (`proptest`) are great for validating validation logic and + parsing code β€” ensure the error surfaces the expected code even for extreme + inputs. + +Keep tests deterministic and avoid network calls; use mocks or in-memory +services instead. diff --git a/docs/wiki/rust-error-handling-basics.md b/docs/wiki/rust-error-handling-basics.md new file mode 100644 index 0000000..68f4fa1 --- /dev/null +++ b/docs/wiki/rust-error-handling-basics.md @@ -0,0 +1,108 @@ +# Rust error handling basics + +This page explains the building blocks of error handling in Rust. The goal is +to make the rest of the wiki easier to follow, even if you are new to the +language. + +## Terminology + +- **`Result`** is an enum with two variants: `Ok(T)` holds a successful + value, and `Err(E)` holds an error. Every function that can fail should return + a `Result`. +- **`?` operator** unwraps an `Ok` value or returns early with the `Err` variant. + It works with any `Result` or `Option` and is the primary way to propagate + errors. +- **`std::error::Error` trait** describes types that behave like errors. Most + libraries implement it for their failure types. Implementing `Error` allows + your type to integrate with logging, conversions, and `anyhow`. +- **`From`/`Into` conversions** are how one error turns into another. + When the `?` operator sees an `Err`, it uses `From` to convert between error + types automatically. + +## Writing a fallible function + +The following example downloads JSON from an in-memory HTTP server and parses a +field. It uses standard library errors and propagates them with `?`. + +```rust +use std::collections::HashMap; + +fn read_flag(data: &str) -> Result { + let payload: HashMap = serde_json::from_str(data)?; + let flag = payload + .get("feature_enabled") + .and_then(|value| value.as_bool()) + .unwrap_or(false); + Ok(flag) +} + +fn parse_response(response: &str) -> Result { + let enabled = read_flag(response)?; + Ok(enabled) +} + +#[derive(Debug, thiserror::Error)] +#[error("failed to parse feature flag: {source}")] +pub struct ReadFlagError { + #[from] + source: serde_json::Error, +} + +fn main() -> Result<(), ReadFlagError> { + let json = r#"{ "feature_enabled": true }"#; + let flag = parse_response(json)?; + assert!(flag); + Ok(()) +} +``` + +Key observations: + +1. `read_flag` returns `Result` because JSON parsing can + fail. Nothing special is required β€” the compiler enforces handling the error. +2. `parse_response` returns a custom `ReadFlagError` that wraps the parsing + error. The `?` operator converts the JSON error into `ReadFlagError` via the + `#[from]` attribute. +3. `main` uses `Result` as its return type. If an error occurs, the program exits + with a non-zero status code and prints the error. + +## Recovering from errors + +Not every error should bubble up. Use `match`, `if let`, or helper methods to +inspect and recover when possible. + +```rust +fn recover_or_default(data: &str) -> bool { + match read_flag(data) { + Ok(flag) => flag, + Err(err) => { + tracing::warn!(error = %err, "invalid feature flag payload"); + false + } + } +} +``` + +Rust encourages explicit recovery paths, so code remains predictable even when a +failure happens. + +## Mapping one error into another + +Applications frequently hide implementation details behind domain-specific +errors. `map_err` is a lightweight way to translate errors without introducing +new types. + +```rust +fn read_flag_for_user(data: &str, user_id: u64) -> Result { + read_flag(data).map_err(|err| { + masterror::AppError::bad_request( + format!("user {user_id} sent invalid JSON: {err}"), + ) + }) +} +``` + +`map_err` receives the original error and lets you convert it into an +application-level type β€” here we produce an HTTP 400 error using `masterror`'s +helper. The next pages expand on this technique and show how to avoid allocating +new `String`s by using structured conversions. From a3512958f4209414bcd09069a1f8bc58960e125a Mon Sep 17 00:00:00 2001 From: RA <70325462+RAprogramm@users.noreply.github.com> Date: Sun, 21 Sep 2025 09:15:21 +0700 Subject: [PATCH 2/2] docs: refresh readmes for expanded scope --- CHANGELOG.md | 10 ++++++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 23 +++++++++++++---------- README.ru.md | 21 +++++++++++++++------ README.template.md | 8 ++++++-- src/lib.rs | 27 +++++++++++++++++++++++++++ 7 files changed, 73 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 51e36e1..5a875ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,16 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +## [0.11.1] - 2025-10-27 + +### Documentation +- Highlighted the native derive macros, typed telemetry, browser logging, and + Turnkey taxonomy across the README template and regenerated README. +- Refreshed the Russian README with the same capability summary and updated the + installation snippets to `0.11.1`. +- Expanded the crate-level documentation to cover `#[app_error]`/`#[provide]` + usage and link to `std::error::Request` telemetry extraction. + ## [0.11.0] - 2025-10-26 ### Changed diff --git a/Cargo.lock b/Cargo.lock index 78d4eb7..907611e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1606,7 +1606,7 @@ dependencies = [ [[package]] name = "masterror" -version = "0.11.0" +version = "0.11.1" dependencies = [ "actix-web", "axum", diff --git a/Cargo.toml b/Cargo.toml index e539f93..beb989f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "masterror" -version = "0.11.0" +version = "0.11.1" rust-version = "1.90" edition = "2024" license = "MIT OR Apache-2.0" diff --git a/README.md b/README.md index bd3f8f8..325449f 100644 --- a/README.md +++ b/README.md @@ -14,14 +14,18 @@ > πŸ‡·πŸ‡Ί Π§ΠΈΡ‚Π°ΠΉΡ‚Π΅ README Π½Π° [русском языкС](README.ru.md). -Small, pragmatic error model for API-heavy Rust services. +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`, `ErrorResponse` -- Optional Axum/Actix integration +- Derive macros: `#[derive(Error)]`, `#[app_error]`, `#[provide]` for domain + mappings and structured telemetry +- Optional Axum/Actix integration and browser/WASM console logging - Optional OpenAPI schema (via `utoipa`) - Conversions from `sqlx`, `reqwest`, `redis`, `validator`, `config`, `tokio` +- Turnkey domain taxonomy and helpers (`turnkey` feature) --- @@ -29,9 +33,9 @@ Stable categories, conservative HTTP mapping, no `unsafe`. ~~~toml [dependencies] -masterror = { version = "0.11.0", default-features = false } +masterror = { version = "0.11.1", default-features = false } # or with features: -# masterror = { version = "0.11.0", features = [ +# masterror = { version = "0.11.1", features = [ # "axum", "actix", "openapi", "serde_json", # "sqlx", "sqlx-migrate", "reqwest", "redis", # "validator", "config", "tokio", "multipart", @@ -66,10 +70,10 @@ masterror = { version = "0.11.0", default-features = false } ~~~toml [dependencies] # lean core -masterror = { version = "0.11.0", default-features = false } +masterror = { version = "0.11.1", default-features = false } # with Axum/Actix + JSON + integrations -# masterror = { version = "0.11.0", features = [ +# masterror = { version = "0.11.1", features = [ # "axum", "actix", "openapi", "serde_json", # "sqlx", "sqlx-migrate", "reqwest", "redis", # "validator", "config", "tokio", "multipart", @@ -625,13 +629,13 @@ assert_eq!(resp.status, 401); Minimal core: ~~~toml -masterror = { version = "0.11.0", default-features = false } +masterror = { version = "0.11.1", default-features = false } ~~~ API (Axum + JSON + deps): ~~~toml -masterror = { version = "0.11.0", features = [ +masterror = { version = "0.11.1", features = [ "axum", "serde_json", "openapi", "sqlx", "reqwest", "redis", "validator", "config", "tokio" ] } @@ -640,7 +644,7 @@ masterror = { version = "0.11.0", features = [ API (Actix + JSON + deps): ~~~toml -masterror = { version = "0.11.0", features = [ +masterror = { version = "0.11.1", features = [ "actix", "serde_json", "openapi", "sqlx", "reqwest", "redis", "validator", "config", "tokio" ] } @@ -711,4 +715,3 @@ MSRV = 1.90 (may raise in minor, never in patch). Apache-2.0 OR MIT, at your option. - diff --git a/README.ru.md b/README.ru.md index 7e3011f..b846592 100644 --- a/README.ru.md +++ b/README.ru.md @@ -11,15 +11,24 @@ [![Security audit](https://github.com/RAprogramm/masterror/actions/workflows/ci.yml/badge.svg?branch=main&label=Security%20audit)](https://github.com/RAprogramm/masterror/actions/workflows/ci.yml?query=branch%3Amain) [![Cargo Deny](https://img.shields.io/github/actions/workflow/status/RAprogramm/masterror/ci.yml?branch=main&label=Cargo%20Deny)](https://github.com/RAprogramm/masterror/actions/workflows/ci.yml?query=branch%3Amain) -НСбольшая прагматичная модСль ошибок для Rust-сСрвисов с Π²Ρ‹Ρ€Π°ΠΆΠ΅Π½Π½Ρ‹ΠΌ API. Основной ΠΊΡ€Π΅ΠΉΡ‚ Π½Π΅ зависит ΠΎΡ‚ Π²Π΅Π±-Ρ„Ρ€Π΅ΠΉΠΌΠ²ΠΎΡ€ΠΊΠΎΠ², Π° Ρ€Π°ΡΡˆΠΈΡ€Π΅Π½ΠΈΡ Π²ΠΊΠ»ΡŽΡ‡Π°ΡŽΡ‚ΡΡ Ρ‡Π΅Ρ€Π΅Π· Ρ„ΠΈΡ‡ΠΈ. Ваксономия ошибок ΡΡ‚Π°Π±ΠΈΠ»ΡŒΠ½Π°, соотвСтствиС HTTP-ΠΊΠΎΠ΄Π°ΠΌ консСрвативно, `unsafe` Π·Π°ΠΏΡ€Π΅Ρ‰Ρ‘Π½. +НСбольшая прагматичная модСль ошибок для Rust-сСрвисов с Π²Ρ‹Ρ€Π°ΠΆΠ΅Π½Π½Ρ‹ΠΌ API ΠΈ +встроСнными Π΄Π΅Ρ€ΠΈΠ²Π°ΠΌΠΈ. +Основной ΠΊΡ€Π΅ΠΉΡ‚ Π½Π΅ зависит ΠΎΡ‚ Π²Π΅Π±-Ρ„Ρ€Π΅ΠΉΠΌΠ²ΠΎΡ€ΠΊΠΎΠ², Π° Ρ€Π°ΡΡˆΠΈΡ€Π΅Π½ΠΈΡ Π²ΠΊΠ»ΡŽΡ‡Π°ΡŽΡ‚ΡΡ Ρ‡Π΅Ρ€Π΅Π· +Ρ„ΠΈΡ‡ΠΈ. Ваксономия ошибок ΡΡ‚Π°Π±ΠΈΠ»ΡŒΠ½Π°, соотвСтствиС HTTP-ΠΊΠΎΠ΄Π°ΠΌ консСрвативно, +`unsafe` Π·Π°ΠΏΡ€Π΅Ρ‰Ρ‘Π½. ## ΠžΡΠ½ΠΎΠ²Π½Ρ‹Π΅ возмоТности - Π‘Π°Π·ΠΎΠ²Ρ‹Π΅ Ρ‚ΠΈΠΏΡ‹: `AppError`, `AppErrorKind`, `AppResult`, `AppCode`, `ErrorResponse`. -- АдаптСры для Axum ΠΈ Actix (ΠΎΠΏΡ†ΠΈΠΎΠ½Π°Π»ΡŒΠ½ΠΎ). +- Π”Π΅Ρ€ΠΈΠ²Ρ‹ `#[derive(Error)]`, `#[app_error]`, `#[provide]` для Ρ‚ΠΈΠΏΠΈΠ·ΠΈΡ€ΠΎΠ²Π°Π½Π½ΠΎΠ³ΠΎ + тСлСмСтричСского контСкста ΠΈ прямых конвСрсий Π΄ΠΎΠΌΠ΅Π½Π½Ρ‹Ρ… ошибок. +- АдаптСры для Axum ΠΈ Actix плюс Π»ΠΎΠ³ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ Π² Π±Ρ€Π°ΡƒΠ·Π΅Ρ€/`JsValue` для WASM (ΠΏΠΎ + Ρ„ΠΈΡ‡Π°ΠΌ). - ГСнСрация схСм OpenAPI Ρ‡Π΅Ρ€Π΅Π· `utoipa`. -- ΠšΠΎΠ½Π²Π΅Ρ€ΡΠΈΠΈ ΠΈΠ· распространённых Π±ΠΈΠ±Π»ΠΈΠΎΡ‚Π΅ΠΊ (`sqlx`, `reqwest`, `redis`, `validator`, `config`, `tokio` ΠΈ Π΄Ρ€.). -- Π“ΠΎΡ‚ΠΎΠ²Ρ‹ΠΉ ΠΏΡ€Π΅Π»ΡŽΠ΄ΠΈΡ-ΠΌΠΎΠ΄ΡƒΠ»ΡŒ, Ρ€Π΅ΡΠΊΡΠΏΠΎΡ€Ρ‚ΠΈΡ€ΡƒΡŽΡ‰ΠΈΠΉ Π½Π°ΠΈΠ±ΠΎΠ»Π΅Π΅ вострСбованныС Ρ‚ΠΈΠΏΡ‹ ΠΈ Ρ‚Ρ€Π΅ΠΉΡ‚Ρ‹. +- ΠšΠΎΠ½Π²Π΅Ρ€ΡΠΈΠΈ ΠΈΠ· распространённых Π±ΠΈΠ±Π»ΠΈΠΎΡ‚Π΅ΠΊ (`sqlx`, `reqwest`, `redis`, + `validator`, `config`, `tokio` ΠΈ Π΄Ρ€.). +- Π“ΠΎΡ‚ΠΎΠ²Ρ‹ΠΉ ΠΏΡ€Π΅Π»ΡŽΠ΄ΠΈΡ-ΠΌΠΎΠ΄ΡƒΠ»ΡŒ ΠΈ Ρ€Π°ΡΡˆΠΈΡ€Π΅Π½ΠΈΠ΅ `turnkey` с собствСнной таксономиСй + ошибок. ## Установка @@ -28,9 +37,9 @@ ~~~toml [dependencies] # минимальноС ядро -masterror = { version = "0.10.4", default-features = false } +masterror = { version = "0.11.1", default-features = false } # ΠΈΠ»ΠΈ с Π½ΡƒΠΆΠ½Ρ‹ΠΌΠΈ интСграциями -# masterror = { version = "0.10.4", features = [ +# masterror = { version = "0.11.1", features = [ # "axum", "actix", "openapi", "serde_json", # "sqlx", "sqlx-migrate", "reqwest", "redis", # "validator", "config", "tokio", "multipart", diff --git a/README.template.md b/README.template.md index e071ddd..0d5097a 100644 --- a/README.template.md +++ b/README.template.md @@ -14,14 +14,18 @@ > πŸ‡·πŸ‡Ί Π§ΠΈΡ‚Π°ΠΉΡ‚Π΅ README Π½Π° [русском языкС](README.ru.md). -Small, pragmatic error model for API-heavy Rust services. +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`, `ErrorResponse` -- Optional Axum/Actix integration +- Derive macros: `#[derive(Error)]`, `#[app_error]`, `#[provide]` for domain + mappings and structured telemetry +- Optional Axum/Actix integration and browser/WASM console logging - Optional OpenAPI schema (via `utoipa`) - Conversions from `sqlx`, `reqwest`, `redis`, `validator`, `config`, `tokio` +- Turnkey domain taxonomy and helpers (`turnkey` feature) --- diff --git a/src/lib.rs b/src/lib.rs index d36879f..ce7fe29 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -52,6 +52,33 @@ //! - `turnkey` β€” domain taxonomy and conversions for Turnkey errors, exposed in //! the `turnkey` module //! +//! # Derive macros and telemetry +//! +//! The [`masterror::Error`](crate::Error) derive mirrors `thiserror` while +//! adding `#[app_error]` and `#[provide]` attributes. Annotate your domain +//! errors once to surface structured telemetry via [`std::error::Request`] and +//! generate conversions into [`AppError`] / [`AppCode`]. +//! +//! ```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)); +//! ``` +//! +//! Use `#[provide]` to forward typed telemetry that downstream consumers can +//! extract from [`AppError`] via `std::error::Request`. +//! //! # Domain integrations: Turnkey //! //! With the `turnkey` feature enabled, the crate exports a `turnkey` module