Skip to main content

baxe_derive/
lib.rs

1use proc_macro::TokenStream;
2use quote::quote;
3use syn::{parse::Parser, spanned::Spanned, Data, DeriveInput, Expr, Fields};
4
5#[proc_macro_attribute]
6pub fn error(attr: TokenStream, item: TokenStream) -> TokenStream {
7    expand(attr.into(), item.into())
8        .unwrap_or_else(|err| err.to_compile_error())
9        .into()
10}
11
12fn expand(
13    attr: proc_macro2::TokenStream,
14    item: proc_macro2::TokenStream,
15) -> syn::Result<proc_macro2::TokenStream> {
16    let mut log_message_with = None;
17    let mut hide_message = false;
18
19    syn::meta::parser(|meta| {
20        if meta.path.is_ident("logMessageWith") {
21            log_message_with = Some(meta.value()?.parse::<Expr>()?);
22        } else if meta.path.is_ident("hideMessage") {
23            hide_message = true;
24        } else if meta.input.peek(syn::Token![=]) {
25            // Preserve support for ignored, unknown options.
26            let _ = meta.value()?.parse::<Expr>()?;
27        }
28        Ok(())
29    })
30    .parse2(attr)?;
31
32    let input: DeriveInput = syn::parse2(item)?;
33
34    for attr in &input.attrs {
35        if attr.path().is_ident("baxe") {
36            let err = syn::Error::new(
37                attr.span(),
38                "The #[baxe(...)] attribute is only allowed on enum variants, not on the enum itself.",
39            );
40            return Err(err);
41        }
42    }
43
44    let enum_name = input.ident;
45    let data = match input.data {
46        Data::Enum(data) => data,
47        _ => {
48            return Err(syn::Error::new(
49                enum_name.span(),
50                "baxe::error can only be applied to enums",
51            ))
52        }
53    };
54
55    let variants_def = data
56        .variants
57        .iter()
58        .map(|v| {
59            let variant_ident = &v.ident;
60            match &v.fields {
61                Fields::Unit => quote! { #variant_ident },
62                Fields::Unnamed(fields) => {
63                    let types = fields.unnamed.iter().map(|f| &f.ty);
64                    quote! { #variant_ident(#(#types),*) }
65                }
66                Fields::Named(fields) => {
67                    let field_defs = fields.named.iter().map(|f| {
68                        let name = &f.ident;
69                        let ty = &f.ty;
70                        quote! { #name: #ty }
71                    });
72                    quote! { #variant_ident { #(#field_defs),* } }
73                }
74            }
75        })
76        .collect::<Vec<_>>();
77
78    let count = data.variants.len();
79    let mut patterns = Vec::with_capacity(count);
80    let mut statuses = Vec::with_capacity(count);
81    let mut tags = Vec::with_capacity(count);
82    let mut codes = Vec::with_capacity(count);
83    let mut messages = Vec::with_capacity(count);
84
85    for variant in &data.variants {
86        let variant_ident = &variant.ident;
87        let attrs = parse_baxe_attributes(variant)?;
88        let message = attrs.message;
89        let (pattern, bindings) = match &variant.fields {
90            Fields::Unit => (quote! { #enum_name::#variant_ident }, Vec::new()),
91            Fields::Unnamed(fields) => {
92                let bindings: Vec<_> = fields
93                    .unnamed
94                    .iter()
95                    .enumerate()
96                    .map(|(i, _)| {
97                        syn::Ident::new(&format!("arg{i}"), proc_macro2::Span::call_site())
98                    })
99                    .collect();
100                (
101                    quote! { #enum_name::#variant_ident(#(#bindings),*) },
102                    bindings,
103                )
104            }
105            Fields::Named(fields) => {
106                let bindings: Vec<_> = fields
107                    .named
108                    .iter()
109                    .map(|field| field.ident.clone().unwrap())
110                    .collect();
111                (
112                    quote! { #enum_name::#variant_ident { #(#bindings),* } },
113                    bindings,
114                )
115            }
116        };
117
118        messages.push(quote! {
119            #pattern => write!(f, #message #(, #bindings)*)
120        });
121        patterns.push(pattern);
122        statuses.push(attrs.status);
123        tags.push(attrs.tag);
124        codes.push(attrs.code);
125    }
126
127    let log_statement = if let Some(log_fn) = log_message_with {
128        quote! {
129            #log_fn!("{}", error.to_string());
130        }
131    } else {
132        quote! {}
133    };
134
135    let to_message = if hide_message {
136        quote! {
137            None
138        }
139    } else {
140        quote! {
141            error.to_string().into()
142        }
143    };
144
145    let expanded = quote! {
146        #[derive(Debug)]
147        pub enum #enum_name {
148            #(#variants_def,)*
149        }
150
151        impl std::fmt::Display for #enum_name {
152            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
153                match self {
154                    #(#messages,)*
155                }
156            }
157        }
158
159        impl std::error::Error for #enum_name {}
160
161        impl BackendError for #enum_name {
162            fn to_status_code(&self) -> axum::http::StatusCode {
163                match self {
164                    #(#patterns => #statuses,)*
165                }
166            }
167
168            fn to_error_tag(&self) -> impl std::fmt::Display {
169                match self {
170                    #(#patterns => #tags,)*
171                }
172            }
173
174            fn to_error_code(&self) -> u16 {
175                match self {
176                    #(#patterns => #codes,)*
177                }
178            }
179        }
180
181        impl From<#enum_name> for BaxeError {
182            fn from(error: #enum_name) -> Self {
183                #log_statement
184                let status = error.to_status_code();
185                let tag: String = error.to_error_tag().to_string();
186                BaxeError::new(status, #to_message, error.to_error_code(), tag)
187            }
188        }
189
190        impl IntoResponse for #enum_name {
191            fn into_response(self) -> axum::response::Response {
192                (self.to_status_code(), Json(BaxeError::from(self))).into_response()
193            }
194        }
195    };
196
197    Ok(expanded)
198}
199
200struct BaxeAttributes {
201    status: proc_macro2::TokenStream,
202    tag: proc_macro2::TokenStream,
203    code: proc_macro2::TokenStream,
204    message: proc_macro2::TokenStream,
205}
206
207fn parse_baxe_attributes(variant: &syn::Variant) -> syn::Result<BaxeAttributes> {
208    let mut attrs = BaxeAttributes {
209        status: quote!(None),
210        tag: quote!(None),
211        code: quote!(None),
212        message: quote!(None),
213    };
214
215    for attr in &variant.attrs {
216        if attr.path().is_ident("baxe") {
217            attr.parse_nested_meta(|meta| {
218                let value = meta.value()?.parse::<Expr>()?;
219                if meta.path.is_ident("status") {
220                    attrs.status = quote!(#value);
221                } else if meta.path.is_ident("tag") {
222                    attrs.tag = quote!(#value);
223                } else if meta.path.is_ident("code") {
224                    attrs.code = quote!(#value);
225                } else if meta.path.is_ident("message") {
226                    attrs.message = quote!(#value);
227                }
228                Ok(())
229            })?;
230        }
231    }
232
233    Ok(attrs)
234}
235
236#[cfg(test)]
237mod tests;