Skip to main content

apiresponse_macro/
lib.rs

1//! Derive macros for the `apiresponse` crate.
2//!
3//! This crate provides the `#[derive(Response)]` macro which implements
4//! the `Response` trait for your error types.
5//!
6//! ## Usage with thiserror
7//!
8//! ```rust,ignore
9//! use apiresponse::Response;
10//! use thiserror::Error;
11//!
12//! #[derive(Debug, Error, Response)]
13//! pub enum MyError {
14//!     #[error("not found")]
15//!     #[response(code = 1001, status = 404)]
16//!     NotFound,
17//!
18//!     #[error("unauthorized: {0}")]
19//!     #[response(code = 1002, status = 401)]
20//!     Unauthorized(String),
21//!
22//!     #[error(transparent)]
23//!     #[response(transparent)]
24//!     Other(#[from] anyhow::Error),
25//! }
26//! ```
27
28use proc_macro::TokenStream;
29use proc_macro2::TokenStream as TokenStream2;
30use quote::quote;
31use syn::{Attribute, Data, DeriveInput, Expr, ExprLit, Fields, Ident, Lit, parse_macro_input};
32
33/// Derive macro for implementing the `Response` trait.
34///
35/// ## Variant-level Attributes
36///
37/// - `#[response(code = N)]` - Set the error code (required for non-transparent variants)
38/// - `#[response(status = N)]` - Set the HTTP status code (optional, defaults to 200)
39/// - `#[response(transparent)]` - Delegate to the inner error's `Response` implementation
40///
41/// ## Example
42///
43/// ```rust,ignore
44/// #[derive(Debug, thiserror::Error, Response)]
45/// pub enum AuthError {
46///     #[error("User not found")]
47///     #[response(code = 1000, status = 404)]
48///     UserNotFound,
49///
50///     #[error("Invalid password")]
51///     #[response(code = 1001, status = 401)]
52///     InvalidPassword,
53///
54///     #[error(transparent)]
55///     #[response(transparent)]
56///     Internal(#[from] anyhow::Error),
57/// }
58/// ```
59#[proc_macro_derive(Response, attributes(response))]
60pub fn response_derive(input: TokenStream) -> TokenStream {
61    let input = parse_macro_input!(input as DeriveInput);
62    match expand_response_derive(&input) {
63        Ok(tokens) => tokens.into(),
64        Err(err) => err.to_compile_error().into(),
65    }
66}
67
68fn expand_response_derive(input: &DeriveInput) -> syn::Result<TokenStream2> {
69    let name = &input.ident;
70    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
71
72    let data = match &input.data {
73        Data::Enum(data) => data,
74        Data::Struct(_) => {
75            return Err(syn::Error::new_spanned(
76                input,
77                "Response can only be derived for enums",
78            ));
79        }
80        Data::Union(_) => {
81            return Err(syn::Error::new_spanned(
82                input,
83                "Response cannot be derived for unions",
84            ));
85        }
86    };
87
88    // Single pass: parse attrs and build pattern once per variant, produce both arms
89    let arms: Vec<_> = data
90        .variants
91        .iter()
92        .map(|v| {
93            let attrs = parse_response_attrs(&v.attrs)?;
94            let pattern = build_pattern(name, &v.ident, &v.fields, attrs.transparent);
95
96            let code_expr = if attrs.transparent {
97                quote! { inner.error_code() }
98            } else if let Some(code) = attrs.code {
99                quote! { #code }
100            } else {
101                return Err(syn::Error::new_spanned(
102                    v,
103                    "Variant must specify an error code with `#[response(code = N)]`.\n\
104                     Example: #[response(code = 1001)]",
105                ));
106            };
107
108            let status_expr = if attrs.transparent {
109                quote! { inner.http_status_code() }
110            } else {
111                let status = attrs.status.unwrap_or(200);
112                quote! { #status }
113            };
114
115            Ok((
116                quote! { #pattern => #code_expr },
117                quote! { #pattern => #status_expr },
118            ))
119        })
120        .collect::<syn::Result<Vec<_>>>()?;
121
122    let (code_arms, status_arms): (Vec<_>, Vec<_>) = arms.into_iter().unzip();
123
124    Ok(quote! {
125        impl #impl_generics apiresponse::Response for #name #ty_generics #where_clause {
126            fn error_code(&self) -> u64 {
127                match self {
128                    #(#code_arms),*
129                }
130            }
131
132            fn message(&self) -> String {
133                self.to_string()
134            }
135
136            fn http_status_code(&self) -> u16 {
137                match self {
138                    #(#status_arms),*
139                }
140            }
141        }
142    })
143}
144
145/// Parse `#[response(...)]` attributes from a variant.
146fn parse_response_attrs(attrs: &[Attribute]) -> syn::Result<ResponseAttrs> {
147    let mut result = ResponseAttrs::default();
148
149    for attr in attrs {
150        if !attr.path().is_ident("response") {
151            continue;
152        }
153
154        attr.parse_nested_meta(|meta| {
155            if meta.path.is_ident("code") {
156                let value: Expr = meta.value()?.parse()?;
157                if let Expr::Lit(ExprLit {
158                    lit: Lit::Int(lit), ..
159                }) = value
160                {
161                    result.code = Some(lit.base10_parse()?);
162                } else {
163                    return Err(meta.error("code must be an integer"));
164                }
165            } else if meta.path.is_ident("status") {
166                let value: Expr = meta.value()?.parse()?;
167                if let Expr::Lit(ExprLit {
168                    lit: Lit::Int(lit), ..
169                }) = value
170                {
171                    result.status = Some(lit.base10_parse()?);
172                } else {
173                    return Err(meta.error("status must be an integer"));
174                }
175            } else if meta.path.is_ident("transparent") {
176                result.transparent = true;
177            }
178            Ok(())
179        })?;
180    }
181
182    Ok(result)
183}
184
185#[derive(Default)]
186struct ResponseAttrs {
187    code: Option<u64>,
188    status: Option<u16>,
189    transparent: bool,
190}
191
192/// Build the match arm pattern for a variant.
193fn build_pattern(
194    enum_name: &Ident,
195    variant_name: &Ident,
196    fields: &Fields,
197    transparent: bool,
198) -> TokenStream2 {
199    match fields {
200        Fields::Unit => quote! { #enum_name::#variant_name },
201        Fields::Unnamed(fields) => {
202            if transparent && fields.unnamed.len() == 1 {
203                quote! { #enum_name::#variant_name(inner) }
204            } else {
205                quote! { #enum_name::#variant_name(..) }
206            }
207        }
208        Fields::Named(_) => {
209            if transparent {
210                quote! { #enum_name::#variant_name { ref inner, .. } }
211            } else {
212                quote! { #enum_name::#variant_name { .. } }
213            }
214        }
215    }
216}