lambda_lw_http_router_macro/
lib.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
use proc_macro::TokenStream;
use darling::{Error, FromMeta};
use darling::ast::NestedMeta;
use quote::{quote, format_ident};
use syn::ItemFn;
use syn::spanned::Spanned;

#[derive(Debug, FromMeta)]
struct RouteArgs {
    path: String,
    #[darling(default = "default_method")]
    method: String,
    #[darling(default = "default_module_name")]
    module: String,
    #[darling(default)]
    set_span_name: Option<bool>,
}

fn default_method() -> String {
    "GET".to_string()
}

fn default_module_name() -> String {
    "__lambda_lw_http_router_core_default_router".to_string()
}

/// Defines a route handler for Lambda HTTP events.
/// 
/// This attribute macro registers a function as a route handler in the router registry.
/// The function will be called when an incoming request matches the specified path and method.
/// Route handlers are registered at compile time, ensuring zero runtime overhead for route setup.
/// 
/// # Arguments
/// 
/// * `path` - The URL path to match (required). Supports path parameters like `{param_name}`
/// * `method` - The HTTP method to match (optional, defaults to "GET")
/// * `module` - The router module name (optional, defaults to internal name)
/// 
/// # Function Signature
/// 
/// The handler function must have exactly one parameter of type RouteContext:
/// 
/// ```rust,ignore
/// #[route(path = "/hello")]
/// async fn handle_hello(ctx: RouteContext) -> Result<Value, Error> {
///     Ok(json!({ "message": "Hello, World!" }))
/// }
/// ```
/// 
/// # Path Parameters
/// 
/// Path parameters are defined using curly braces and are available in the `RouteContext.params`:
/// * `/users/{id}` - Matches `/users/123` and provides `id = "123"`
/// * `/posts/{category}/{slug}` - Matches `/posts/tech/my-post`
/// 
/// # Examples
/// 
/// Route with path parameters and custom method:
/// ```rust,ignore
/// use lambda_lw_http_router::{route, define_router};
/// use aws_lambda_events::apigw::ApiGatewayV2httpRequest;
/// use serde_json::{json, Value};
/// use lambda_runtime::Error;
/// 
/// #[derive(Clone)]
/// struct AppState {
///     // your state fields here
/// }
/// 
/// define_router!(event = ApiGatewayV2httpRequest, state = AppState);
/// 
/// #[route(path = "/users/{id}", method = "POST", state = AppState)]
/// async fn create_user(ctx: RouteContext) -> Result<Value, Error> {
///     let user_id = ctx.params.get("id").unwrap();
///     Ok(json!({ "created": user_id }))
/// }
/// ```
#[proc_macro_attribute]
pub fn route(args: TokenStream, input: TokenStream) -> TokenStream {
    let attr_args = match NestedMeta::parse_meta_list(args.into()) {
        Ok(v) => v,
        Err(e) => { return TokenStream::from(Error::from(e).write_errors()); }
    };
    let input = syn::parse_macro_input!(input as ItemFn);
    
    let output: TokenStream = impl_router(attr_args, input).into();
    output
}

fn impl_router(args: Vec<NestedMeta>, input: ItemFn) -> proc_macro2::TokenStream {
    let route_args = match RouteArgs::from_list(&args) {
        Ok(v) => v,
        Err(e) => { return TokenStream::from(e.write_errors()).into(); }
    };

    let fn_name = &input.sig.ident;
    let method = &route_args.method;
    let path = &route_args.path;
    let module = format_ident!("{}", route_args.module);
    let register_fn = format_ident!("__register_{}", fn_name);

    // Validate function signature
    if input.sig.inputs.len() != 1 {
        return syn::Error::new(
            input.sig.span(),
            "Route handler must have exactly one parameter of type RouteContext"
        ).to_compile_error();
    }

    // Extract and validate the parameter type
    let param = input.sig.inputs.first().unwrap();
    match param {
        syn::FnArg::Typed(pat_type) => {
            match &*pat_type.ty {
                syn::Type::Path(type_path) => {
                    let last_segment = type_path.path.segments.last()
                        .ok_or_else(|| syn::Error::new(
                            type_path.span(),
                            "Invalid parameter type"
                        )).unwrap();
                    
                    if last_segment.ident != "RouteContext" {
                        return syn::Error::new(
                            type_path.span(),
                            "Parameter must be of type RouteContext"
                        ).to_compile_error();
                    }
                }
                _ => {
                    return syn::Error::new(
                        pat_type.ty.span(),
                        "Parameter must be of type RouteContext"
                    ).to_compile_error();
                }
            }
        }
        _ => {
            return syn::Error::new(
                param.span(),
                "Invalid parameter declaration"
            ).to_compile_error();
        }
    }

    // Default to true if not specified and otel feature is enabled
    let set_span_name = route_args.set_span_name.unwrap_or(true);
    let span_setting = if set_span_name {
        quote! {
            ctx.set_otel_span_name();
        }
    } else {
        quote! {}
    };

    let output = quote! {
        #[::lambda_lw_http_router::ctor::ctor]
        fn #register_fn() {
            ::lambda_lw_http_router::register_route::<#module::State, #module::Event>(
                #method,
                #path,
                |ctx| Box::pin(async move {
                    #span_setting
                    #fn_name(ctx).await
                })
            );
        }

        #input
    };
    output
}