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 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;