use quote::quote;
use syn::{Data, DeriveInput, Expr, Fields, Result};
use crate::derive_error::attributes::DisplayAttribute;
use crate::utils::bail;
pub fn parse_display_template(display_attr: &DisplayAttribute, input: &DeriveInput) -> Result<proc_macro2::TokenStream> {
let mut result = String::new();
let mut chars = display_attr.template.chars().peekable();
let mut format_args = Vec::new();
let mut arg_index = 0;
while let Some(ch) = chars.next() {
match ch {
'{' => {
if chars.peek() == Some(&'{') {
chars.next();
result.push_str("{{");
} else {
let (field_name, format_spec) = parse_field_reference(&mut chars);
let format_str = if format_spec.is_empty() {
"{}".to_string()
} else {
format!("{{:{format_spec}}}")
};
result.push_str(&format_str);
if field_name.is_empty() {
if arg_index >= display_attr.args.len() {
bail!("Not enough arguments for format placeholders");
}
let arg = &display_attr.args[arg_index];
let arg_tokens = convert_expr_to_field_access(arg);
format_args.push(arg_tokens);
arg_index += 1;
} else {
validate_field_exists(&field_name, input)?;
let field_ident = syn::Ident::new(&field_name, proc_macro2::Span::call_site());
format_args.push(quote! { &self.#field_ident });
}
}
}
_ => result.push(ch),
}
}
if arg_index != display_attr.args.len() {
bail!("Too many arguments for format placeholders");
}
Ok(generate_display_expression(&result, &format_args))
}
fn convert_expr_to_field_access(expr: &Expr) -> proc_macro2::TokenStream {
quote! { &self.#expr }
}
fn parse_field_reference(chars: &mut std::iter::Peekable<std::str::Chars>) -> (String, String) {
let mut field_name = String::new();
let mut format_spec = String::new();
let mut in_format = false;
while let Some(&ch) = chars.peek() {
if ch == '}' {
chars.next();
break;
} else if ch == ':' && !in_format {
in_format = true;
chars.next();
} else if in_format {
format_spec.push(ch);
chars.next();
} else {
field_name.push(ch);
chars.next();
}
}
(field_name, format_spec)
}
fn validate_field_exists(field_name: &str, input: &DeriveInput) -> Result<()> {
if !field_exists(field_name, input) {
bail!("Field '{field_name}' not found in struct");
}
Ok(())
}
fn generate_display_expression(result: &str, format_args: &[proc_macro2::TokenStream]) -> proc_macro2::TokenStream {
if format_args.is_empty() {
quote! { std::borrow::Cow::from(#result) }
} else {
quote! { std::borrow::Cow::from(format!(#result, #(#format_args),*)) }
}
}
pub fn field_exists(field_name: &str, input: &DeriveInput) -> bool {
let Data::Struct(data_struct) = &input.data else {
return false;
};
let Fields::Named(fields) = &data_struct.fields else {
return false;
};
fields
.named
.iter()
.any(|field| field.ident.as_ref().is_some_and(|ident| ident == field_name))
}
#[cfg(test)]
mod tests {
use syn::parse_quote;
use super::*;
fn da(template: &str, args: Vec<syn::Expr>) -> crate::derive_error::attributes::DisplayAttribute {
crate::derive_error::attributes::DisplayAttribute {
template: template.to_string(),
args,
}
}
fn parse(template: &str, args: Vec<syn::Expr>, input: &DeriveInput) -> proc_macro2::TokenStream {
parse_display_template(&da(template, args), input).unwrap()
}
#[test]
fn test_parse_display_template_simple() {
let input: DeriveInput = parse_quote! {
struct TestError {
path: String,
#[error]
inner: OhnoCore,
}
};
let result = parse("Error with path: {path}", vec![], &input);
let expected = quote! { std::borrow::Cow::from(format!("Error with path: {}", &self.path)) };
assert_eq!(result.to_string(), expected.to_string());
}
#[test]
fn test_parse_display_template_no_fields() {
let input: DeriveInput = parse_quote! {
struct TestError { #[error] inner: OhnoCore }
};
let result = parse("Static error message", vec![], &input);
let expected = quote! { std::borrow::Cow::from("Static error message") };
assert_eq!(result.to_string(), expected.to_string());
}
#[test]
fn test_parse_display_template_with_args() {
let input: DeriveInput = parse_quote! { struct TestError { data: Data, #[error] inner: OhnoCore } };
let result = parse(
"Invalid data: {} - {}",
vec![parse_quote! { data.0 }, parse_quote! { data.1 }],
&input,
);
let expected = quote! { std::borrow::Cow::from(format!("Invalid data: {} - {}", &self.data.0, &self.data.1)) };
assert_eq!(result.to_string(), expected.to_string());
}
#[test]
fn test_field_exists_valid_and_invalid() {
let input: DeriveInput = parse_quote! {
struct TestError { path: String, code: i32, #[error] inner: OhnoCore }
};
validate_field_exists("path", &input).unwrap();
validate_field_exists("code", &input).unwrap();
validate_field_exists("inner", &input).unwrap();
assert!(validate_field_exists("nonexistent", &input).is_err());
assert!(validate_field_exists("inner2", &input).is_err());
}
#[test]
fn test_field_exists_negative_struct_variants() {
let enum_input: DeriveInput = parse_quote! { enum TestError { Variant1, Variant2 { field: String } } };
assert!(!field_exists("field", &enum_input));
assert!(!field_exists("any_field", &enum_input));
let tuple_input: DeriveInput = parse_quote! { struct TestError(String, i32); };
assert!(!field_exists("0", &tuple_input));
assert!(!field_exists("field", &tuple_input));
let unit_input: DeriveInput = parse_quote! { struct TestError; };
assert!(!field_exists("field", &unit_input));
}
#[test]
#[expect(clippy::literal_string_with_formatting_args, reason = "False positive")]
fn test_parse_display_template_with_format_specifiers() {
let input: DeriveInput = parse_quote! { struct TestError { errors: Vec<String>, #[error] inner: OhnoCore } };
let result = parse("Failed to parse: {errors:?}", vec![], &input);
let expected = quote! { std::borrow::Cow::from(format!("Failed to parse: {:?}", &self.errors)) };
assert_eq!(result.to_string(), expected.to_string());
}
#[test]
fn test_parse_display_template_escaped_braces() {
let input: DeriveInput = parse_quote! { struct TestError { field: String, #[error] inner: OhnoCore } };
let r1 = parse("Error: {{static}} with {field}", vec![], &input);
let e1 = quote! { std::borrow::Cow::from(format!("Error: {{static}} with {}", &self.field)) };
assert_eq!(r1.to_string(), e1.to_string());
let r2 = parse("Error: {field}} extra brace", vec![], &input);
let e2 = quote! { std::borrow::Cow::from(format!("Error: {}} extra brace", &self.field)) };
assert_eq!(r2.to_string(), e2.to_string());
let r3 = parse("{{Error}}: {field} {{end}}", vec![], &input);
let e3 = quote! { std::borrow::Cow::from(format!("{{Error}}: {} {{end}}", &self.field)) };
assert_eq!(r3.to_string(), e3.to_string());
}
#[test]
fn test_parse_display_template_with_method_calls() {
let input: DeriveInput = parse_quote! { struct TestError { data: Data, #[error] inner: OhnoCore } };
let result = parse(
"Error: {} - {}",
vec![parse_quote! { data.to_string() }, parse_quote! { data.len() }],
&input,
);
assert_eq!(
result.to_string(),
"std :: borrow :: Cow :: from (format ! (\"Error: {} - {}\" , & self . data . to_string () , & self . data . len ()))"
);
}
#[test]
fn test_parse_display_template_with_nested_access() {
let input: DeriveInput = parse_quote! { struct TestError { t: TupleType, #[error] inner: OhnoCore } };
let result = parse(
"Error: {} - {}",
vec![parse_quote! { t.0.0.0.message() }, parse_quote! { t.0.0.0.m }],
&input,
);
assert_eq!(
result.to_string(),
"std :: borrow :: Cow :: from (format ! (\"Error: {} - {}\" , & self . t . 0 . 0 . 0 . message () , & self . t . 0 . 0 . 0 . m))"
);
}
#[test]
fn test_parse_display_template_not_enough_arguments() {
let input: DeriveInput = parse_quote! { struct TestError { data: Data, #[error] inner: OhnoCore } };
let display_attr = da(
"Error: {} - {} - {}",
vec![parse_quote! { data.field1 }, parse_quote! { data.field2 }],
);
let result = parse_display_template(&display_attr, &input);
assert!(result.is_err());
assert_eq!(result.unwrap_err().to_string(), "Not enough arguments for format placeholders");
}
#[test]
fn test_parse_display_template_too_many_arguments() {
let input: DeriveInput = parse_quote! { struct TestError { data: Data, #[error] inner: OhnoCore } };
let display_attr = da(
"Error: {} - {}",
vec![
parse_quote! { data.field1 },
parse_quote! { data.field2 },
parse_quote! { data.field3 },
],
);
let result = parse_display_template(&display_attr, &input);
assert!(result.is_err());
assert_eq!(result.unwrap_err().to_string(), "Too many arguments for format placeholders");
}
#[test]
fn test_parse_display_template_exact_argument_match() {
let input: DeriveInput = parse_quote! { struct TestError { data: Data, #[error] inner: OhnoCore } };
let display_attr = da("Error: {} - {}", vec![parse_quote! { data.field1 }, parse_quote! { data.field2 }]);
parse_display_template(&display_attr, &input).unwrap();
}
}