use syn::spanned::Spanned;
use syn::{DeriveInput, Expr, Ident, Lit, Meta, Result, Type};
use crate::derive_error::types::FromConfig;
use crate::utils::bail;
const FROM_EMPTY_PARENS: &str =
"empty #[from()] attribute is not allowed. Either specify types like #[from(ErrorType)] or remove the attribute entirely";
const FROM_EMPTY_PATH: &str =
"empty #[from] attribute is not allowed. Either specify types like #[from(ErrorType)] or remove the attribute entirely";
const FROM_INVALID_FORM: &str = "from attribute must be in the form #[from(Type1, Type2, ...)] with at least one type specified";
#[derive(Debug)]
pub(crate) struct DisplayAttribute {
pub template: String,
pub args: Vec<syn::Expr>,
}
pub(crate) fn find_display_attribute(input: &DeriveInput) -> Result<Option<DisplayAttribute>> {
for attr in &input.attrs {
if attr.path().is_ident("display") {
let Meta::List(meta_list) = &attr.meta else {
bail!(
attr.span(),
"display attribute must be in the form #[display(\"message\")] or #[display(\"format_template\", arg1, arg2)]"
);
};
return parse_display_tokens(&meta_list.tokens).map(Some);
}
}
Ok(None)
}
fn parse_display_tokens(tokens: &proc_macro2::TokenStream) -> Result<DisplayAttribute> {
syn::parse::Parser::parse2(
|input: syn::parse::ParseStream| {
let template_lit: Lit = input.parse()?;
let Lit::Str(template_str) = template_lit else {
bail!(template_lit.span(), "display attribute template must be a string literal");
};
let mut args = Vec::new();
while input.peek(syn::Token![,]) {
input.parse::<syn::Token![,]>()?;
if !input.is_empty() {
let expr: Expr = input.parse()?;
args.push(expr);
}
}
Ok(DisplayAttribute {
template: template_str.value(),
args,
})
},
tokens.clone(),
)
}
pub(crate) fn has_no_constructors_attribute(input: &DeriveInput) -> bool {
has_simple_attribute(input, "no_constructors")
}
pub(crate) fn has_no_debug_attribute(input: &DeriveInput) -> bool {
has_simple_attribute(input, "no_debug")
}
pub(crate) fn find_from_attribute(input: &DeriveInput) -> Result<Vec<FromConfig>> {
let mut from_configs = Vec::new();
for attr in &input.attrs {
if attr.path().is_ident("from") {
let mut configs = parse_from_configs_from_meta(&attr.meta, attr.span())?;
from_configs.append(&mut configs);
}
}
Ok(from_configs)
}
fn has_simple_attribute(input: &DeriveInput, attr_name: &str) -> bool {
input.attrs.iter().any(|attr| attr.path().is_ident(attr_name))
}
fn parse_from_configs_from_meta(meta: &Meta, span: proc_macro2::Span) -> Result<Vec<FromConfig>> {
match meta {
Meta::List(meta_list) => {
if meta_list.tokens.is_empty() {
bail!(span, FROM_EMPTY_PARENS)
}
parse_from_config_list(&meta_list.tokens)
}
Meta::Path(_) => bail!(span, FROM_EMPTY_PATH),
Meta::NameValue(_) => bail!(span, FROM_INVALID_FORM),
}
}
fn parse_from_config_list(tokens: &proc_macro2::TokenStream) -> Result<Vec<FromConfig>> {
use std::collections::HashMap;
let mut configs = Vec::new();
syn::parse::Parser::parse2(
|input: syn::parse::ParseStream| {
while !input.is_empty() {
let from_type: Type = input.parse()?;
let field_expressions = if input.peek(syn::token::Paren) {
let content;
syn::parenthesized!(content in input);
parse_field_expressions(&content)?
} else {
HashMap::new()
};
configs.push(FromConfig {
from_type,
field_expressions,
});
if !input.is_empty() {
input.parse::<syn::Token![,]>()?;
}
}
Ok(())
},
tokens.clone(),
)?;
Ok(configs)
}
fn parse_field_expressions(content: syn::parse::ParseStream) -> Result<std::collections::HashMap<String, Expr>> {
let mut field_expressions = std::collections::HashMap::new();
while !content.is_empty() {
let field_key = parse_field_key(content)?;
content.parse::<syn::Token![:]>()?;
let expr: Expr = content.parse()?;
field_expressions.insert(field_key, expr);
if !content.is_empty() {
content.parse::<syn::Token![,]>()?;
}
}
Ok(field_expressions)
}
fn parse_field_key(content: syn::parse::ParseStream) -> Result<String> {
if content.peek(syn::Lit) {
let lit: syn::Lit = content.parse()?;
match lit {
syn::Lit::Int(lit_int) => Ok(lit_int.base10_digits().to_string()),
_ => Err(syn::Error::new(
lit.span(),
"Only integer literals are supported for tuple field indices",
)),
}
} else {
let field_name: Ident = content.parse()?;
Ok(field_name.to_string())
}
}
#[cfg(test)]
mod tests {
use syn::parse_quote;
use super::*;
fn expect_display_error(input: &DeriveInput, expected: &str) {
let err = find_display_attribute(input).unwrap_err();
assert!(
err.to_string().contains(expected),
"Expected error to contain '{expected}', got '{err}'"
);
}
fn expect_from_error(input: &DeriveInput, expected: &str) {
let err = find_from_attribute(input).unwrap_err();
assert!(
err.to_string().contains(expected),
"Expected error to contain '{expected}', got '{err}'"
);
}
fn assert_field_keys(config: &FromConfig, expected: &[&str]) {
assert_eq!(config.field_expressions.len(), expected.len());
for key in expected {
assert!(config.field_expressions.contains_key(*key), "Missing expected key '{key}'");
}
}
#[test]
fn test_find_display_attribute() {
let input: DeriveInput = parse_quote! {
#[display("Failed to read config with path: {path}")]
struct ConfigError {
pub path: String,
#[error]
pub inner_error: OhnoCore,
}
};
let attr = find_display_attribute(&input).unwrap();
assert!(attr.is_some());
let attr = attr.unwrap();
assert_eq!(attr.template, "Failed to read config with path: {path}");
assert!(attr.args.is_empty());
}
#[test]
fn test_find_display_attribute_error_cases() {
let cases = vec![
(
parse_quote! {
#[display]
struct ConfigError {
pub path: String,
#[error]
pub inner_error: OhnoCore,
}
},
"display attribute must be in the form",
),
(
parse_quote! {
#[display(1, 2, 3)]
struct ConfigError {
pub path: String,
#[error]
pub inner_error: OhnoCore,
}
},
"display attribute template must be a string literal",
),
];
for (input, expected) in cases {
expect_display_error(&input, expected);
}
}
#[test]
fn test_find_display_attribute_with_args() {
let input: DeriveInput = parse_quote! {
#[display("Invalid data: {} - {}", data.0, data.1, )]
struct InvalidData {
pub data: Data,
#[error]
pub inner_error: OhnoCore,
}
};
let attr = find_display_attribute(&input).unwrap();
assert!(attr.is_some());
let attr = attr.unwrap();
assert_eq!(attr.template, "Invalid data: {} - {}");
assert_eq!(attr.args.len(), 2);
}
#[test]
fn test_has_no_constructors_attribute() {
let input_with: DeriveInput = parse_quote! {
#[no_constructors]
struct TestError {
#[error]
inner: OhnoCore,
}
};
let input_without: DeriveInput = parse_quote! {
struct TestError {
#[error]
inner: OhnoCore,
}
};
assert!(has_no_constructors_attribute(&input_with));
assert!(!has_no_constructors_attribute(&input_without));
}
#[test]
fn test_find_from_attribute_valid() {
let input: DeriveInput = parse_quote! {
#[from(std::io::Error, std::fmt::Error)]
struct TestError {
#[error]
inner: OhnoCore,
}
};
let from_configs = find_from_attribute(&input).unwrap();
assert_eq!(from_configs.len(), 2);
assert!(from_configs[0].field_expressions.is_empty());
assert!(from_configs[1].field_expressions.is_empty());
}
#[test]
fn test_find_from_attribute_with_field_expressions() {
let input: DeriveInput = parse_quote! {
#[from(std::io::Error(kind: ErrorKind::Io, message: "IO error occurred".to_string()))]
struct TestError {
kind: ErrorKind,
message: String,
#[error]
inner: OhnoCore,
}
};
let from_configs = find_from_attribute(&input).unwrap();
assert_eq!(from_configs.len(), 1);
assert_field_keys(&from_configs[0], &["kind", "message"]);
}
#[test]
fn test_find_from_attribute_with_field_expressions_for_tuple_error() {
let input: DeriveInput = parse_quote! {
#[from(std::io::Error(1: "IO error occurred".to_string()))]
struct TestError(OhnoCore, String);
};
let from_configs = find_from_attribute(&input).unwrap();
assert_eq!(from_configs.len(), 1);
assert_field_keys(&from_configs[0], &["1"]);
}
#[test]
fn test_find_from_attribute_with_invalid_field_for_tuple() {
let input: DeriveInput = parse_quote! {
#[from(std::io::Error("XYZ": "IO error occurred".to_string()))]
struct TestError(OhnoCore, String);
};
expect_from_error(&input, "Only integer literals are supported for tuple field indices");
}
#[test]
fn test_find_from_attribute_invalid_forms() {
let cases = vec![
(
parse_quote! {
#[from()]
struct TestError {
#[error]
inner: OhnoCore,
}
},
"empty #[from()] attribute is not allowed",
),
(
parse_quote! {
#[from]
struct TestError {
#[error]
inner: OhnoCore,
}
},
"empty #[from] attribute is not allowed",
),
(
parse_quote! {
#[from = "Hello"]
struct TestError(OhnoCore, String);
},
"from attribute must be in the form",
),
];
for (input, expected) in cases {
expect_from_error(&input, expected);
}
}
}