Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,23 @@ All notable changes to this project will be documented in this file.

## [Unreleased]

## [0.9.0] - 2025-10-20

### Added
- Parsed dot-prefixed display shorthands into a projection AST so `.limits.lo`,
`.0.data`, and chained method calls like `.suggestion.as_ref().map_or_else(...)`
resolve against struct fields and variant bindings.
- Extended the `error_derive` integration suite and trybuild fixtures with
regressions covering nested projections for named and tuple variants.

### Changed
- Shorthand resolution now builds expressions from the projection AST, preserving
raw identifiers, tuple indices, and method invocations when generating code.

### Documentation
- Documented the richer shorthand projection support in the README and template
so downstream users know complex field/method chains are available.

## [0.8.0] - 2025-10-14

### Added
Expand Down
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "masterror"
version = "0.8.0"
version = "0.9.0"
rust-version = "1.90"
edition = "2024"
license = "MIT OR Apache-2.0"
Expand Down Expand Up @@ -49,7 +49,7 @@ turnkey = []
openapi = ["dep:utoipa"]

[workspace.dependencies]
masterror-derive = { version = "0.4.0", path = "masterror-derive" }
masterror-derive = { version = "0.5.0", path = "masterror-derive" }
masterror-template = { version = "0.2.0", path = "masterror-template" }

[dependencies]
Expand Down
112 changes: 55 additions & 57 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ Stable categories, conservative HTTP mapping, no `unsafe`.

~~~toml
[dependencies]
masterror = { version = "0.8.0", default-features = false }
masterror = { version = "0.9.0", default-features = false }
# or with features:
# masterror = { version = "0.8.0", features = [
# masterror = { version = "0.9.0", features = [
# "axum", "actix", "openapi", "serde_json",
# "sqlx", "sqlx-migrate", "reqwest", "redis",
# "validator", "config", "tokio", "multipart",
Expand Down Expand Up @@ -66,10 +66,10 @@ masterror = { version = "0.8.0", default-features = false }
~~~toml
[dependencies]
# lean core
masterror = { version = "0.8.0", default-features = false }
masterror = { version = "0.9.0", default-features = false }

# with Axum/Actix + JSON + integrations
# masterror = { version = "0.8.0", features = [
# masterror = { version = "0.9.0", features = [
# "axum", "actix", "openapi", "serde_json",
# "sqlx", "sqlx-migrate", "reqwest", "redis",
# "validator", "config", "tokio", "multipart",
Expand Down Expand Up @@ -163,6 +163,54 @@ assert_eq!(wrapped.to_string(), "I/O failed: disk offline");
placeholder, making it easy to branch on the requested rendering behaviour
without manually matching every enum variant.

#### Display shorthand projections

`#[error("...")]` supports the same shorthand syntax as `thiserror` for
referencing fields with `.field` or `.0`. The derive now understands chained
segments, so projections like `.limits.lo`, `.0.data` or
`.suggestion.as_ref().map_or_else(...)` keep compiling unchanged. Raw
identifiers and tuple indices are preserved, ensuring keywords such as
`r#type` and tuple fields continue to work even when you call methods on the
projected value.

~~~rust
use masterror::Error;

#[derive(Debug)]
struct Limits {
lo: i32,
hi: i32,
}

#[derive(Debug, Error)]
#[error(
"range {lo}-{hi} suggestion {suggestion}",
lo = .limits.lo,
hi = .limits.hi,
suggestion = .suggestion.as_ref().map_or_else(|| "<none>", |s| s.as_str())
)]
struct RangeError {
limits: Limits,
suggestion: Option<String>,
}

#[derive(Debug)]
struct Payload {
data: &'static str,
}

#[derive(Debug, Error)]
enum UiError {
#[error("tuple data {data}", data = .0.data)]
Tuple(Payload),
#[error(
"named suggestion {value}",
value = .suggestion.as_ref().map_or_else(|| "<none>", |s| s.as_str())
)]
Named { suggestion: Option<String> },
}
~~~

#### AppError conversions

Annotating structs or enum variants with `#[app_error(...)]` captures the
Expand Down Expand Up @@ -211,56 +259,6 @@ let as_app: AppError = missing.into();
assert_eq!(as_app.message.as_deref(), Some("missing resource 7"));
~~~

#### Provide custom telemetry

Fields can expose structured telemetry to `std::error::Request` consumers by
annotating them with `#[provide(...)]`. The attribute accepts `ref = <Type>` to
publish borrowed values via `Request::provide_ref` and `value = <Type>` to clone
and forward owned data with `Request::provide_value`. Both specifiers are
optional, enabling one field to provide different representations. Optional
fields only emit telemetry when they contain a value, mirroring the
backtrace-handling behaviour of `thiserror`.

