use proc_macro2::{Ident, TokenStream};
use quote::quote;
use syn::Error;
use super::{
attachment::{
backtrace_attachment_tokens, metadata_attach_tokens, redact_tokens,
source_attachment_tokens, telemetry_initialization
},
binding::{bind_struct_fields, bind_variant_fields, field_usage_tokens}
};
use crate::input::{ErrorInput, MasterrorSpec, StructData, VariantData};
pub fn struct_conversion_impl(
input: &ErrorInput,
data: &StructData,
spec: &MasterrorSpec
) -> TokenStream {
let ident = &input.ident;
let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
let code = &spec.code;
let category = &spec.category;
let message_init = message_initialization(spec.expose_message, quote!(&value));
let (destructure, bound_fields) = bind_struct_fields(ident, &data.fields);
let field_usage = field_usage_tokens(&bound_fields);
let telemetry_init = telemetry_initialization(&spec.telemetry);
let metadata_attach = metadata_attach_tokens();
let redact_tokens = redact_tokens(&spec.redact);
let source_tokens = source_attachment_tokens(&bound_fields);
let backtrace_tokens = backtrace_attachment_tokens(&data.fields, &bound_fields);
quote! {
impl #impl_generics core::convert::From<#ident #ty_generics> for masterror::Error #where_clause {
fn from(value: #ident #ty_generics) -> Self {
#message_init
#destructure
#field_usage
#telemetry_init
let mut __masterror_error = match __masterror_message {
Some(message) => masterror::Error::with((#category), message),
None => masterror::Error::bare((#category))
};
__masterror_error = __masterror_error.with_code((#code));
#metadata_attach
#redact_tokens
#source_tokens
#backtrace_tokens
__masterror_error
}
}
}
}
pub fn enum_conversion_impl(input: &ErrorInput, variants: &[VariantData]) -> TokenStream {
let ident = &input.ident;
let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
let mut arms = Vec::new();
let mut message_arms = Vec::new();
for variant in variants {
let spec = variant.masterror.as_ref().expect("presence checked");
let code = &spec.code;
let category = &spec.category;
let (pattern, bound_fields) = bind_variant_fields(ident, variant);
let field_usage = field_usage_tokens(&bound_fields);
let telemetry_init = telemetry_initialization(&spec.telemetry);
let metadata_attach = metadata_attach_tokens();
let redact_tokens = redact_tokens(&spec.redact);
let source_tokens = source_attachment_tokens(&bound_fields);
let backtrace_tokens = backtrace_attachment_tokens(&variant.fields, &bound_fields);
message_arms.push(enum_message_arm(ident, variant, spec.expose_message));
arms.push(quote! {
#pattern => {
#field_usage
#telemetry_init
let mut __masterror_error = match __masterror_message {
Some(message) => masterror::Error::with((#category), message),
None => masterror::Error::bare((#category))
};
__masterror_error = __masterror_error.with_code((#code));
#metadata_attach
#redact_tokens
#source_tokens
#backtrace_tokens
__masterror_error
}
});
}
let message_match = quote! {
let __masterror_message: Option<String> = match &value {
#(#message_arms)*
};
};
quote! {
impl #impl_generics core::convert::From<#ident #ty_generics> for masterror::Error #where_clause {
fn from(value: #ident #ty_generics) -> Self {
#message_match
match value {
#(#arms),*
}
}
}
}
}
pub fn message_initialization(enabled: bool, value: TokenStream) -> TokenStream {
if enabled {
quote! {
let __masterror_message = Some(std::string::ToString::to_string(#value));
}
} else {
quote! {
let __masterror_message: Option<String> = None;
}
}
}
pub fn ensure_all_variants_have_masterror(variants: &[VariantData]) -> Result<(), Error> {
for variant in variants {
if variant.masterror.is_none() {
return Err(Error::new(
variant.span,
"all variants must use #[masterror(...)] to derive masterror::Error conversion"
));
}
}
Ok(())
}
fn enum_message_arm(
enum_ident: &Ident,
variant: &VariantData,
expose_message: bool
) -> TokenStream {
use quote::format_ident;
if expose_message {
let binding = format_ident!("__masterror_variant_ref");
let pattern = enum_message_pattern(enum_ident, variant, Some(&binding));
quote! {
#pattern => Some(std::string::ToString::to_string(#binding)),
}
} else {
let pattern = enum_message_pattern(enum_ident, variant, None);
quote! {
#pattern => None,
}
}
}
fn enum_message_pattern(
enum_ident: &Ident,
variant: &VariantData,
binding: Option<&Ident>
) -> TokenStream {
use crate::input::Fields;
let variant_ident = &variant.ident;
match (&variant.fields, binding) {
(Fields::Unit, Some(binding)) => quote!(#binding @ #enum_ident::#variant_ident),
(Fields::Unit, None) => quote!(#enum_ident::#variant_ident),
(Fields::Named(_), Some(binding)) => quote!(#binding @ #enum_ident::#variant_ident { .. }),
(Fields::Named(_), None) => quote!(#enum_ident::#variant_ident { .. }),
(Fields::Unnamed(_), Some(binding)) => quote!(#binding @ #enum_ident::#variant_ident(..)),
(Fields::Unnamed(_), None) => quote!(#enum_ident::#variant_ident(..))
}
}
#[cfg(test)]
mod tests {
use quote::quote;
use super::*;
#[test]
fn test_message_initialization_enabled() {
let result = message_initialization(true, quote!(&value));
let expected = quote! {
let __masterror_message = Some(std::string::ToString::to_string(&value));
};
assert_eq!(result.to_string(), expected.to_string());
}
#[test]
fn test_message_initialization_disabled() {
let result = message_initialization(false, quote!(&value));
let expected = quote! {
let __masterror_message: Option<String> = None;
};
assert_eq!(result.to_string(), expected.to_string());
}
#[test]
fn test_ensure_all_variants_have_masterror_valid() {
let variants = vec![];
assert!(ensure_all_variants_have_masterror(&variants).is_ok());
}
#[test]
fn test_enum_message_pattern_unit() {
use proc_macro2::Span;
use quote::format_ident;
use crate::input::{DisplaySpec, Fields};
let variant = VariantData {
ident: format_ident!("NotFound"),
fields: Fields::Unit,
display: DisplaySpec::Template(crate::template_support::DisplayTemplate {
segments: vec![]
}),
format_args: Default::default(),
app_error: None,
masterror: None,
span: Span::call_site()
};
let enum_ident = format_ident!("MyError");
let result = enum_message_pattern(&enum_ident, &variant, None);
let result_str = result.to_string();
assert!(result_str.contains("MyError :: NotFound"));
}
#[test]
fn test_enum_message_pattern_named() {
use proc_macro2::Span;
use quote::format_ident;
use syn::parse_quote;
use crate::input::{DisplaySpec, Field, FieldAttrs, Fields};
let field = Field {
ident: Some(format_ident!("message")),
member: syn::Member::Named(format_ident!("message")),
ty: parse_quote!(String),
index: 0,
attrs: FieldAttrs::default(),
span: Span::call_site()
};
let variant = VariantData {
ident: format_ident!("Custom"),
fields: Fields::Named(vec![field]),
display: DisplaySpec::Template(crate::template_support::DisplayTemplate {
segments: vec![]
}),
format_args: Default::default(),
app_error: None,
masterror: None,
span: Span::call_site()
};
let enum_ident = format_ident!("MyError");
let result = enum_message_pattern(&enum_ident, &variant, None);
let result_str = result.to_string();
assert!(result_str.contains("MyError :: Custom"));
assert!(result_str.contains(".."));
}
}