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.1"
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;
25mod routable_http_event;
26mod route_context;
27mod router;
28pub use routable_http_event::RoutableHttpEvent;
29pub use route_context::RouteContext;
30pub use router::{register_route, Router, RouterBuilder};
31
32#[cfg(test)]
33mod tests {
34    use super::*;
35    use aws_lambda_events::apigw::ApiGatewayProxyRequest;
36    use aws_lambda_events::http::Method;
37    use lambda_runtime::LambdaEvent;
38    use serde_json::json;
39    use std::collections::HashMap;
40    use std::sync::Arc;
41
42    /// Test event struct that implements RoutableHttpEvent
43    #[derive(Clone)]
44    struct TestHttpEvent {
45        path: String,
46        method: String,
47    }
48
49    impl RoutableHttpEvent for TestHttpEvent {
50        fn path(&self) -> Option<String> {
51            Some(self.path.clone())
52        }
53
54        fn http_method(&self) -> String {
55            self.method.clone()
56        }
57    }
58
59    /// Simple state struct for testing
60    #[derive(Clone)]
61    struct TestState {}
62
63    #[tokio::test]
64    async fn test_path_parameter_extraction() {
65        let mut router = Router::<TestState, TestHttpEvent>::new();
66
67        // Register a route with path parameters
68        router.register_route("GET", "/users/{id}/posts/{post_id}", |ctx| async move {
69            Ok(json!({
70                "user_id": ctx.params.get("id"),
71                "post_id": ctx.params.get("post_id"),
72            }))
73        });
74
75        // Create a test event
76        let event = TestHttpEvent {
77            path: "/users/123/posts/456".to_string(),
78            method: "GET".to_string(),
79        };
80        let lambda_context = lambda_runtime::Context::default();
81        let lambda_event = LambdaEvent::new(event, lambda_context);
82
83        // Handle the request
84        let result = router
85            .handle_request(lambda_event, Arc::new(TestState {}))
86            .await
87            .unwrap();
88
89        // Verify the extracted parameters
90        assert_eq!(result["user_id"], "123");
91        assert_eq!(result["post_id"], "456");
92    }
93
94    #[tokio::test]
95    async fn test_greedy_path_parameter() {
96        let mut router = Router::<TestState, TestHttpEvent>::new();
97
98        // Register a route with a greedy path parameter
99        router.register_route("GET", "/files/{path+}", |ctx| async move {
100            Ok(json!({
101                "path": ctx.params.get("path"),
102            }))
103        });
104
105        // Create a test event with a nested path
106        let event = TestHttpEvent {
107            path: "/files/documents/2024/report.pdf".to_string(),
108            method: "GET".to_string(),
109        };
110        let lambda_context = lambda_runtime::Context::default();
111        let lambda_event = LambdaEvent::new(event, lambda_context);
112
113        // Handle the request
114        let result = router
115            .handle_request(lambda_event, Arc::new(TestState {}))
116            .await
117            .unwrap();
118
119        // Verify the extracted parameter captures the full path
120        assert_eq!(result["path"], "documents/2024/report.pdf");
121    }
122
123    #[tokio::test]
124    async fn test_no_match_returns_404() {
125        let router = Router::<TestState, TestHttpEvent>::new();
126
127        // Create a test event with a path that doesn't match any routes
128        let event = TestHttpEvent {
129            path: "/nonexistent".to_string(),
130            method: "GET".to_string(),
131        };
132        let lambda_context = lambda_runtime::Context::default();
133        let lambda_event = LambdaEvent::new(event, lambda_context);
134
135        // Handle the request
136        let result = router
137            .handle_request(lambda_event, Arc::new(TestState {}))
138            .await
139            .unwrap();
140
141        // Verify we get a 404 response
142        assert_eq!(result["statusCode"], 404);
143    }
144
145    #[tokio::test]
146    async fn test_apigw_resource_path_parameters() {
147        let mut router = Router::<TestState, ApiGatewayProxyRequest>::new();
148
149        router.register_route("GET", "/users/{id}/posts/{post_id}", |ctx| async move {
150            Ok(json!({
151                "params": ctx.params,
152            }))
153        });
154
155        let mut path_parameters = HashMap::new();
156        path_parameters.insert("id".to_string(), "123".to_string());
157        path_parameters.insert("post_id".to_string(), "456".to_string());
158
159        let event = ApiGatewayProxyRequest {
160            path: Some("/users/123/posts/456".to_string()),
161            http_method: Method::GET,
162            resource: Some("/users/{id}/posts/{post_id}".to_string()),
163            path_parameters,
164            ..Default::default()
165        };
166
167        let lambda_context = lambda_runtime::Context::default();
168        let lambda_event = LambdaEvent::new(event, lambda_context);
169
170        let result = router
171            .handle_request(lambda_event, Arc::new(TestState {}))
172            .await
173            .unwrap();
174
175        assert_eq!(result["params"]["id"], "123");
176        assert_eq!(result["params"]["post_id"], "456");
177    }
178
179    #[tokio::test]
180    async fn test_method_matching_with_apigw() {
181        let mut router = Router::<TestState, ApiGatewayProxyRequest>::new();
182
183        // Register both GET and POST handlers for the same path
184        router.register_route("GET", "/quotes", |_| async move {
185            Ok(json!({ "method": "GET" }))
186        });
187
188        router.register_route("POST", "/quotes", |_| async move {
189            Ok(json!({ "method": "POST" }))
190        });
191
192        // Create a POST request
193        let post_event = ApiGatewayProxyRequest {
194            path: Some("/quotes".to_string()),
195            http_method: Method::POST,
196            resource: Some("/quotes".to_string()),
197            path_parameters: HashMap::new(),
198            ..Default::default()
199        };
200
201        let lambda_context = lambda_runtime::Context::default();
202        let lambda_event = LambdaEvent::new(post_event, lambda_context);
203
204        // Handle the POST request
205        let result = router
206            .handle_request(lambda_event, Arc::new(TestState {}))
207            .await
208            .unwrap();
209        assert_eq!(
210            result["method"], "POST",
211            "POST request should be handled by POST handler"
212        );
213
214        // Create a GET request to the same path
215        let get_event = ApiGatewayProxyRequest {
216            path: Some("/quotes".to_string()),
217            http_method: Method::GET,
218            resource: Some("/quotes".to_string()),
219            path_parameters: HashMap::new(),
220            ..Default::default()
221        };
222
223        let lambda_context = lambda_runtime::Context::default();
224        let lambda_event = LambdaEvent::new(get_event, lambda_context);
225
226        // Handle the GET request
227        let result = router
228            .handle_request(lambda_event, Arc::new(TestState {}))
229            .await
230            .unwrap();
231        assert_eq!(
232            result["method"], "GET",
233            "GET request should be handled by GET handler"
234        );
235    }
236}