spikard_core/
router.rs

1//! Route management and handler registration
2
3use crate::parameters::ParameterValidator;
4use crate::schema_registry::SchemaRegistry;
5use crate::validation::SchemaValidator;
6use crate::{CorsConfig, Method, RouteMetadata};
7use serde_json::Value;
8use std::collections::HashMap;
9use std::sync::Arc;
10
11/// Handler function type (placeholder - will be enhanced with Python callbacks)
12pub type RouteHandler = Arc<dyn Fn() -> String + Send + Sync>;
13
14/// Route definition with compiled validators
15///
16/// Validators are Arc-wrapped to enable cheap cloning across route instances
17/// and to support schema deduplication via SchemaRegistry.
18#[derive(Clone)]
19pub struct Route {
20    pub method: Method,
21    pub path: String,
22    pub handler_name: String,
23    pub request_validator: Option<Arc<SchemaValidator>>,
24    pub response_validator: Option<Arc<SchemaValidator>>,
25    pub parameter_validator: Option<ParameterValidator>,
26    pub file_params: Option<Value>,
27    pub is_async: bool,
28    pub cors: Option<CorsConfig>,
29    /// Precomputed flag: true if this route expects a JSON request body
30    /// Used by middleware to validate Content-Type headers
31    pub expects_json_body: bool,
32    /// List of dependency keys this handler requires (for DI)
33    #[cfg(feature = "di")]
34    pub handler_dependencies: Vec<String>,
35}
36
37impl Route {
38    /// Create a route from metadata, using schema registry for deduplication
39    ///
40    /// Auto-generates parameter schema from type hints in the path if no explicit schema provided.
41    /// Type hints like `/items/{id:uuid}` generate appropriate JSON Schema validation.
42    /// Explicit parameter_schema overrides auto-generated schemas.
43    ///
44    /// The schema registry ensures each unique schema is compiled only once, improving
45    /// startup performance and memory usage for applications with many routes.
46    pub fn from_metadata(metadata: RouteMetadata, registry: &SchemaRegistry) -> Result<Self, String> {
47        let method = metadata.method.parse()?;
48
49        let request_validator = metadata
50            .request_schema
51            .as_ref()
52            .map(|schema| registry.get_or_compile(schema))
53            .transpose()?;
54
55        let response_validator = metadata
56            .response_schema
57            .as_ref()
58            .map(|schema| registry.get_or_compile(schema))
59            .transpose()?;
60
61        let final_parameter_schema = match (
62            crate::type_hints::auto_generate_parameter_schema(&metadata.path),
63            metadata.parameter_schema,
64        ) {
65            (Some(auto_schema), Some(explicit_schema)) => {
66                Some(crate::type_hints::merge_parameter_schemas(auto_schema, explicit_schema))
67            }
68            (Some(auto_schema), None) => Some(auto_schema),
69            (None, Some(explicit_schema)) => Some(explicit_schema),
70            (None, None) => None,
71        };
72
73        let parameter_validator = final_parameter_schema.map(ParameterValidator::new).transpose()?;
74
75        let expects_json_body = request_validator.is_some();
76
77        Ok(Self {
78            method,
79            path: metadata.path,
80            handler_name: metadata.handler_name,
81            request_validator,
82            response_validator,
83            parameter_validator,
84            file_params: metadata.file_params,
85            is_async: metadata.is_async,
86            cors: metadata.cors,
87            expects_json_body,
88            #[cfg(feature = "di")]
89            handler_dependencies: metadata.handler_dependencies.unwrap_or_default(),
90        })
91    }
92}
93
94/// Router that manages routes
95pub struct Router {
96    routes: HashMap<String, HashMap<Method, Route>>,
97}
98
99impl Router {
100    /// Create a new router
101    pub fn new() -> Self {
102        Self { routes: HashMap::new() }
103    }
104
105    /// Add a route to the router
106    pub fn add_route(&mut self, route: Route) {
107        let path_routes = self.routes.entry(route.path.clone()).or_default();
108        path_routes.insert(route.method.clone(), route);
109    }
110
111    /// Find a route by method and path
112    pub fn find_route(&self, method: &Method, path: &str) -> Option<&Route> {
113        self.routes.get(path)?.get(method)
114    }
115
116    /// Get all routes
117    pub fn routes(&self) -> Vec<&Route> {
118        self.routes.values().flat_map(|methods| methods.values()).collect()
119    }
120
121    /// Get route count
122    pub fn route_count(&self) -> usize {
123        self.routes.values().map(|m| m.len()).sum()
124    }
125}
126
127impl Default for Router {
128    fn default() -> Self {
129        Self::new()
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136    use serde_json::json;
137
138    #[test]
139    fn test_router_add_and_find() {
140        let mut router = Router::new();
141        let registry = SchemaRegistry::new();
142
143        let metadata = RouteMetadata {
144            method: "GET".to_string(),
145            path: "/users".to_string(),
146            handler_name: "get_users".to_string(),
147            request_schema: None,
148            response_schema: None,
149            parameter_schema: None,
150            file_params: None,
151            is_async: true,
152            cors: None,
153            body_param_name: None,
154            #[cfg(feature = "di")]
155            handler_dependencies: None,
156        };
157
158        let route = Route::from_metadata(metadata, &registry).unwrap();
159        router.add_route(route);
160
161        assert_eq!(router.route_count(), 1);
162        assert!(router.find_route(&Method::Get, "/users").is_some());
163        assert!(router.find_route(&Method::Post, "/users").is_none());
164    }
165
166    #[test]
167    fn test_route_with_validators() {
168        let registry = SchemaRegistry::new();
169
170        let metadata = RouteMetadata {
171            method: "POST".to_string(),
172            path: "/users".to_string(),
173            handler_name: "create_user".to_string(),
174            request_schema: Some(json!({
175                "type": "object",
176                "properties": {
177                    "name": {"type": "string"}
178                },
179                "required": ["name"]
180            })),
181            response_schema: None,
182            parameter_schema: None,
183            file_params: None,
184            is_async: true,
185            cors: None,
186            body_param_name: None,
187            #[cfg(feature = "di")]
188            handler_dependencies: None,
189        };
190
191        let route = Route::from_metadata(metadata, &registry).unwrap();
192        assert!(route.request_validator.is_some());
193        assert!(route.response_validator.is_none());
194    }
195
196    #[test]
197    fn test_schema_deduplication_in_routes() {
198        let registry = SchemaRegistry::new();
199
200        let shared_schema = json!({
201            "type": "object",
202            "properties": {
203                "id": {"type": "integer"}
204            }
205        });
206
207        let metadata1 = RouteMetadata {
208            method: "POST".to_string(),
209            path: "/items".to_string(),
210            handler_name: "create_item".to_string(),
211            request_schema: Some(shared_schema.clone()),
212            response_schema: None,
213            parameter_schema: None,
214            file_params: None,
215            is_async: true,
216            cors: None,
217            body_param_name: None,
218            #[cfg(feature = "di")]
219            handler_dependencies: None,
220        };
221
222        let metadata2 = RouteMetadata {
223            method: "PUT".to_string(),
224            path: "/items/{id}".to_string(),
225            handler_name: "update_item".to_string(),
226            request_schema: Some(shared_schema),
227            response_schema: None,
228            parameter_schema: None,
229            file_params: None,
230            is_async: true,
231            cors: None,
232            body_param_name: None,
233            #[cfg(feature = "di")]
234            handler_dependencies: None,
235        };
236
237        let route1 = Route::from_metadata(metadata1, &registry).unwrap();
238        let route2 = Route::from_metadata(metadata2, &registry).unwrap();
239
240        assert!(route1.request_validator.is_some());
241        assert!(route2.request_validator.is_some());
242
243        let validator1 = route1.request_validator.as_ref().unwrap();
244        let validator2 = route2.request_validator.as_ref().unwrap();
245        assert!(Arc::ptr_eq(validator1, validator2));
246
247        assert_eq!(registry.schema_count(), 1);
248    }
249}