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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,17 @@ All notable changes to this project will be documented in this file.
### Added
- _Nothing yet._

## [0.5.8] - 2025-09-30

### Changed
- `masterror::Error` now infers sources named `source` and backtrace fields of
type `std::backtrace::Backtrace`/`Option<std::backtrace::Backtrace>` even
without explicit attributes, matching `thiserror`'s ergonomics.

### Tests
- Expanded derive tests to cover implicit `source`/`backtrace` detection across
structs and enums.

## [0.5.7] - 2025-09-29

### 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.5.7"
version = "0.5.8"
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.1.3", path = "masterror-derive" }
masterror-derive = { version = "0.1.4", path = "masterror-derive" }
masterror-template = { version = "0.1.2", path = "masterror-template" }

[dependencies]
Expand Down
14 changes: 7 additions & 7 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.5.7", default-features = false }
masterror = { version = "0.5.8", default-features = false }
# or with features:
# masterror = { version = "0.5.7", features = [
# masterror = { version = "0.5.8", 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.5.7", default-features = false }
~~~toml
[dependencies]
# lean core
masterror = { version = "0.5.7", default-features = false }
masterror = { version = "0.5.8", default-features = false }

# with Axum/Actix + JSON + integrations
# masterror = { version = "0.5.7", features = [
# masterror = { version = "0.5.8", features = [
# "axum", "actix", "openapi", "serde_json",
# "sqlx", "sqlx-migrate", "reqwest", "redis",
# "validator", "config", "tokio", "multipart",
Expand Down Expand Up @@ -342,13 +342,13 @@ assert_eq!(resp.status, 401);
Minimal core:

~~~toml
masterror = { version = "0.5.7", default-features = false }
masterror = { version = "0.5.8", default-features = false }
~~~

API (Axum + JSON + deps):

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

