diff --git a/CHANGELOG.md b/CHANGELOG.md index 72e2b54..9fb6121 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,27 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +## [0.8.0] - 2025-10-14 + +### Added +- Recognised `#[provide(ref = ..., value = ...)]` on struct and enum fields, + allowing derived errors to surface domain telemetry through + `std::error::Request` alongside backtraces. + +### Changed +- `masterror-derive` now generates `provide` implementations whenever custom + telemetry is requested, forwarding `Request` values to sources and invoking + `provide_ref`/`provide_value` with proper `Option` handling. + +### Tests +- Extended the `error_derive` integration suite with regressions covering + telemetry provided by structs, tuple variants and optional fields, including + both reference and owned payloads. + +### Documentation +- Documented the `#[provide(...)]` attribute in the README with examples showing + reference and owned telemetry as well as optional fields. + ## [0.7.0] - 2025-10-13 ### Added diff --git a/Cargo.lock b/Cargo.lock index 120035c..9152a19 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1527,7 +1527,7 @@ dependencies = [ [[package]] name = "masterror" -version = "0.7.0" +version = "0.8.0" dependencies = [ "actix-web", "axum", @@ -1557,7 +1557,7 @@ dependencies = [ [[package]] name = "masterror-derive" -version = "0.3.0" +version = "0.4.0" dependencies = [ "masterror-template", "proc-macro2", diff --git a/Cargo.toml b/Cargo.toml index 7511b74..262e39a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "masterror" -version = "0.7.0" +version = "0.8.0" rust-version = "1.90" edition = "2024" license = "MIT OR Apache-2.0" @@ -49,7 +49,7 @@ turnkey = [] openapi = ["dep:utoipa"] [workspace.dependencies] -masterror-derive = { version = "0.3.0", path = "masterror-derive" } +masterror-derive = { version = "0.4.0", path = "masterror-derive" } masterror-template = { version = "0.2.0", path = "masterror-template" } [dependencies] diff --git a/README.md b/README.md index 23a3424..9b62ef7 100644 --- a/README.md +++ b/README.md @@ -29,9 +29,9 @@ Stable categories, conservative HTTP mapping, no `unsafe`. ~~~toml [dependencies] -masterror = { version = "0.7.0", default-features = false } +masterror = { version = "0.8.0", default-features = false } # or with features: -# masterror = { version = "0.7.0", features = [ +# masterror = { version = "0.8.0", features = [ # "axum", "actix", "openapi", "serde_json", # "sqlx", "sqlx-migrate", "reqwest", "redis", # "validator", "config", "tokio", "multipart", @@ -66,10 +66,10 @@ masterror = { version = "0.7.0", default-features = false } ~~~toml [dependencies] # lean core -masterror = { version = "0.7.0", default-features = false } +masterror = { version = "0.8.0", default-features = false } # with Axum/Actix + JSON + integrations -# masterror = { version = "0.7.0", features = [ +# masterror = { version = "0.8.0", features = [ # "axum", "actix", "openapi", "serde_json", # "sqlx", "sqlx-migrate", "reqwest", "redis", # "validator", "config", "tokio", "multipart", @@ -211,6 +211,56 @@ 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 = ` to +publish borrowed values via `Request::provide_ref` and `value = ` 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 +); + +#[cfg(error_generic_member_access)] +{ + let err = SnapshotError(Snapshot("trace")); + let borrowed = std::error::request_ref::(&err).unwrap(); + assert_eq!(borrowed, &Snapshot("trace")); + let owned = std::error::request_value::(&err).unwrap(); + assert_eq!(owned, Snapshot("trace")); + + let maybe = MaybeSnapshot(Some(Snapshot("span"))); + assert!(std::error::request_ref::(&maybe).is_some()); + let empty = MaybeSnapshot(None); + assert!(std::error::request_ref::(&empty).is_none()); +} +~~~ + #### Formatter traits Placeholders default to `Display` (`{value}`) but can opt into richer @@ -435,13 +485,13 @@ assert_eq!(resp.status, 401); Minimal core: ~~~toml -masterror = { version = "0.7.0", default-features = false } +masterror = { version = "0.8.0", default-features = false } ~~~ API (Axum + JSON + deps): ~~~toml -masterror = { version = "0.7.0", features = [ +masterror = { version = "0.8.0", features = [ "axum", "serde_json", "openapi", "sqlx", "reqwest", "redis", "validator", "config", "tokio" ] } @@ -450,7 +500,7 @@ masterror = { version = "0.7.0", features = [ API (Actix + JSON + deps): ~~~toml -masterror = { version = "0.7.0", features = [ +masterror = { version = "0.8.0", features = [ "actix", "serde_json", "openapi", "sqlx", "reqwest", "redis", "validator", "config", "tokio" ] } diff --git a/masterror-derive/Cargo.toml b/masterror-derive/Cargo.toml index bdf3dda..c253ce5 100644 --- a/masterror-derive/Cargo.toml +++ b/masterror-derive/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "masterror-derive" rust-version = "1.90" -version = "0.3.0" +version = "0.4.0" edition = "2024" license = "MIT OR Apache-2.0" repository = "https://github.com/RAprogramm/masterror" diff --git a/masterror-derive/src/error_trait.rs b/masterror-derive/src/error_trait.rs index 88cf9da..1eb6f34 100644 --- a/masterror-derive/src/error_trait.rs +++ b/masterror-derive/src/error_trait.rs @@ -1,10 +1,10 @@ use proc_macro2::{Ident, TokenStream}; use quote::{format_ident, quote}; -use syn::Error; +use syn::{Error, TypePath}; use crate::input::{ BacktraceField, BacktraceFieldKind, DisplaySpec, ErrorData, ErrorInput, Field, Fields, - StructData, VariantData, is_backtrace_storage, is_option_type + ProvideSpec, StructData, VariantData, is_backtrace_storage, is_option_type }; pub fn expand(input: &ErrorInput) -> Result { @@ -315,15 +315,16 @@ fn field_backtrace_expr( } fn struct_provide_method(fields: &Fields) -> Option { - let backtrace = fields.backtrace_field()?; - let field = backtrace.field(); + let backtrace = fields.backtrace_field(); + let source_field = fields.iter().find(|candidate| candidate.attrs.has_source()); let request = quote!(request); - let delegates_to_source = - matches!(backtrace.kind(), BacktraceFieldKind::Explicit) && !backtrace.stores_backtrace(); + let delegates_to_source = backtrace.is_some_and(|backtrace| { + matches!(backtrace.kind(), BacktraceFieldKind::Explicit) && !backtrace.stores_backtrace() + }); let mut statements = Vec::new(); let mut needs_trait_import = false; - if let Some(source_field) = fields.iter().find(|candidate| candidate.attrs.has_source()) { + if let Some(source_field) = source_field { needs_trait_import = true; let member = &source_field.member; statements.push(provide_source_tokens( @@ -331,25 +332,31 @@ fn struct_provide_method(fields: &Fields) -> Option { source_field, &request )); + } + if let Some(backtrace) = backtrace { if backtrace.stores_backtrace() - && source_field.index != backtrace.index() + && !source_field.is_some_and(|source| source.index == backtrace.index()) && !delegates_to_source { - let member = &field.member; + let member = &backtrace.field().member; statements.push(provide_backtrace_tokens( quote!(self.#member), - field, + backtrace.field(), &request )); } - } else if backtrace.stores_backtrace() && !delegates_to_source { + } + + for field in fields.iter() { + if field.attrs.provides.is_empty() { + continue; + } let member = &field.member; - statements.push(provide_backtrace_tokens( - quote!(self.#member), - field, - &request - )); + let expr = quote!(self.#member); + for spec in &field.attrs.provides { + statements.extend(provide_custom_tokens(expr.clone(), field, spec, &request)); + } } if statements.is_empty() { @@ -373,6 +380,7 @@ fn struct_provide_method(fields: &Fields) -> Option { fn enum_provide_method(variants: &[VariantData]) -> Option { let mut has_backtrace = false; + let mut has_custom_provides = false; let mut needs_trait_import = false; let mut arms = Vec::new(); let request = quote!(request); @@ -381,6 +389,13 @@ fn enum_provide_method(variants: &[VariantData]) -> Option { if variant.fields.backtrace_field().is_some() { has_backtrace = true; } + if variant + .fields + .iter() + .any(|field| !field.attrs.provides.is_empty()) + { + has_custom_provides = true; + } arms.push(variant_provide_arm_tokens( variant, &request, @@ -388,7 +403,7 @@ fn enum_provide_method(variants: &[VariantData]) -> Option { )); } - if !has_backtrace { + if !has_backtrace && !has_custom_provides { return None; } @@ -419,71 +434,70 @@ fn variant_provide_arm_tokens( let backtrace = variant.fields.backtrace_field(); let source_field = variant.fields.iter().find(|field| field.attrs.has_source()); - match (&variant.fields, backtrace) { - (Fields::Unit, _) => quote! { Self::#variant_ident => {} }, - (Fields::Named(fields), Some(backtrace_field)) => variant_provide_named_arm( + match &variant.fields { + Fields::Unit => quote! { Self::#variant_ident => {} }, + Fields::Named(fields) => variant_provide_named_arm( variant_ident, fields, - backtrace_field, + backtrace, source_field, request, needs_trait_import ), - (Fields::Unnamed(fields), Some(backtrace_field)) => variant_provide_unnamed_arm( + Fields::Unnamed(fields) => variant_provide_unnamed_arm( variant_ident, fields, - backtrace_field, + backtrace, source_field, request, needs_trait_import - ), - (Fields::Named(fields), None) => { - let mut entries = Vec::new(); - for field in fields { - let ident = field.ident.clone().expect("named field"); - entries.push(quote!(#ident: _)); - } - quote! { Self::#variant_ident { #(#entries),* } => {} } - } - (Fields::Unnamed(fields), None) => { - if fields.is_empty() { - quote! { Self::#variant_ident() => {} } - } else { - let placeholders = vec![quote!(_); fields.len()]; - quote! { Self::#variant_ident(#(#placeholders),*) => {} } - } - } + ) } } fn variant_provide_named_arm( variant_ident: &Ident, fields: &[Field], - backtrace: BacktraceField<'_>, + backtrace: Option>, source: Option<&Field>, request: &TokenStream, needs_trait_import: &mut bool ) -> TokenStream { - let same_as_source = source.is_some_and(|field| field.index == backtrace.index()); - let delegates_to_source = - matches!(backtrace.kind(), BacktraceFieldKind::Explicit) && !backtrace.stores_backtrace(); + let same_as_source = if let (Some(backtrace_field), Some(source_field)) = (backtrace, source) { + source_field.index == backtrace_field.index() + } else { + false + }; + let delegates_to_source = backtrace.is_some_and(|field| { + matches!(field.kind(), BacktraceFieldKind::Explicit) && !field.stores_backtrace() + }); let mut entries = Vec::new(); let mut backtrace_binding = None; let mut source_binding = None; + let mut provide_bindings: Vec<(Ident, &Field)> = Vec::new(); for field in fields { let ident = field.ident.clone().expect("named field"); - if field.index == backtrace.index() { + let needs_binding = backtrace.is_some_and(|candidate| candidate.index() == field.index) + || source.is_some_and(|candidate| candidate.index == field.index) + || !field.attrs.provides.is_empty(); + + if needs_binding { let binding = binding_ident(field); - entries.push(quote!(#ident: #binding)); - backtrace_binding = Some(binding.clone()); - if same_as_source { - source_binding = Some(binding); + let pattern_binding = binding.clone(); + entries.push(quote!(#ident: #pattern_binding)); + + if backtrace.is_some_and(|candidate| candidate.index() == field.index) { + backtrace_binding = Some(binding.clone()); + } + + if source.is_some_and(|candidate| candidate.index == field.index) { + source_binding = Some(binding.clone()); + } + + if !field.attrs.provides.is_empty() { + provide_bindings.push((binding, field)); } - } else if source.is_some_and(|candidate| candidate.index == field.index) { - let binding = binding_ident(field); - entries.push(quote!(#ident: #binding)); - source_binding = Some(binding); } else { entries.push(quote!(#ident: _)); } @@ -501,13 +515,27 @@ fn variant_provide_named_arm( )); } - if backtrace.stores_backtrace() && !same_as_source && !delegates_to_source { - let binding = backtrace_binding.expect("backtrace binding"); - statements.push(provide_backtrace_tokens( - quote!(#binding), - backtrace.field(), - request - )); + if let Some(backtrace_field) = backtrace { + if backtrace_field.stores_backtrace() && !same_as_source && !delegates_to_source { + let binding = backtrace_binding.expect("backtrace binding"); + statements.push(provide_backtrace_tokens( + quote!(#binding), + backtrace_field.field(), + request + )); + } + } + + for (binding, field) in provide_bindings { + let binding_expr = quote!(#binding); + for spec in &field.attrs.provides { + statements.extend(provide_custom_tokens( + binding_expr.clone(), + field, + spec, + request + )); + } } let pattern = quote!(Self::#variant_ident { #(#entries),* }); @@ -522,30 +550,45 @@ fn variant_provide_named_arm( fn variant_provide_unnamed_arm( variant_ident: &Ident, fields: &[Field], - backtrace: BacktraceField<'_>, + backtrace: Option>, source: Option<&Field>, request: &TokenStream, needs_trait_import: &mut bool ) -> TokenStream { - let same_as_source = source.is_some_and(|field| field.index == backtrace.index()); - let delegates_to_source = - matches!(backtrace.kind(), BacktraceFieldKind::Explicit) && !backtrace.stores_backtrace(); + let same_as_source = if let (Some(backtrace_field), Some(source_field)) = (backtrace, source) { + source_field.index == backtrace_field.index() + } else { + false + }; + let delegates_to_source = backtrace.is_some_and(|field| { + matches!(field.kind(), BacktraceFieldKind::Explicit) && !field.stores_backtrace() + }); let mut elements = Vec::new(); let mut backtrace_binding = None; let mut source_binding = None; + let mut provide_bindings: Vec<(Ident, &Field)> = Vec::new(); for (index, field) in fields.iter().enumerate() { - if index == backtrace.index() { + let needs_binding = backtrace.is_some_and(|candidate| candidate.index() == index) + || source.is_some_and(|candidate| candidate.index == index) + || !field.attrs.provides.is_empty(); + + if needs_binding { let binding = binding_ident(field); - elements.push(quote!(#binding)); - backtrace_binding = Some(binding.clone()); - if same_as_source { - source_binding = Some(binding); + let pattern_binding = binding.clone(); + elements.push(quote!(#pattern_binding)); + + if backtrace.is_some_and(|candidate| candidate.index() == index) { + backtrace_binding = Some(binding.clone()); + } + + if source.is_some_and(|candidate| candidate.index == index) { + source_binding = Some(binding.clone()); + } + + if !field.attrs.provides.is_empty() { + provide_bindings.push((binding, field)); } - } else if source.is_some_and(|candidate| candidate.index == index) { - let binding = binding_ident(field); - elements.push(quote!(#binding)); - source_binding = Some(binding); } else { elements.push(quote!(_)); } @@ -563,13 +606,27 @@ fn variant_provide_unnamed_arm( )); } - if backtrace.stores_backtrace() && !same_as_source && !delegates_to_source { - let binding = backtrace_binding.expect("backtrace binding"); - statements.push(provide_backtrace_tokens( - quote!(#binding), - backtrace.field(), - request - )); + if let Some(backtrace_field) = backtrace { + if backtrace_field.stores_backtrace() && !same_as_source && !delegates_to_source { + let binding = backtrace_binding.expect("backtrace binding"); + statements.push(provide_backtrace_tokens( + quote!(#binding), + backtrace_field.field(), + request + )); + } + } + + for (binding, field) in provide_bindings { + let binding_expr = quote!(#binding); + for spec in &field.attrs.provides { + statements.extend(provide_custom_tokens( + binding_expr.clone(), + field, + spec, + request + )); + } } let pattern = if elements.is_empty() { @@ -585,6 +642,70 @@ fn variant_provide_unnamed_arm( } } +fn provide_custom_tokens( + expr: TokenStream, + field: &Field, + spec: &ProvideSpec, + request: &TokenStream +) -> Vec { + let mut tokens = Vec::new(); + if let Some(reference) = &spec.reference { + tokens.push(provide_custom_ref_tokens( + expr.clone(), + field, + reference, + request + )); + } + if let Some(value) = &spec.value { + tokens.push(provide_custom_value_tokens( + expr.clone(), + field, + value, + request + )); + } + tokens +} + +fn provide_custom_ref_tokens( + expr: TokenStream, + field: &Field, + ty: &TypePath, + request: &TokenStream +) -> TokenStream { + if is_option_type(&field.ty) { + quote! { + if let Some(value) = #expr.as_ref() { + #request.provide_ref::<#ty>(value); + } + } + } else { + quote! { + #request.provide_ref::<#ty>(#expr); + } + } +} + +fn provide_custom_value_tokens( + expr: TokenStream, + field: &Field, + ty: &TypePath, + request: &TokenStream +) -> TokenStream { + if is_option_type(&field.ty) { + quote! { + if let Some(value) = #expr.clone() { + #request.provide_value::<#ty>(value); + } + } + } else { + quote! { + #request.provide_value::<#ty>(#expr.clone()); + } + } +} + fn provide_backtrace_tokens( expr: TokenStream, field: &Field, diff --git a/masterror-derive/src/input.rs b/masterror-derive/src/input.rs index 8b05214..43593ba 100644 --- a/masterror-derive/src/input.rs +++ b/masterror-derive/src/input.rs @@ -3,7 +3,8 @@ use std::collections::HashSet; use proc_macro2::Span; use syn::{ Attribute, Data, DataEnum, DataStruct, DeriveInput, Error, Expr, ExprPath, Field as SynField, - Fields as SynFields, GenericArgument, Ident, LitBool, LitInt, LitStr, Token, + Fields as SynFields, GenericArgument, Ident, LitBool, LitInt, LitStr, Token, TypePath, + ext::IdentExt, parse::{Parse, ParseStream}, spanned::Spanned }; @@ -212,10 +213,17 @@ pub struct FieldAttrs { pub from: Option, pub source: Option, pub backtrace: Option, + pub provides: Vec, inferred_source: bool, inferred_backtrace: bool } +#[derive(Debug)] +pub struct ProvideSpec { + pub reference: Option, + pub value: Option +} + impl FieldAttrs { fn from_attrs( attrs: &[Attribute], @@ -256,6 +264,11 @@ impl FieldAttrs { continue; } result.backtrace = Some(attr.clone()); + } else if path_is(attr, "provide") { + match parse_provide_attribute(attr) { + Ok(spec) => result.provides.push(spec), + Err(err) => errors.push(err) + } } } @@ -309,6 +322,63 @@ impl FieldAttrs { } } +fn parse_provide_attribute(attr: &Attribute) -> Result { + attr.parse_args_with(|input: ParseStream| { + let mut reference = None; + let mut value = None; + + while !input.is_empty() { + let ident: Ident = input.call(Ident::parse_any)?; + let name = ident.to_string(); + match name.as_str() { + "ref" => { + if reference.is_some() { + return Err(Error::new(ident.span(), "duplicate `ref` specification")); + } + input.parse::()?; + let ty: TypePath = input.parse()?; + reference = Some(ty); + } + "value" => { + if value.is_some() { + return Err(Error::new(ident.span(), "duplicate `value` specification")); + } + input.parse::()?; + let ty: TypePath = input.parse()?; + value = Some(ty); + } + other => { + return Err(Error::new( + ident.span(), + format!("unknown #[provide] option `{}`", other) + )); + } + } + + if input.peek(Token![,]) { + input.parse::()?; + } else if !input.is_empty() { + return Err(Error::new( + input.span(), + "expected `,` or end of input in #[provide(...)]" + )); + } + } + + if reference.is_none() && value.is_none() { + return Err(Error::new( + attr.span(), + "`#[provide]` requires at least one of `ref = ...` or `value = ...`" + )); + } + + Ok(ProvideSpec { + reference, + value + }) + }) +} + #[derive(Debug)] pub enum DisplaySpec { Transparent { diff --git a/masterror-derive/src/lib.rs b/masterror-derive/src/lib.rs index 1682aa1..cffacd7 100644 --- a/masterror-derive/src/lib.rs +++ b/masterror-derive/src/lib.rs @@ -15,7 +15,7 @@ use proc_macro::TokenStream; use quote::quote; use syn::{DeriveInput, Error, parse_macro_input}; -#[proc_macro_derive(Error, attributes(error, source, from, backtrace, app_error))] +#[proc_macro_derive(Error, attributes(error, source, from, backtrace, app_error, provide))] pub fn derive_error(tokens: TokenStream) -> TokenStream { let input = parse_macro_input!(tokens as DeriveInput); match expand(input) { diff --git a/tests/error_derive.rs b/tests/error_derive.rs index ea51f56..8398406 100644 --- a/tests/error_derive.rs +++ b/tests/error_derive.rs @@ -150,6 +150,47 @@ enum EnumWithBacktrace { Unit } +#[derive(Clone, Debug, PartialEq, Eq)] +struct TelemetrySnapshot { + name: &'static str, + value: u64 +} + +#[derive(Debug, Error)] +#[error("structured telemetry {snapshot:?}")] +struct StructuredTelemetryError { + #[provide(ref = TelemetrySnapshot, value = TelemetrySnapshot)] + snapshot: TelemetrySnapshot +} + +#[derive(Debug, Error)] +#[error("optional telemetry {telemetry:?}")] +struct OptionalTelemetryError { + #[provide(ref = TelemetrySnapshot)] + telemetry: Option +} + +#[derive(Debug, Error)] +#[error("optional owned telemetry {telemetry:?}")] +struct OptionalOwnedTelemetryError { + #[provide(value = TelemetrySnapshot)] + telemetry: Option +} + +#[derive(Debug, Error)] +enum EnumTelemetryError { + #[error("named {label}")] + Named { + label: &'static str, + #[provide(ref = TelemetrySnapshot)] + snapshot: TelemetrySnapshot + }, + #[error("optional tuple")] + Optional(#[provide(ref = TelemetrySnapshot)] Option), + #[error("owned tuple")] + Owned(#[provide(value = TelemetrySnapshot)] TelemetrySnapshot) +} + #[derive(Debug, Error)] #[error("{source:?}")] struct DelegatedBacktraceFromSource { @@ -349,6 +390,97 @@ where { } +#[cfg(error_generic_member_access)] +#[test] +fn struct_provides_custom_telemetry() { + let telemetry = TelemetrySnapshot { + name: "job", + value: 7 + }; + let err = StructuredTelemetryError { + snapshot: telemetry.clone() + }; + + let provided_ref = + std::error::request_ref::(&err).expect("telemetry reference"); + assert!(ptr::eq(provided_ref, &err.snapshot)); + + let provided_value = + std::error::request_value::(&err).expect("telemetry value"); + assert_eq!(provided_value, telemetry); +} + +#[cfg(error_generic_member_access)] +#[test] +fn option_telemetry_only_provided_when_present() { + let snapshot = TelemetrySnapshot { + name: "task", + value: 13 + }; + + let with_value = OptionalTelemetryError { + telemetry: Some(snapshot.clone()) + }; + let provided = + std::error::request_ref::(&with_value).expect("optional telemetry"); + let inner = with_value.telemetry.as_ref().expect("inner telemetry"); + assert!(ptr::eq(provided, inner)); + + let without = OptionalTelemetryError { + telemetry: None + }; + assert!(std::error::request_ref::(&without).is_none()); + + let owned_value = OptionalOwnedTelemetryError { + telemetry: Some(snapshot.clone()) + }; + let provided_owned = + std::error::request_value::(&owned_value).expect("owned telemetry"); + assert_eq!(provided_owned, snapshot); + + let owned_none = OptionalOwnedTelemetryError { + telemetry: None + }; + assert!(std::error::request_value::(&owned_none).is_none()); +} + +#[cfg(error_generic_member_access)] +#[test] +fn enum_variants_provide_custom_telemetry() { + let named_snapshot = TelemetrySnapshot { + name: "span", + value: 21 + }; + + let named = EnumTelemetryError::Named { + label: "named", + snapshot: named_snapshot.clone() + }; + let provided_named = + std::error::request_ref::(&named).expect("named telemetry"); + if let EnumTelemetryError::Named { + snapshot, .. + } = &named + { + assert!(ptr::eq(provided_named, snapshot)); + } + + let optional = EnumTelemetryError::Optional(Some(named_snapshot.clone())); + let provided_optional = + std::error::request_ref::(&optional).expect("optional telemetry"); + if let EnumTelemetryError::Optional(Some(snapshot)) = &optional { + assert!(ptr::eq(provided_optional, snapshot)); + } + + let optional_none = EnumTelemetryError::Optional(None); + assert!(std::error::request_ref::(&optional_none).is_none()); + + let owned = EnumTelemetryError::Owned(named_snapshot.clone()); + let provided_owned = + std::error::request_value::(&owned).expect("owned telemetry"); + assert_eq!(provided_owned, named_snapshot); +} + #[test] fn named_struct_display_and_source() { let err = NamedError {