From 899b32c9755f305afe3ead446d33132c75ed831e Mon Sep 17 00:00:00 2001 From: RA <70325462+RAprogramm@users.noreply.github.com> Date: Wed, 17 Sep 2025 15:25:04 +0700 Subject: [PATCH] docs: capture post-0.4 updates --- CHANGELOG.md | 25 +++++++++++++++++-- README.md | 60 +++++++++++++++++++++++++++++++++++++++++++--- README.template.md | 60 +++++++++++++++++++++++++++++++++++++++++++--- src/lib.rs | 45 +++++++++++++++++++++++++++++----- 4 files changed, 176 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b990fe7..627579c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,29 @@ All notable changes to this project will be documented in this file. ## [Unreleased] ### Added -- `#[error(transparent)]` support in the derive macro: validates wrapper shape, - delegates `Display`/`source` to the inner error, and works with `#[from]`. +- Re-exported `thiserror::Error` as `masterror::Error`, making it possible to + derive domain errors without an extra dependency. The derive supports + `#[from]` conversions, validates `#[error(transparent)]` wrappers, and mirrors + `thiserror`'s ergonomics. +- Added `BrowserConsoleError::context()` for retrieving browser-provided + diagnostics when console logging fails. + +### Changed +- README generation now pulls from crate metadata via the build script while + staying inert during `cargo package`, preventing dirty worktrees in release + workflows. + +### Documentation +- Documented deriving custom errors via `masterror::Error` and expanded the + browser console section with context-handling guidance. +- Added a release checklist and described the automated README sync process. + +### Tests +- Added regression tests covering derive behaviour (including `#[from]` and + transparent wrappers) and ensuring the README stays in sync with its + template. +- Added a guard test that enforces the `AppResult<_>` alias over raw + `Result<_, AppError>` usages within the crate. ## [0.4.0] - 2025-09-15 ### Added diff --git a/README.md b/README.md index 1912398..ec1421f 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ masterror = { version = "0.4.0", default-features = false } # ] } ~~~ +*Unreleased: derive custom errors via `#[derive(Error)]` (`use masterror::Error;`) and inspect browser logging failures with `BrowserConsoleError::context()`.* *Since v0.4.0: optional `frontend` feature for WASM/browser console logging.* *Since v0.3.0: stable `AppCode` enum and extended `ErrorResponse` with retry/authentication metadata.* @@ -47,8 +48,9 @@ masterror = { version = "0.4.0", default-features = false } - **Opt-in integrations.** Zero default features; you enable what you need. - **Clean wire contract.** `ErrorResponse { status, code, message, details?, retry?, www_authenticate? }`. - **One log at boundary.** Log once with `tracing`. -- **Less boilerplate.** Built-in conversions, compact prelude, - derive macro support for transparent wrappers via `#[error(transparent)]`. +- **Less boilerplate.** Built-in conversions, compact prelude, and the + `masterror::Error` re-export of `thiserror::Error` with `#[from]` / + `#[error(transparent)]` support. - **Consistent workspace.** Same error surface across crates. @@ -102,6 +104,49 @@ fn do_work(flag: bool) -> AppResult<()> { +
+ Derive custom errors + +~~~rust +use std::io; + +use masterror::Error; + +#[derive(Debug, Error)] +#[error("I/O failed: {source}")] +pub struct DomainError { + #[from] + #[source] + source: io::Error, +} + +#[derive(Debug, Error)] +#[error(transparent)] +pub struct WrappedDomainError( + #[from] + #[source] + DomainError +); + +fn load() -> Result<(), DomainError> { + Err(io::Error::other("disk offline").into()) +} + +let err = load().unwrap_err(); +assert_eq!(err.to_string(), "I/O failed: disk offline"); + +let wrapped = WrappedDomainError::from(err); +assert_eq!(wrapped.to_string(), "I/O failed: disk offline"); +~~~ + +- `use masterror::Error;` re-exports `thiserror::Error`. +- `#[from]` automatically implements `From<...>` while ensuring wrapper shapes are + valid. +- `#[error(transparent)]` enforces single-field wrappers that forward + `Display`/`source` to the inner error. + +
+
Error response payload @@ -131,7 +176,14 @@ assert_eq!(resp.status, 401); assert!(payload.is_object()); #[cfg(target_arch = "wasm32")] - err.log_to_browser_console()?; + { + if let Err(console_err) = err.log_to_browser_console() { + eprintln!( + "failed to log to browser console: {:?}", + console_err.context() + ); + } + } Ok(()) } @@ -139,6 +191,8 @@ assert_eq!(resp.status, 401); - On non-WASM targets `log_to_browser_console` returns `BrowserConsoleError::UnsupportedTarget`. +- `BrowserConsoleError::context()` exposes optional browser diagnostics for + logging/telemetry when console logging fails.
diff --git a/README.template.md b/README.template.md index 82dc5fc..735285a 100644 --- a/README.template.md +++ b/README.template.md @@ -31,6 +31,7 @@ masterror = { version = "{{CRATE_VERSION}}", default-features = false } # ] } ~~~ +*Unreleased: derive custom errors via `#[derive(Error)]` (`use masterror::Error;`) and inspect browser logging failures with `BrowserConsoleError::context()`.* *Since v0.4.0: optional `frontend` feature for WASM/browser console logging.* *Since v0.3.0: stable `AppCode` enum and extended `ErrorResponse` with retry/authentication metadata.* @@ -44,8 +45,9 @@ masterror = { version = "{{CRATE_VERSION}}", default-features = false } - **Opt-in integrations.** Zero default features; you enable what you need. - **Clean wire contract.** `ErrorResponse { status, code, message, details?, retry?, www_authenticate? }`. - **One log at boundary.** Log once with `tracing`. -- **Less boilerplate.** Built-in conversions, compact prelude, - derive macro support for transparent wrappers via `#[error(transparent)]`. +- **Less boilerplate.** Built-in conversions, compact prelude, and the + `masterror::Error` re-export of `thiserror::Error` with `#[from]` / + `#[error(transparent)]` support. - **Consistent workspace.** Same error surface across crates. @@ -96,6 +98,49 @@ fn do_work(flag: bool) -> AppResult<()> { +
+ Derive custom errors + +~~~rust +use std::io; + +use masterror::Error; + +#[derive(Debug, Error)] +#[error("I/O failed: {source}")] +pub struct DomainError { + #[from] + #[source] + source: io::Error, +} + +#[derive(Debug, Error)] +#[error(transparent)] +pub struct WrappedDomainError( + #[from] + #[source] + DomainError +); + +fn load() -> Result<(), DomainError> { + Err(io::Error::other("disk offline").into()) +} + +let err = load().unwrap_err(); +assert_eq!(err.to_string(), "I/O failed: disk offline"); + +let wrapped = WrappedDomainError::from(err); +assert_eq!(wrapped.to_string(), "I/O failed: disk offline"); +~~~ + +- `use masterror::Error;` re-exports `thiserror::Error`. +- `#[from]` automatically implements `From<...>` while ensuring wrapper shapes are + valid. +- `#[error(transparent)]` enforces single-field wrappers that forward + `Display`/`source` to the inner error. + +
+
Error response payload @@ -125,7 +170,14 @@ assert_eq!(resp.status, 401); assert!(payload.is_object()); #[cfg(target_arch = "wasm32")] - err.log_to_browser_console()?; + { + if let Err(console_err) = err.log_to_browser_console() { + eprintln!( + "failed to log to browser console: {:?}", + console_err.context() + ); + } + } Ok(()) } @@ -133,6 +185,8 @@ assert_eq!(resp.status, 401); - On non-WASM targets `log_to_browser_console` returns `BrowserConsoleError::UnsupportedTarget`. +- `BrowserConsoleError::context()` exposes optional browser diagnostics for + logging/telemetry when console logging fails.
diff --git a/src/lib.rs b/src/lib.rs index 8214c92..cbb406a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -203,8 +203,10 @@ pub use app_error::{AppError, AppResult}; pub use code::AppCode; pub use kind::AppErrorKind; pub use response::{ErrorResponse, RetryAdvice}; -#[cfg_attr(docsrs, doc(cfg(feature = "derive")))] -/// Derive macro replicating the ergonomics of `thiserror::Error`. +/// Derive macro re-export providing the same ergonomics as `thiserror::Error`. +/// +/// Supports `#[from]` conversions and `#[error(transparent)]` wrappers out of +/// the box while keeping compile-time validation of wrapper shapes. /// /// ``` /// use std::error::Error as StdError; @@ -218,11 +220,42 @@ pub use response::{ErrorResponse, RetryAdvice}; /// message: &'static str /// } /// -/// let err = MiniError { +/// #[derive(Debug, Error)] +/// #[error("wrapper -> {0}")] +/// struct MiniWrapper( +/// #[from] +/// #[source] +/// MiniError +/// ); +/// +/// #[derive(Debug, Error)] +/// #[error(transparent)] +/// struct MiniTransparent(#[from] MiniError); +/// +/// let wrapped = MiniWrapper::from(MiniError { /// code: 500, /// message: "boom" -/// }; -/// assert_eq!(err.to_string(), "500: boom"); -/// assert!(StdError::source(&err).is_none()); +/// }); +/// assert_eq!(wrapped.to_string(), "wrapper -> 500: boom"); +/// assert_eq!( +/// StdError::source(&wrapped).map(|err| err.to_string()), +/// Some(String::from("500: boom")) +/// ); +/// +/// let expected_source = StdError::source(&MiniError { +/// code: 503, +/// message: "oops" +/// }) +/// .map(|err| err.to_string()); +/// +/// let transparent = MiniTransparent::from(MiniError { +/// code: 503, +/// message: "oops" +/// }); +/// assert_eq!(transparent.to_string(), "503: oops"); +/// assert_eq!( +/// StdError::source(&transparent).map(|err| err.to_string()), +/// expected_source +/// ); /// ``` pub use thiserror::Error;