~~~toml
masterror = { version = "0.5.7", features = [
masterror = { version = "0.5.8", features = [
"actix", "serde_json", "openapi",
"sqlx", "reqwest", "redis", "validator", "config", "tokio"
] }
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.1.3"
version = "0.1.4"
edition = "2024"
license = "MIT OR Apache-2.0"
repository = "https://github.com/RAprogramm/masterror"
Expand Down
7 changes: 2 additions & 5 deletions masterror-derive/src/error_trait.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ fn struct_source_body(fields: &Fields, display: &DisplaySpec) -> TokenStream {
}
}
DisplaySpec::Template(_) => {
if let Some(field) = fields.iter().find(|field| field.attrs.source.is_some()) {
if let Some(field) = fields.iter().find(|field| field.attrs.has_source()) {
let member = &field.member;
field_source_expr(quote!(self.#member), quote!(&self.#member), &field.ty)
} else {
Expand Down Expand Up @@ -135,10 +135,7 @@ fn variant_transparent_source(variant: &VariantData) -> TokenStream {

fn variant_template_source(variant: &VariantData) -> TokenStream {
let variant_ident = &variant.ident;
let source_field = variant
.fields
.iter()
.find(|field| field.attrs.source.is_some());
let source_field = variant.fields.iter().find(|field| field.attrs.has_source());

match (&variant.fields, source_field) {
(Fields::Unit, _) => quote! { Self::#variant_ident => None },
Expand Down
4 changes: 2 additions & 2 deletions masterror-derive/src/from_impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,11 +127,11 @@ fn field_value_expr(field: &Field, from_field: &Field) -> Result<TokenStream, Er
return Ok(quote! { value });
}

if field.attrs.backtrace.is_some() {
if field.attrs.has_backtrace() {
return Ok(backtrace_initializer(field));
}

if field.attrs.source.is_some() && field.attrs.from.is_none() {
if field.attrs.has_source() && field.attrs.from.is_none() {
return source_initializer(field);
}

Expand Down
86 changes: 71 additions & 15 deletions masterror-derive/src/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ impl Fields {
}

pub fn backtrace_field(&self) -> Option<&Field> {
self.iter().find(|field| field.attrs.backtrace.is_some())
self.iter().find(|field| field.attrs.has_backtrace())
}
}

Expand Down Expand Up @@ -134,7 +134,7 @@ impl Field {
None => syn::Member::Unnamed(syn::Index::from(index))
};

let attrs = FieldAttrs::from_attrs(&field.attrs, errors);
let attrs = FieldAttrs::from_attrs(&field.attrs, ident.as_ref(), &field.ty, errors);

Self {
ident,
Expand All @@ -149,13 +149,20 @@ impl Field {

#[derive(Debug, Default)]
pub struct FieldAttrs {
pub from: Option<Attribute>,
pub source: Option<Attribute>,
pub backtrace: Option<Attribute>
pub from: Option<Attribute>,
pub source: Option<Attribute>,
pub backtrace: Option<Attribute>,
inferred_source: bool,
inferred_backtrace: bool
}

impl FieldAttrs {
fn from_attrs(attrs: &[Attribute], errors: &mut Vec<Error>) -> Self {
fn from_attrs(
attrs: &[Attribute],
ident: Option<&Ident>,
ty: &syn::Type,
errors: &mut Vec<Error>
) -> Self {
let mut result = FieldAttrs::default();

for attr in attrs {
Expand Down Expand Up @@ -198,8 +205,42 @@ impl FieldAttrs {
result.source = Some(attr.clone());
}

if result.source.is_none() && ident.is_some_and(|ident| ident == "source") {
result.inferred_source = true;
}

if result.backtrace.is_none() {
if is_option_type(ty) {
if option_inner_type(ty).is_some_and(is_backtrace_type) {
result.inferred_backtrace = true;
}
} else if is_backtrace_type(ty) {
result.inferred_backtrace = true;
}
}

result
}

pub fn has_source(&self) -> bool {
self.source.is_some() || self.inferred_source
}

pub fn has_backtrace(&self) -> bool {
self.backtrace.is_some() || self.inferred_backtrace
}

pub fn is_backtrace_inferred(&self) -> bool {
self.inferred_backtrace
}

pub fn source_attribute(&self) -> Option<&Attribute> {
self.source.as_ref()
}

pub fn backtrace_attribute(&self) -> Option<&Attribute> {
self.backtrace.as_ref()
}
}

#[derive(Debug)]
Expand Down Expand Up @@ -403,16 +444,23 @@ fn validate_from_usage(fields: &Fields, display: &DisplaySpec, errors: &mut Vec<
continue;
}

if companion.attrs.backtrace.is_some() {
if companion.attrs.has_backtrace() {
continue;
}

if let Some(attr) = &companion.attrs.source {
if companion.attrs.has_source() {
if companion.attrs.from.is_none() && !is_option_type(&companion.ty) {
errors.push(Error::new_spanned(
attr,
"additional #[source] fields used with #[from] must be Option<_>"
));
if let Some(attr) = companion.attrs.source_attribute() {
errors.push(Error::new_spanned(
attr,
"additional #[source] fields used with #[from] must be Option<_>"
));
} else {
errors.push(Error::new(
companion.span,
"additional #[source] fields used with #[from] must be Option<_>"
));
}
}
continue;
}
Expand Down Expand Up @@ -442,7 +490,7 @@ fn validate_from_usage(fields: &Fields, display: &DisplaySpec, errors: &mut Vec<
fn validate_backtrace_usage(fields: &Fields, errors: &mut Vec<Error>) {
let backtrace_fields: Vec<_> = fields
.iter()
.filter(|field| field.attrs.backtrace.is_some())
.filter(|field| field.attrs.has_backtrace())
.collect();

for field in &backtrace_fields {
Expand All @@ -454,17 +502,25 @@ fn validate_backtrace_usage(fields: &Fields, errors: &mut Vec<Error>) {
}

for field in backtrace_fields.iter().skip(1) {
if let Some(attr) = &field.attrs.backtrace {
if let Some(attr) = field.attrs.backtrace_attribute() {
errors.push(Error::new_spanned(
attr,
"multiple #[backtrace] fields are not supported"
));
} else {
errors.push(Error::new(
field.span,
"multiple #[backtrace] fields are not supported"
));
}
}
}

fn validate_backtrace_field_type(field: &Field, errors: &mut Vec<Error>) {
let Some(attr) = &field.attrs.backtrace else {
let Some(attr) = field.attrs.backtrace_attribute() else {
if field.attrs.is_backtrace_inferred() {
return;
}
return;
};

Expand Down
Loading
Loading