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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ All notable changes to this project will be documented in this file.
`#[error("...")]` strings and a formatter hook for future custom derives.
- Internal `masterror-derive` crate powering the native `masterror::Error`
derive macro.
- Template placeholders now accept the same formatter traits as `thiserror`
(`:?`, `:x`, `:X`, `:p`, `:b`, `:o`, `:e`, `:E`) so existing derives keep
compiling when hexadecimal, binary, pointer or exponential formatting is
requested.

### Changed
- `masterror::Error` now uses the in-tree derive, removing the dependency on
Expand Down
6 changes: 3 additions & 3 deletions Cargo.lock

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

6 changes: 3 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "masterror"
version = "0.5.2"
version = "0.5.3"
rust-version = "1.90"
edition = "2024"
description = "Application error types and response mapping"
Expand Down Expand Up @@ -35,8 +35,8 @@ turnkey = []

openapi = ["dep:utoipa"]
[workspace.dependencies]
masterror-derive = { version = "0.1", path = "masterror-derive" }
masterror-template = { version = "0.1", path = "masterror-template" }
masterror-derive = { version = "0.1.1", path = "masterror-derive" }
masterror-template = { version = "0.1.1", path = "masterror-template" }

[dependencies]
# masterror-derive = { path = "masterror-derive" }
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.2", default-features = false }
masterror = { version = "0.5.3", default-features = false }
# or with features:
# masterror = { version = "0.5.2", features = [
# masterror = { version = "0.5.3", features = [
# "axum", "actix", "openapi", "serde_json",
# "sqlx", "reqwest", "redis", "validator",
# "config", "tokio", "multipart", "teloxide",
Expand Down Expand Up @@ -66,10 +66,10 @@ masterror = { version = "0.5.2", default-features = false }
~~~toml
[dependencies]
# lean core
masterror = { version = "0.5.2", default-features = false }
masterror = { version = "0.5.3", default-features = false }

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

~~~toml
masterror = { version = "0.5.2", default-features = false }
masterror = { version = "0.5.3", default-features = false }
~~~

API (Axum + JSON + deps):

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

