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;
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 event = ApiGatewayProxyRequest {
161            path: Some("/users/123/posts/456".to_string()),
162            http_method: Method::GET,
163            resource: Some("/users/{id}/posts/{post_id}".to_string()),
164            path_parameters,
165            ..Default::default()
166        };
167
168        let lambda_context = lambda_runtime::Context::default();
169        let lambda_event = LambdaEvent::new(event, lambda_context);
170
171        let result = router
172            .handle_request(lambda_event, Arc::new(TestState {}))
173            .await
174            .unwrap();
175
176        assert_eq!(result["params"]["id"], "123");
177        assert_eq!(result["params"]["post_id"], "456");
178    }
179
180    #[tokio::test]
181    async fn test_method_matching_with_apigw() {
182        let mut router = Router::<TestState, ApiGatewayProxyRequest>::new();
183
184        // Register both GET and POST handlers for the same path
185        router.register_route("GET", "/quotes", |_| async move {
186            Ok(json!({ "method": "GET" }))
187        });
188
189        router.register_route("POST", "/quotes", |_| async move {
190            Ok(json!({ "method": "POST" }))
191        });
192
193        // Create a POST request
194        let post_event = ApiGatewayProxyRequest {
195            path: Some("/quotes".to_string()),
196            http_method: Method::POST,
197            resource: Some("/quotes".to_string()),
198            path_parameters: HashMap::new(),
199            ..Default::default()
200        };
201
202        let lambda_context = lambda_runtime::Context::default();
203        let lambda_event = LambdaEvent::new(post_event, lambda_context);
204
205        // Handle the POST request
206        let result = router
207            .handle_request(lambda_event, Arc::new(TestState {}))
208            .await
209            .unwrap();
210        assert_eq!(
211            result["method"], "POST",
212            "POST request should be handled by POST handler"
213        );
214
215        // Create a GET request to the same path
216        let get_event = ApiGatewayProxyRequest {
217            path: Some("/quotes".to_string()),
218            http_method: Method::GET,
219            resource: Some("/quotes".to_string()),
220            path_parameters: HashMap::new(),
221            ..Default::default()
222        };
223
224        let lambda_context = lambda_runtime::Context::default();
225        let lambda_event = LambdaEvent::new(get_event, lambda_context);
226
227        // Handle the GET request
228        let result = router
229            .handle_request(lambda_event, Arc::new(TestState {}))
230            .await
231            .unwrap();
232        assert_eq!(
233            result["method"], "GET",
234            "GET request should be handled by GET handler"
235        );
236    }
237}