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}