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}