1use 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
11pub type RouteHandler = Arc<dyn Fn() -> String + Send + Sync>;
13
14#[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 pub expects_json_body: bool,
32 #[cfg(feature = "di")]
34 pub handler_dependencies: Vec<String>,
35}
36
37impl Route {
38 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
94pub struct Router {
96 routes: HashMap<String, HashMap<Method, Route>>,
97}
98
99impl Router {
100 pub fn new() -> Self {
102 Self { routes: HashMap::new() }
103 }
104
105 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 pub fn find_route(&self, method: &Method, path: &str) -> Option<&Route> {
113 self.routes.get(path)?.get(method)
114 }
115
116 pub fn routes(&self) -> Vec<&Route> {
118 self.routes.values().flat_map(|methods| methods.values()).collect()
119 }
120
121 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, ®istry).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, ®istry).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, ®istry).unwrap();
238 let route2 = Route::from_metadata(metadata2, ®istry).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}