Skip to main content

baxe_derive/
lib.rs

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