When the crate is compiled with `--cfg error_generic_member_access` (required by
the current unstable `std::error::request_*` helpers), the derive generates a
`provide` implementation that forwards the `Request` to any `#[source]` field
before yielding the annotated telemetry:

~~~rust
use masterror::Error;

#[derive(Clone, Debug, PartialEq, Eq)]
struct Snapshot(&'static str);

#[derive(Debug, Error)]
#[error("snapshot {0:?}")]
struct SnapshotError(
#[provide(ref = Snapshot, value = Snapshot)]
Snapshot
);

#[derive(Debug, Error)]
#[error("optional telemetry {0:?}")]
struct MaybeSnapshot(
#[provide(ref = Snapshot)]
Option<Snapshot>
);

#[cfg(error_generic_member_access)]
{
let err = SnapshotError(Snapshot("trace"));
let borrowed = std::error::request_ref::<Snapshot>(&err).unwrap();
assert_eq!(borrowed, &Snapshot("trace"));
let owned = std::error::request_value::<Snapshot>(&err).unwrap();
assert_eq!(owned, Snapshot("trace"));

let maybe = MaybeSnapshot(Some(Snapshot("span")));
assert!(std::error::request_ref::<Snapshot>(&maybe).is_some());
let empty = MaybeSnapshot(None);
assert!(std::error::request_ref::<Snapshot>(&empty).is_none());
}
~~~

#### Formatter traits

Placeholders default to `Display` (`{value}`) but can opt into richer
Expand Down Expand Up @@ -485,13 +483,13 @@ assert_eq!(resp.status, 401);
Minimal core:

~~~toml
masterror = { version = "0.8.0", default-features = false }
masterror = { version = "0.9.0", default-features = false }
~~~

API (Axum + JSON + deps):

~~~toml
masterror = { version = "0.8.0", features = [
masterror = { version = "0.9.0", features = [
"axum", "serde_json", "openapi",
"sqlx", "reqwest", "redis", "validator", "config", "tokio"
] }
Expand All @@ -500,7 +498,7 @@ masterror = { version = "0.8.0", features = [
API (Actix + JSON + deps):

~~~toml
masterror = { version = "0.8.0", features = [
masterror = { version = "0.9.0", features = [
"actix", "serde_json", "openapi",
"sqlx", "reqwest", "redis", "validator", "config", "tokio"
] }
Expand Down
48 changes: 48 additions & 0 deletions README.template.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,54 @@ assert_eq!(wrapped.to_string(), "I/O failed: disk offline");
placeholder, making it easy to branch on the requested rendering behaviour
without manually matching every enum variant.

#### Display shorthand projections

`#[error("...")]` supports the same shorthand syntax as `thiserror` for
referencing fields with `.field` or `.0`. The derive now understands chained
segments, so projections like `.limits.lo`, `.0.data` or
`.suggestion.as_ref().map_or_else(...)` keep compiling unchanged. Raw
identifiers and tuple indices are preserved, ensuring keywords such as
`r#type` and tuple fields continue to work even when you call methods on the
projected value.

~~~rust
use masterror::Error;

#[derive(Debug)]
struct Limits {
lo: i32,
hi: i32,
}

#[derive(Debug, Error)]
#[error(
"range {lo}-{hi} suggestion {suggestion}",
lo = .limits.lo,
hi = .limits.hi,
suggestion = .suggestion.as_ref().map_or_else(|| "<none>", |s| s.as_str())
)]
struct RangeError {
limits: Limits,
suggestion: Option<String>,
}

#[derive(Debug)]
struct Payload {
data: &'static str,
}

#[derive(Debug, Error)]
enum UiError {
#[error("tuple data {data}", data = .0.data)]
Tuple(Payload),
#[error(
"named suggestion {value}",
value = .suggestion.as_ref().map_or_else(|| "<none>", |s| s.as_str())
)]
Named { suggestion: Option<String> },
}
~~~

#### AppError conversions

Annotating structs or enum variants with `#[app_error(...)]` captures the
Expand Down
2 changes: 1 addition & 1 deletion masterror-derive/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "masterror-derive"
rust-version = "1.90"
version = "0.4.0"
version = "0.5.0"
edition = "2024"
license = "MIT OR Apache-2.0"
repository = "https://github.com/RAprogramm/masterror"
Expand Down
Loading
Loading