Skip to main content

feather_macros/
lib.rs

1use proc_macro::TokenStream;
2use quote::quote;
3#[cfg(feature = "jwt")]
4use syn::{Data, DeriveInput, Fields};
5use syn::{ItemFn, parse_macro_input};
6
7/// Derive macro for implementing the `Claim` trait for JWT claims.
8///
9/// This macro automatically derives the `Claim` trait for your claims struct,
10/// enabling validation of required fields and JWT expiration times.
11///
12/// # Attributes
13///
14/// - `#[required]` - Mark a field as required (must not be empty)
15/// - `#[exp]` - Mark a field as the expiration timestamp (checks against current time)
16///
17/// # Example: Simple Claims
18///
19/// ```rust,ignore
20/// use feather::jwt::Claim;
21///
22/// #[derive(Claim, Clone)]
23/// struct MyClaims {
24///     user_id: String,
25///     username: String,
26/// }
27/// ```
28///
29/// # Example: With Validation
30///
31/// ```rust,ignore
32/// use feather::jwt::Claim;
33///
34/// #[derive(Claim, Clone)]
35/// struct AuthClaims {
36///     #[required]
37///     user_id: String,
38///     #[required]
39///     username: String,
40/// }
41/// ```
42///
43/// # Example: With Expiration
44///
45/// ```rust,ignore
46/// use feather::jwt::Claim;
47///
48/// #[derive(Claim, Clone)]
49/// struct TokenClaims {
50///     #[required]
51///     user_id: String,
52///     #[exp]
53///     expires_at: usize,  // Unix timestamp
54/// }
55/// ```
56///
57/// # How It Works
58///
59/// The macro generates a `validate()` method that:
60/// 1. Checks all `#[required]` fields are non-empty
61/// 2. Checks `#[exp]` fields contain timestamps greater than current time
62/// 3. Returns `Err` if any validation fails
63///
64/// This is automatically called by the JWT manager when decoding tokens.
65///
66/// # See Also
67///
68/// - [`SimpleClaims`](https://docs.rs/feather/latest/feather/jwt/struct.SimpleClaims.html) for a built-in claims struct
69/// - [Authentication Guide](https://docs.rs/feather/latest/feather/guides/authentication/) for JWT patterns
70#[cfg(feature = "jwt")]
71#[proc_macro_derive(Claim, attributes(required, exp))]
72pub fn derive_claim(input: TokenStream) -> TokenStream {
73    let input = parse_macro_input!(input as DeriveInput);
74    let name = &input.ident;
75    let mut checks = Vec::new();
76
77    if let Data::Struct(data_struct) = &input.data {
78        if let Fields::Named(fields) = &data_struct.fields {
79            for field in &fields.named {
80                let field_name = &field.ident;
81                for attr in &field.attrs {
82                    if attr.path().is_ident("required") {
83                        checks.push(quote! {
84                            if self.#field_name.is_empty() {
85                                return Err(feather::jwt::Error::from(feather::jwt::ErrorKind::InvalidToken));
86                            }
87                        });
88                    }
89                    if attr.path().is_ident("exp") {
90                        checks.push(quote! {
91                            if self.#field_name < ::std::time::SystemTime::now().duration_since(::std::time::UNIX_EPOCH).unwrap().as_secs() as usize {
92                                return Err(feather::jwt::Error::from(feather::jwt::ErrorKind::ExpiredSignature));
93                            }
94                        });
95                    }
96                }
97            }
98        }
99    }
100
101    let expanded = quote! {
102        impl feather::jwt::Claim for #name {
103            fn validate(&self) -> Result<(), feather::jwt::Error> {
104                #(#checks)*
105                Ok(())
106            }
107        }
108    };
109    TokenStream::from(expanded)
110}
111
112/// Attribute macro for defining middleware functions with automatic signature injection.
113///
114/// This macro eliminates boilerplate by automatically providing `req`, `res`, and `ctx` parameters
115/// to your middleware function. It transforms a simple function into a proper Feather middleware.
116///
117/// # What This Macro Does
118///
119/// The `#[middleware_fn]` macro injects three parameters into your function:
120/// - `req: &mut Request` - The HTTP request
121/// - `res: &mut Response` - The HTTP response
122/// - `ctx: &AppContext` - Application context for accessing state
123///
124/// Your function must return `Outcome` (which is `Result<MiddlewareResult, Box<dyn Error>>`).
125///
126/// # Basic Example
127///
128/// ```rust,ignore
129/// use feather::middleware_fn;
130///
131/// #[middleware_fn]
132/// fn log_requests() {
133///     println!("{} {}", req.method, req.uri);
134///     next!()
135/// }
136///
137/// app.use_middleware(log_requests);
138/// ```
139///
140/// # With Route Handlers
141///
142/// ```rust,ignore
143/// use feather::{App, middleware_fn};
144///
145/// #[middleware_fn]
146/// fn greet() {
147///     let name = req.param("name").unwrap_or("Guest".to_string());
148///     res.send_text(format!("Hello, {}!", name));
149///     next!()
150/// }
151///
152/// let mut app = App::new();
153/// app.get("/greet/:name", greet);
154/// ```
155///
156/// # Compared to the middleware! Macro
157///
158/// Both `#[middleware_fn]` and `middleware!` work similarly, but `#[middleware_fn]` is
159/// best for reusable, named middleware functions, while `middleware!` is best for inline closures:
160///
161/// ```rust,ignore
162/// // Using #[middleware_fn] - for reusable middleware
163/// #[middleware_fn]
164/// fn validate_auth() {
165///     if !req.headers.contains_key("Authorization") {
166///         res.set_status(401);
167///         res.send_text("Unauthorized");
168///         return next!();
169///     }
170///     next!()
171/// }
172///
173/// app.use_middleware(validate_auth);
174///
175/// // Using middleware! - for inline middleware
176/// app.get("/", middleware!(|_req, res, _ctx| {
177///     res.send_text("Hello!");
178///     next!()
179/// }));
180/// ```
181///
182/// # Accessing Application State
183///
184/// ```rust,ignore
185/// use feather::{State, middleware_fn};
186///
187/// #[derive(Clone)]
188/// struct Config {
189///     api_key: String,
190/// }
191///
192/// #[middleware_fn]
193/// fn check_api_key() {
194///     let config = ctx.get_state::<State<Config>>();
195///     let is_valid = config.with_scope(|cfg| cfg.api_key == "secret");
196///     
197///     if !is_valid {
198///         res.set_status(403);
199///         res.send_text("Forbidden");
200///         return next!();
201///     }
202///     next!()
203/// }
204/// ```
205///
206/// # Error Handling
207///
208/// ```rust,ignore
209/// use feather::middleware_fn;
210///
211/// #[middleware_fn]
212/// fn parse_json() {
213///     if let Ok(body) = String::from_utf8(req.body.clone()) {
214///         // Process body
215///         next!()
216///     } else {
217///         res.set_status(400);
218///         res.send_text("Invalid UTF-8");
219///         next!()
220///     }
221/// }
222/// ```
223///
224/// # See Also
225///
226/// - Use `#[jwt_required]` together with `#[middleware_fn]` for JWT-protected routes
227/// - See the [Middlewares Guide](https://docs.rs/feather/latest/feather/guides/middlewares/) for more patterns
228#[proc_macro_attribute]
229pub fn middleware_fn(_attr: TokenStream, item: TokenStream) -> TokenStream {
230    let input = parse_macro_input!(item as ItemFn);
231    let vis = &input.vis;
232    let sig: &syn::Signature = &input.sig;
233    let block = &input.block;
234    let fn_name = &sig.ident;
235
236    let expanded = quote! {
237        #vis fn #fn_name(
238            req: &mut feather::Request,
239            res: &mut feather::Response,
240            ctx: &feather::AppContext
241        ) -> feather::Outcome {
242            #block
243        }
244    };
245    TokenStream::from(expanded)
246}
247
248/// Attribute macro for creating JWT-protected middleware.
249///
250/// Combines with `#[middleware_fn]` to automatically extract and validate JWT claims
251/// from the `Authorization` header. Only works with `#[middleware_fn]`.
252///
253/// # How It Works
254///
255/// This macro:
256/// 1. Extracts the JWT token from the `Authorization: Bearer <token>` header
257/// 2. Decodes and validates the token using the app's JWT manager
258/// 3. Validates claims using the `Claim` trait
259/// 4. Injects the decoded claims into your function
260///
261/// If any step fails, it returns a 401 Unauthorized response automatically.
262///
263/// # Syntax
264///
265/// ```rust,ignore
266/// #[jwt_required]
267/// #[middleware_fn]
268/// fn your_handler(claims: YourClaimsType) {
269///     // claims are now available
270///     next!()
271/// }
272/// ```
273///
274/// # Example: Protecting a Route
275///
276/// ```rust,ignore
277/// use feather::{jwt_required, middleware_fn, Claim};
278///
279/// #[derive(Claim, Clone)]
280/// struct AuthClaims {
281///     #[required]
282///     user_id: String,
283///     username: String,
284/// }
285///
286/// #[jwt_required]
287/// #[middleware_fn]
288/// fn protected_profile() {
289///     res.send_text(format!("Profile for: {}", claims.username));
290///     next!()
291/// }
292///
293/// let mut app = App::new();
294/// app.get("/profile", protected_profile);
295/// ```
296///
297/// # Example: With SimpleClaims
298///
299/// ```rust,ignore
300/// use feather::{jwt_required, middleware_fn};
301/// use feather::jwt::SimpleClaims;
302///
303/// #[jwt_required]
304/// #[middleware_fn]
305/// fn get_user() {
306///     res.send_text(format!("User: {}", claims.sub));
307///     next!()
308/// }
309/// ```
310///
311/// # Example: Accessing Claim Fields
312///
313/// ```rust,ignore
314/// #[jwt_required]
315/// #[middleware_fn]
316/// fn protected_route(claims: AuthClaims) {
317///     // Access claim fields
318///     let user_id = &claims.user_id;
319///     let username = &claims.username;
320///     
321///     // Store in response or context
322///     ctx.set_state(State::new(user_id.clone()));
323///     res.send_text(format!("Welcome, {}!", username));
324///     next!()
325/// }
326/// ```
327///
328/// # Integration with the App
329///
330/// Remember to configure the JWT manager:
331/// ```rust,ignore
332/// use feather::App;
333/// use feather::jwt::JwtManager;
334///
335/// let mut app = App::new();
336/// let jwt_manager = JwtManager::new("your-secret-key");
337/// app.context().set_state(State::new(jwt_manager));
338/// ```
339///
340/// # Error Handling
341///
342/// Automatic 401 responses are sent if:
343/// - `Authorization` header is missing or malformed
344/// - Token is invalid or expired
345/// - Claims fail validation
346///
347/// To customize error responses, use `#[middleware_fn]` with manual JWT handling.
348///
349/// # See Also
350///
351/// - [`#[middleware_fn]`](attr.middleware_fn.html) - The companion macro required with `#[jwt_required]`
352/// - [`JwtManager`](https://docs.rs/feather/latest/feather/jwt/struct.JwtManager.html) - JWT token management
353/// - [Authentication Guide](https://docs.rs/feather/latest/feather/guides/authentication/) - JWT patterns and examples
354#[cfg(feature = "jwt")]
355#[proc_macro_attribute]
356pub fn jwt_required(_attr: TokenStream, item: TokenStream) -> TokenStream {
357    let input = parse_macro_input!(item as ItemFn);
358    let fn_name = &input.sig.ident;
359    let vis = &input.vis;
360    let block = &input.block;
361    let inputs = &input.sig.inputs;
362
363    let claims_ident = inputs.iter().find_map(|arg| {
364        if let syn::FnArg::Typed(pat_type) = arg {
365            if let syn::Pat::Ident(ident) = &*pat_type.pat {
366                Some((&ident.ident, &*pat_type.ty))
367            } else {
368                None
369            }
370        } else {
371            None
372        }
373    });
374
375    let (claims_name, claims_type) = match claims_ident {
376        Some(x) => x,
377        None => {
378            return syn::Error::new_spanned(&input.sig, "expected a `claims: T` argument for #[jwt_required]").to_compile_error().into();
379        }
380    };
381
382    let expanded = quote! {
383        #vis fn #fn_name(req: &mut feather::Request, res: &mut feather::Response, ctx: &feather::AppContext) -> feather::Outcome {
384            let manager = ctx.jwt();
385            let token = match req
386                .headers
387                .get("Authorization")
388                .and_then(|h| h.to_str().ok())
389                .and_then(|h| h.strip_prefix("Bearer ")) {
390                    Some(t) => t,
391                    None => {
392                        res.set_status(401);
393                        res.send_text("Missing or invalid Authorization header");
394                        return feather::next!();
395                    }
396                };
397
398            let #claims_name: #claims_type = match manager.decode(token) {
399                Ok(c) => c,
400                Err(_) => {
401                    res.set_status(401);
402                    res.send_text("Invalid or expired token");
403                    return feather::next!();
404                }
405            };
406
407            if let Err(_) = #claims_name.validate() {
408                res.set_status(401);
409                res.send_text("Invalid or expired token");
410                return feather::next!();
411            }
412
413            #block
414        }
415    };
416
417    TokenStream::from(expanded)
418}