Skip to main content

lambda_lw_http_router_macro/
lib.rs

1//! Procedural macros for the lambda-lw-http-router crate.
2//!
3//! **Note**: This is a proc-macro implementation crate for [lambda-lw-http-router](https://crates.io/crates/lambda-lw-http-router)
4//! and is not meant to be used directly. Please use the main crate instead.
5//!
6//! The macros in this crate are re-exported by the main crate, and using them directly
7//! may lead to version conflicts or other issues. Additionally, this crate's API is not
8//! guaranteed to be stable between minor versions.
9//!
10//! # Usage
11//!
12//! Instead of using this crate directly, use the main crate:
13//!
14//! ```toml
15//! [dependencies]
16//! lambda-lw-http-router = "0.6.0"
17//! ```
18//!
19//! See the [lambda-lw-http-router documentation](https://docs.rs/lambda-lw-http-router)
20//! for more information on how to use the router.
21
22use darling::{ast::NestedMeta, Error, FromMeta};
23use proc_macro::TokenStream;
24use quote::{format_ident, quote};
25use syn::spanned::Spanned;
26use syn::ItemFn;
27
28#[derive(Debug, FromMeta)]
29struct RouteArgs {
30    path: String,
31    #[darling(default = "default_method")]
32    method: String,
33    #[darling(default = "default_module_name")]
34    module: String,
35}
36
37fn default_method() -> String {
38    "GET".to_string()
39}
40
41fn default_module_name() -> String {
42    String::new() // Default to empty, indicating types are in the current/crate scope
43}
44
45/// Defines a route handler for Lambda HTTP events.
46///
47/// This attribute macro registers a function as a route handler in the router registry.
48/// The function will be called when an incoming request matches the specified path and method.
49/// Route handlers are registered at compile time, ensuring zero runtime overhead for route setup.
50///
51/// # Arguments
52///
53/// * `path` - The URL path to match (required). Supports path parameters like `{param_name}`
54/// * `method` - The HTTP method to match (optional, defaults to "GET")
55/// * `module` - The router module name (optional, defaults to internal name)
56///
57/// # Function Signature
58///
59/// The handler function must have exactly one parameter of type RouteContext:
60///
61/// ```rust,ignore
62/// #[route(path = "/hello")]
63/// async fn handle_hello(ctx: RouteContext) -> Result<Value, Error> {
64///     Ok(json!({ "message": "Hello, World!" }))
65/// }
66/// ```
67///
68/// # Path Parameters
69///
70/// Path parameters are defined using curly braces and are available in the `RouteContext.params`:
71/// * `/users/{id}` - Matches `/users/123` and provides `id = "123"`
72/// * `/posts/{category}/{slug}` - Matches `/posts/tech/my-post`
73///
74/// # Examples
75///
76/// Route with path parameters and custom method:
77/// ```rust,ignore
78/// use lambda_lw_http_router::{route, define_router};
79/// use aws_lambda_events::apigw::ApiGatewayV2httpRequest;
80/// use serde_json::{json, Value};
81/// use lambda_runtime::Error;
82///
83/// #[derive(Clone)]
84/// struct AppState {
85///     // your state fields here
86/// }
87///
88/// define_router!(event = ApiGatewayV2httpRequest, state = AppState);
89///
90/// #[route(path = "/users/{id}", method = "POST", state = AppState)]
91/// async fn create_user(ctx: RouteContext) -> Result<Value, Error> {
92///     let user_id = ctx.params.get("id").unwrap();
93///     Ok(json!({ "created": user_id }))
94/// }
95/// ```
96#[proc_macro_attribute]
97pub fn route(args: TokenStream, input: TokenStream) -> TokenStream {
98    let attr_args = match NestedMeta::parse_meta_list(args.into()) {
99        Ok(v) => v,
100        Err(e) => {
101            return TokenStream::from(Error::from(e).write_errors());
102        }
103    };
104    let input = syn::parse_macro_input!(input as ItemFn);
105
106    let output: TokenStream = impl_router(attr_args, input).into();
107    output
108}
109
110fn impl_router(args: Vec<NestedMeta>, input: ItemFn) -> proc_macro2::TokenStream {
111    let route_args = match RouteArgs::from_list(&args) {
112        Ok(v) => v,
113        Err(e) => {
114            return TokenStream::from(e.write_errors()).into();
115        }
116    };
117
118    let fn_name = &input.sig.ident;
119    let method = &route_args.method;
120    let path = &route_args.path;
121    let register_fn = format_ident!("__register_{}", fn_name);
122
123    // Validate function signature
124    if input.sig.inputs.len() != 1 {
125        return syn::Error::new(
126            input.sig.span(),
127            "Route handler must have exactly one parameter of type RouteContext",
128        )
129        .to_compile_error();
130    }
131
132    // Extract and validate the parameter type
133    let param = input.sig.inputs.first().unwrap();
134    match param {
135        syn::FnArg::Typed(pat_type) => match &*pat_type.ty {
136            syn::Type::Path(type_path) => {
137                let last_segment = type_path
138                    .path
139                    .segments
140                    .last()
141                    .ok_or_else(|| syn::Error::new(type_path.span(), "Invalid parameter type"))
142                    .unwrap();
143
144                if last_segment.ident != "RouteContext" {
145                    return syn::Error::new(
146                        type_path.span(),
147                        "Parameter must be of type RouteContext",
148                    )
149                    .to_compile_error();
150                }
151            }
152            _ => {
153                return syn::Error::new(
154                    pat_type.ty.span(),
155                    "Parameter must be of type RouteContext",
156                )
157                .to_compile_error();
158            }
159        },
160        _ => {
161            return syn::Error::new(param.span(), "Invalid parameter declaration")
162                .to_compile_error();
163        }
164    }
165
166    let (state_path, event_path) = if route_args.module.is_empty() {
167        (quote! { crate::State }, quote! { crate::Event })
168    } else {
169        let mod_ident = format_ident!("{}", route_args.module);
170        (quote! { #mod_ident::State }, quote! { #mod_ident::Event })
171    };
172
173    let output = quote! {
174        #[::lambda_lw_http_router::ctor_attribute(crate_path = ::lambda_lw_http_router::ctor)]
175        fn #register_fn() {
176            ::lambda_lw_http_router::register_route::<#state_path, #event_path>(
177                #method,
178                #path,
179                |ctx| Box::pin(async move {
180                    #fn_name(ctx).await
181                })
182            );
183        }
184        #input
185    };
186    output
187}