~~~toml
masterror = { version = "0.5.2", features = [
masterror = { version = "0.5.3", features = [
"actix", "serde_json", "openapi",
"sqlx", "reqwest", "redis", "validator", "config", "tokio"
] }
Expand Down
4 changes: 2 additions & 2 deletions 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.0"
version = "0.1.1"
edition = "2024"
license = "MIT OR Apache-2.0"

Expand All @@ -12,4 +12,4 @@ proc-macro = true
proc-macro2 = "1"
quote = "1"
syn = { version = "2", features = ["full", "extra-traits"] }
masterror-template = { path = "../masterror-template", version = "0.1" }
masterror-template = { path = "../masterror-template", version = "0.1.1" }
189 changes: 167 additions & 22 deletions masterror-derive/src/display.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,29 @@ fn render_variant(variant: &VariantData) -> Result<TokenStream, Error> {
}
}

#[derive(Debug)]
struct ResolvedPlaceholderExpr {
expr: TokenStream,
pointer_value: bool
}

impl ResolvedPlaceholderExpr {
fn new(expr: TokenStream) -> Self {
Self::with(expr, false)
}

fn pointer(expr: TokenStream) -> Self {
Self::with(expr, true)
}

fn with(expr: TokenStream, pointer_value: bool) -> Self {
Self {
expr,
pointer_value
}
}
}

fn render_variant_transparent(variant: &VariantData) -> Result<TokenStream, Error> {
let variant_ident = &variant.ident;

Expand Down Expand Up @@ -172,7 +195,7 @@ fn render_variant_template(

fn render_template<F>(template: &DisplayTemplate, resolver: F) -> Result<TokenStream, Error>
where
F: Fn(&TemplatePlaceholderSpec) -> Result<TokenStream, Error>
F: Fn(&TemplatePlaceholderSpec) -> Result<ResolvedPlaceholderExpr, Error>
{
let mut pieces = Vec::new();
for segment in &template.segments {
Expand All @@ -181,8 +204,8 @@ where
pieces.push(quote! { f.write_str(#text)?; });
}
TemplateSegmentSpec::Placeholder(placeholder) => {
let expr = resolver(placeholder)?;
pieces.push(format_placeholder(expr, placeholder.formatter));
let resolved = resolver(placeholder)?;
pieces.push(format_placeholder(resolved, placeholder.formatter));
}
}
}
Expand All @@ -196,40 +219,78 @@ where
fn struct_placeholder_expr(
fields: &Fields,
placeholder: &TemplatePlaceholderSpec
) -> Result<TokenStream, Error> {
) -> Result<ResolvedPlaceholderExpr, Error> {
match &placeholder.identifier {
TemplateIdentifierSpec::Named(name) if name == "self" => Ok(quote!(self)),
TemplateIdentifierSpec::Named(name) if name == "self" => {
Ok(ResolvedPlaceholderExpr::with(
quote!(self),
needs_pointer_value(placeholder.formatter)
))
}
TemplateIdentifierSpec::Named(name) => {
if let Some(field) = fields.get_named(name) {
let member = &field.member;
Ok(quote!(&self.#member))
} else {
Err(placeholder_error(placeholder.span, &placeholder.identifier))
}
}
TemplateIdentifierSpec::Positional(index) => {
if let Some(field) = fields.get_positional(*index) {
let member = &field.member;
Ok(quote!(&self.#member))
Ok(struct_field_expr(field, placeholder.formatter))
} else {
Err(placeholder_error(placeholder.span, &placeholder.identifier))
}
}
TemplateIdentifierSpec::Positional(index) => fields
.get_positional(*index)
.map(|field| struct_field_expr(field, placeholder.formatter))
.ok_or_else(|| placeholder_error(placeholder.span, &placeholder.identifier))
}
}

fn struct_field_expr(field: &Field, formatter: TemplateFormatter) -> ResolvedPlaceholderExpr {
let member = &field.member;

if needs_pointer_value(formatter) && pointer_prefers_value(&field.ty) {
ResolvedPlaceholderExpr::pointer(quote!(self.#member))
} else {
ResolvedPlaceholderExpr::new(quote!(&self.#member))
}
}

fn needs_pointer_value(formatter: TemplateFormatter) -> bool {
matches!(formatter, TemplateFormatter::Pointer { .. })
}

fn pointer_prefers_value(ty: &syn::Type) -> bool {
match ty {
syn::Type::Ptr(_) => true,
syn::Type::Reference(reference) => reference.mutability.is_none(),
syn::Type::Path(path) => path
.path
.segments
.last()
.map(|segment| segment.ident == "NonNull")
.unwrap_or(false),
_ => false
}
}

fn variant_tuple_placeholder(
bindings: &[Ident],
placeholder: &TemplatePlaceholderSpec
) -> Result<TokenStream, Error> {
) -> Result<ResolvedPlaceholderExpr, Error> {
match &placeholder.identifier {
TemplateIdentifierSpec::Named(name) if name == "self" => Ok(quote!(self)),
TemplateIdentifierSpec::Named(name) if name == "self" => {
Ok(ResolvedPlaceholderExpr::with(
quote!(self),
needs_pointer_value(placeholder.formatter)
))
}
TemplateIdentifierSpec::Named(_) => {
Err(placeholder_error(placeholder.span, &placeholder.identifier))
}
TemplateIdentifierSpec::Positional(index) => bindings
.get(*index)
.map(|binding| quote!(#binding))
.map(|binding| {
ResolvedPlaceholderExpr::with(
quote!(#binding),
needs_pointer_value(placeholder.formatter)
)
})
.ok_or_else(|| placeholder_error(placeholder.span, &placeholder.identifier))
}
}
Expand All @@ -238,16 +299,24 @@ fn variant_named_placeholder(
fields: &[Field],
bindings: &[Ident],
placeholder: &TemplatePlaceholderSpec
) -> Result<TokenStream, Error> {
) -> Result<ResolvedPlaceholderExpr, Error> {
match &placeholder.identifier {
TemplateIdentifierSpec::Named(name) if name == "self" => Ok(quote!(self)),
TemplateIdentifierSpec::Named(name) if name == "self" => {
Ok(ResolvedPlaceholderExpr::with(
quote!(self),
needs_pointer_value(placeholder.formatter)
))
}
TemplateIdentifierSpec::Named(name) => {
if let Some(index) = fields
.iter()
.position(|field| field.ident.as_ref().is_some_and(|ident| ident == name))
{
let binding = &bindings[index];
Ok(quote!(#binding))
Ok(ResolvedPlaceholderExpr::with(
quote!(#binding),
needs_pointer_value(placeholder.formatter)
))
} else {
Err(placeholder_error(placeholder.span, &placeholder.identifier))
}
Expand All @@ -259,7 +328,15 @@ fn variant_named_placeholder(
}
}

fn format_placeholder(expr: TokenStream, formatter: TemplateFormatter) -> TokenStream {
fn format_placeholder(
resolved: ResolvedPlaceholderExpr,
formatter: TemplateFormatter
) -> TokenStream {
let ResolvedPlaceholderExpr {
expr,
pointer_value
} = resolved;

match formatter {
TemplateFormatter::Display => quote! {
core::fmt::Display::fmt(#expr, f)?;
Expand All @@ -273,6 +350,74 @@ fn format_placeholder(expr: TokenStream, formatter: TemplateFormatter) -> TokenS
alternate: true
} => quote! {
write!(f, "{:#?}", #expr)?;
},
TemplateFormatter::LowerHex {
alternate
} => {
if alternate {
quote! { write!(f, "{:#x}", #expr)?; }
} else {
quote! { core::fmt::LowerHex::fmt(#expr, f)?; }
}
}
TemplateFormatter::UpperHex {
alternate
} => {
if alternate {
quote! { write!(f, "{:#X}", #expr)?; }
} else {
quote! { core::fmt::UpperHex::fmt(#expr, f)?; }
}
}
TemplateFormatter::Pointer {
alternate
} => {
if alternate {
quote! { write!(f, "{:#p}", #expr)?; }
} else if pointer_value {
quote! {{
let value = #expr;
core::fmt::Pointer::fmt(&value, f)?;
}}
} else {
quote! { core::fmt::Pointer::fmt(#expr, f)?; }
Comment on lines +372 to +383

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Pointer formatter prints field addresses instead of pointer values

The pointer branch in format_placeholder still hands core::fmt::Pointer a reference to the placeholder expression (&value) even when pointer_value is true. For raw pointers, immutable references, NonNull, or {self:p}, this means the generated Display impl prints the address of the local/field that stores the pointer instead of the pointer value itself—{ptr:p} formats like format!("{:p}", &ptr) rather than format!("{:p}", ptr). As a result, pointer placeholders yield stack/field addresses rather than the intended target pointer, diverging from Rust’s built‑in pointer formatting. The branch should format the pointer value directly without introducing another &.

Useful? React with 👍 / 👎.

}
}
TemplateFormatter::Binary {
alternate
} => {
if alternate {
quote! { write!(f, "{:#b}", #expr)?; }
} else {
quote! { core::fmt::Binary::fmt(#expr, f)?; }
}
}
TemplateFormatter::Octal {
alternate
} => {
if alternate {
quote! { write!(f, "{:#o}", #expr)?; }
} else {
quote! { core::fmt::Octal::fmt(#expr, f)?; }
}
}
TemplateFormatter::LowerExp {
alternate
} => {
if alternate {
quote! { write!(f, "{:#e}", #expr)?; }
} else {
quote! { core::fmt::LowerExp::fmt(#expr, f)?; }
}
}
TemplateFormatter::UpperExp {
alternate
} => {
if alternate {
quote! { write!(f, "{:#E}", #expr)?; }
} else {
quote! { core::fmt::UpperExp::fmt(#expr, f)?; }
}
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion masterror-template/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "masterror-template"
version = "0.1.0"
version = "0.1.1"
rust-version = "1.90"
edition = "2024"
license = "MIT OR Apache-2.0"
Expand Down
Loading
Loading