Skip to main content

lambda_lw_http_router_core/
lib.rs

1#![allow(clippy::type_complexity)]
2
3//! Core functionality for the lambda-lw-http-router crate.
4//!
5//! **Note**: This is an implementation crate for [lambda-lw-http-router](https://crates.io/crates/lambda-lw-http-router)
6//! and is not meant to be used directly. Please use the main crate instead.
7//!
8//! The functionality in this crate is re-exported by the main crate, and using it directly
9//! may lead to version conflicts or other issues. Additionally, this crate's API is not
10//! guaranteed to be stable between minor versions.
11//!
12//! # Usage
13//!
14//! Instead of using this crate directly, use the main crate:
15//!
16//! ```toml
17//! [dependencies]
18//! lambda-lw-http-router = "0.6.0"
19//! ```
20//!
21//! See the [lambda-lw-http-router documentation](https://docs.rs/lambda-lw-http-router)
22//! for more information on how to use the router.
23
24pub use ctor;
25pub use ctor::ctor as ctor_attribute;
26mod routable_http_event;
27mod route_context;
28mod router;
29pub use routable_http_event::RoutableHttpEvent;
30pub use route_context::RouteContext;
31pub use router::{register_route, Router, RouterBuilder};
32
33#[cfg(test)]
34mod tests {
35    use super::*;
36    use aws_lambda_events::apigw::ApiGatewayProxyRequest;
37    use aws_lambda_events::http::Method;
38    use lambda_runtime::LambdaEvent;
39    use serde_json::json;
40    use std::collections::HashMap;
41    use std::sync::Arc;
42
43    /// Test event struct that implements RoutableHttpEvent
44    #[derive(Clone)]
45    struct TestHttpEvent {
46        path: String,
47        method: String,
48    }
49
50    impl RoutableHttpEvent for TestHttpEvent {
51        fn path(&self) -> Option<String> {
52            Some(self.path.clone())
53        }
54
55        fn http_method(&self) -> String {
56            self.method.clone()
57        }
58    }
59
60    /// Simple state struct for testing
61    #[derive(Clone)]
62    struct TestState {}
63
64    #[tokio::test]
65    async fn test_path_parameter_extraction() {
66        let mut router = Router::<TestState, TestHttpEvent>::new();
67
68        // Register a route with path parameters
69        router.register_route("GET", "/users/{id}/posts/{post_id}", |ctx| async move {
70            Ok(json!({
71                "user_id": ctx.params.get("id"),
72                "post_id": ctx.params.get("post_id"),
73            }))
74        });
75
76        // Create a test event
77        let event = TestHttpEvent {
78            path: "/users/123/posts/456".to_string(),
79            method: "GET".to_string(),
80        };
81        let lambda_context = lambda_runtime::Context::default();
82        let lambda_event = LambdaEvent::new(event, lambda_context);
83
84        // Handle the request
85        let result = router
86            .handle_request(lambda_event, Arc::new(TestState {}))
87            .await
88            .unwrap();
89
90        // Verify the extracted parameters
91        assert_eq!(result["user_id"], "123");
92        assert_eq!(result["post_id"], "456");
93    }
94
95    #[tokio::test]
96    async fn test_greedy_path_parameter() {
97        let mut router = Router::<TestState, TestHttpEvent>::new();
98
99        // Register a route with a greedy path parameter
100        router.register_route("GET", "/files/{path+}", |ctx| async move {
101            Ok(json!({
102                "path": ctx.params.get("path"),
103            }))
104        });
105
106        // Create a test event with a nested path
107        let event = TestHttpEvent {
108            path: "/files/documents/2024/report.pdf".to_string(),
109            method: "GET".to_string(),
110        };
111        let lambda_context = lambda_runtime::Context::default();
112        let lambda_event = LambdaEvent::new(event, lambda_context);
113
114        // Handle the request
115        let result = router
116            .handle_request(lambda_event, Arc::new(TestState {}))
117            .await
118            .unwrap();
119
120        // Verify the extracted parameter captures the full path
121        assert_eq!(result["path"], "documents/2024/report.pdf");
122    }
123
124    #[tokio::test]
125    async fn test_no_match_returns_404() {
126        let router = Router::<TestState, TestHttpEvent>::new();
127
128        // Create a test event with a path that doesn't match any routes
129        let event = TestHttpEvent {
130            path: "/nonexistent".to_string(),
131            method: "GET".to_string(),
132        };
133        let lambda_context = lambda_runtime::Context::default();
134        let lambda_event = LambdaEvent::new(event, lambda_context);
135
136        // Handle the request
137        let result = router
138            .handle_request(lambda_event, Arc::new(TestState {}))
139            .await
140            .unwrap();
141
142        // Verify we get a 404 response
143        assert_eq!(result["statusCode"], 404);
144    }
145
146    #[tokio::test]
147    async fn test_apigw_resource_path_parameters() {
148        let mut router = Router::<TestState, ApiGatewayProxyRequest>::new();
149
150        router.register_route("GET", "/users/{id}/posts/{post_id}", |ctx| async move {
151            Ok(json!({
152                "params": ctx.params,
153            }))
154        });
155
156        let mut path_parameters = HashMap::new();
157        path_parameters.insert("id".to_string(), "123".to_string());
158        path_parameters.insert("post_id".to_string(), "456".to_string());
159
160        let mut event = ApiGatewayProxyRequest::default();
161        event.path = Some("/users/123/posts/456".to_string());
162        event.http_method = Method::GET;
163        event.resource = Some("/users/{id}/posts/{post_id}".to_string());
164        event.path_parameters = path_parameters;
165
166        let lambda_context = lambda_runtime::Context::default();
167        let lambda_event = LambdaEvent::new(event, lambda_context);
168
169        let result = router
170            .handle_request(lambda_event, Arc::new(TestState {}))
171            .await
172            .unwrap();
173
174        assert_eq!(result["params"]["id"], "123");
175        assert_eq!(result["params"]["post_id"], "456");
176    }
177
178    #[tokio::test]
179    async fn test_method_matching_with_apigw() {
180        let mut router = Router::<TestState, ApiGatewayProxyRequest>::new();
181
182        // Register both GET and POST handlers for the same path
183        router.register_route("GET", "/quotes", |_| async move {
184            Ok(json!({ "method": "GET" }))
185        });
186
187        router.register_route("POST", "/quotes", |_| async move {
188            Ok(json!({ "method": "POST" }))
189        });
190
191        // Create a POST request
192        let mut post_event = ApiGatewayProxyRequest::default();
193        post_event.path = Some("/quotes".to_string());
194        post_event.http_method = Method::POST;
195        post_event.resource = Some("/quotes".to_string());
196        post_event.path_parameters = HashMap::new();
197
198        let lambda_context = lambda_runtime::Context::default();
199        let lambda_event = LambdaEvent::new(post_event, lambda_context);
200
201        // Handle the POST request
202        let result = router
203            .handle_request(lambda_event, Arc::new(TestState {}))
204            .await
205            .unwrap();
206        assert_eq!(
207            result["method"], "POST",
208            "POST request should be handled by POST handler"
209        );
210
211        // Create a GET request to the same path
212        let mut get_event = ApiGatewayProxyRequest::default();
213        get_event.path = Some("/quotes".to_string());
214        get_event.http_method = Method::GET;
215        get_event.resource = Some("/quotes".to_string());
216        get_event.path_parameters = HashMap::new();
217
218        let lambda_context = lambda_runtime::Context::default();
219        let lambda_event = LambdaEvent::new(get_event, lambda_context);
220
221        // Handle the GET request
222        let result = router
223            .handle_request(lambda_event, Arc::new(TestState {}))
224            .await
225            .unwrap();
226        assert_eq!(
227            result["method"], "GET",
228            "GET request should be handled by GET handler"
229        );
230    }
231}