1use crate::parameters::ParameterValidator;
4use crate::schema_registry::SchemaRegistry;
5use crate::validation::SchemaValidator;
6use crate::{CorsConfig, Method, RouteMetadata};
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9use std::collections::HashMap;
10use std::sync::Arc;
11
12pub type RouteHandler = Arc<dyn Fn() -> String + Send + Sync>;
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct JsonRpcMethodInfo {
47 pub method_name: String,
49
50 #[serde(skip_serializing_if = "Option::is_none")]
52 pub description: Option<String>,
53
54 #[serde(skip_serializing_if = "Option::is_none")]
56 pub params_schema: Option<Value>,
57
58 #[serde(skip_serializing_if = "Option::is_none")]
60 pub result_schema: Option<Value>,
61
62 #[serde(default)]
64 pub deprecated: bool,
65
66 #[serde(default)]
68 pub tags: Vec<String>,
69}
70
71#[derive(Clone)]
79pub struct Route {
80 pub method: Method,
81 pub path: String,
82 pub handler_name: String,
83 pub request_validator: Option<Arc<SchemaValidator>>,
84 pub response_validator: Option<Arc<SchemaValidator>>,
85 pub parameter_validator: Option<ParameterValidator>,
86 pub file_params: Option<Value>,
87 pub is_async: bool,
88 pub cors: Option<CorsConfig>,
89 pub expects_json_body: bool,
92 #[cfg(feature = "di")]
94 pub handler_dependencies: Vec<String>,
95 pub jsonrpc_method: Option<JsonRpcMethodInfo>,
98}
99
100impl Route {
101 pub fn from_metadata(metadata: RouteMetadata, registry: &SchemaRegistry) -> Result<Self, String> {
110 let method = metadata.method.parse()?;
111
112 let request_validator = metadata
113 .request_schema
114 .as_ref()
115 .map(|schema| registry.get_or_compile(schema))
116 .transpose()?;
117
118 let response_validator = metadata
119 .response_schema
120 .as_ref()
121 .map(|schema| registry.get_or_compile(schema))
122 .transpose()?;
123
124 let final_parameter_schema = match (
125 crate::type_hints::auto_generate_parameter_schema(&metadata.path),
126 metadata.parameter_schema,
127 ) {
128 (Some(auto_schema), Some(explicit_schema)) => {
129 Some(crate::type_hints::merge_parameter_schemas(auto_schema, explicit_schema))
130 }
131 (Some(auto_schema), None) => Some(auto_schema),
132 (None, Some(explicit_schema)) => Some(explicit_schema),
133 (None, None) => None,
134 };
135
136 let parameter_validator = final_parameter_schema.map(ParameterValidator::new).transpose()?;
137
138 let expects_json_body = request_validator.is_some();
139
140 let jsonrpc_method = metadata
141 .jsonrpc_method
142 .as_ref()
143 .and_then(|json_value| serde_json::from_value(json_value.clone()).ok());
144
145 Ok(Self {
146 method,
147 path: metadata.path,
148 handler_name: metadata.handler_name,
149 request_validator,
150 response_validator,
151 parameter_validator,
152 file_params: metadata.file_params,
153 is_async: metadata.is_async,
154 cors: metadata.cors,
155 expects_json_body,
156 #[cfg(feature = "di")]
157 handler_dependencies: metadata.handler_dependencies.unwrap_or_default(),
158 jsonrpc_method,
159 })
160 }
161
162 pub fn with_jsonrpc_method(mut self, info: JsonRpcMethodInfo) -> Self {
181 self.jsonrpc_method = Some(info);
182 self
183 }
184
185 pub fn is_jsonrpc_method(&self) -> bool {
187 self.jsonrpc_method.is_some()
188 }
189
190 pub fn jsonrpc_method_name(&self) -> Option<&str> {
192 self.jsonrpc_method.as_ref().map(|m| m.method_name.as_str())
193 }
194}
195
196pub struct Router {
198 routes: HashMap<String, HashMap<Method, Route>>,
199}
200
201impl Router {
202 pub fn new() -> Self {
204 Self { routes: HashMap::new() }
205 }
206
207 pub fn add_route(&mut self, route: Route) {
209 let path_routes = self.routes.entry(route.path.clone()).or_default();
210 path_routes.insert(route.method.clone(), route);
211 }
212
213 pub fn find_route(&self, method: &Method, path: &str) -> Option<&Route> {
215 self.routes.get(path)?.get(method)
216 }
217
218 pub fn routes(&self) -> Vec<&Route> {
220 self.routes.values().flat_map(|methods| methods.values()).collect()
221 }
222
223 pub fn route_count(&self) -> usize {
225 self.routes.values().map(|m| m.len()).sum()
226 }
227}
228
229impl Default for Router {
230 fn default() -> Self {
231 Self::new()
232 }
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238 use serde_json::json;
239
240 #[test]
241 fn test_router_add_and_find() {
242 let mut router = Router::new();
243 let registry = SchemaRegistry::new();
244
245 let metadata = RouteMetadata {
246 method: "GET".to_string(),
247 path: "/users".to_string(),
248 handler_name: "get_users".to_string(),
249 request_schema: None,
250 response_schema: None,
251 parameter_schema: None,
252 file_params: None,
253 is_async: true,
254 cors: None,
255 body_param_name: None,
256 jsonrpc_method: None,
257 #[cfg(feature = "di")]
258 handler_dependencies: None,
259 };
260
261 let route = Route::from_metadata(metadata, ®istry).unwrap();
262 router.add_route(route);
263
264 assert_eq!(router.route_count(), 1);
265 assert!(router.find_route(&Method::Get, "/users").is_some());
266 assert!(router.find_route(&Method::Post, "/users").is_none());
267 }
268
269 #[test]
270 fn test_route_with_validators() {
271 let registry = SchemaRegistry::new();
272
273 let metadata = RouteMetadata {
274 method: "POST".to_string(),
275 path: "/users".to_string(),
276 handler_name: "create_user".to_string(),
277 request_schema: Some(json!({
278 "type": "object",
279 "properties": {
280 "name": {"type": "string"}
281 },
282 "required": ["name"]
283 })),
284 response_schema: None,
285 parameter_schema: None,
286 file_params: None,
287 is_async: true,
288 cors: None,
289 body_param_name: None,
290 jsonrpc_method: None,
291 #[cfg(feature = "di")]
292 handler_dependencies: None,
293 };
294
295 let route = Route::from_metadata(metadata, ®istry).unwrap();
296 assert!(route.request_validator.is_some());
297 assert!(route.response_validator.is_none());
298 }
299
300 #[test]
301 fn test_schema_deduplication_in_routes() {
302 let registry = SchemaRegistry::new();
303
304 let shared_schema = json!({
305 "type": "object",
306 "properties": {
307 "id": {"type": "integer"}
308 }
309 });
310
311 let metadata1 = RouteMetadata {
312 method: "POST".to_string(),
313 path: "/items".to_string(),
314 handler_name: "create_item".to_string(),
315 request_schema: Some(shared_schema.clone()),
316 response_schema: None,
317 parameter_schema: None,
318 file_params: None,
319 is_async: true,
320 cors: None,
321 body_param_name: None,
322 jsonrpc_method: None,
323 #[cfg(feature = "di")]
324 handler_dependencies: None,
325 };
326
327 let metadata2 = RouteMetadata {
328 method: "PUT".to_string(),
329 path: "/items/{id}".to_string(),
330 handler_name: "update_item".to_string(),
331 request_schema: Some(shared_schema),
332 response_schema: None,
333 parameter_schema: None,
334 file_params: None,
335 is_async: true,
336 cors: None,
337 body_param_name: None,
338 jsonrpc_method: None,
339 #[cfg(feature = "di")]
340 handler_dependencies: None,
341 };
342
343 let route1 = Route::from_metadata(metadata1, ®istry).unwrap();
344 let route2 = Route::from_metadata(metadata2, ®istry).unwrap();
345
346 assert!(route1.request_validator.is_some());
347 assert!(route2.request_validator.is_some());
348
349 let validator1 = route1.request_validator.as_ref().unwrap();
350 let validator2 = route2.request_validator.as_ref().unwrap();
351 assert!(Arc::ptr_eq(validator1, validator2));
352
353 assert_eq!(registry.schema_count(), 1);
354 }
355
356 #[test]
357 fn test_jsonrpc_method_info() {
358 let rpc_info = JsonRpcMethodInfo {
359 method_name: "user.create".to_string(),
360 description: Some("Creates a new user account".to_string()),
361 params_schema: Some(json!({
362 "type": "object",
363 "properties": {
364 "name": {"type": "string"},
365 "email": {"type": "string"}
366 },
367 "required": ["name", "email"]
368 })),
369 result_schema: Some(json!({
370 "type": "object",
371 "properties": {
372 "id": {"type": "integer"},
373 "name": {"type": "string"},
374 "email": {"type": "string"}
375 }
376 })),
377 deprecated: false,
378 tags: vec!["users".to_string(), "admin".to_string()],
379 };
380
381 assert_eq!(rpc_info.method_name, "user.create");
382 assert_eq!(rpc_info.description.as_ref().unwrap(), "Creates a new user account");
383 assert!(rpc_info.params_schema.is_some());
384 assert!(rpc_info.result_schema.is_some());
385 assert!(!rpc_info.deprecated);
386 assert_eq!(rpc_info.tags.len(), 2);
387 assert!(rpc_info.tags.contains(&"users".to_string()));
388 }
389
390 #[test]
391 fn test_route_with_jsonrpc_method() {
392 let registry = SchemaRegistry::new();
393
394 let metadata = RouteMetadata {
395 method: "POST".to_string(),
396 path: "/user/create".to_string(),
397 handler_name: "create_user".to_string(),
398 request_schema: Some(json!({
399 "type": "object",
400 "properties": {
401 "name": {"type": "string"}
402 },
403 "required": ["name"]
404 })),
405 response_schema: Some(json!({
406 "type": "object",
407 "properties": {
408 "id": {"type": "integer"}
409 }
410 })),
411 parameter_schema: None,
412 file_params: None,
413 is_async: true,
414 cors: None,
415 body_param_name: None,
416 jsonrpc_method: None,
417 #[cfg(feature = "di")]
418 handler_dependencies: None,
419 };
420
421 let rpc_info = JsonRpcMethodInfo {
422 method_name: "user.create".to_string(),
423 description: Some("Creates a new user".to_string()),
424 params_schema: Some(json!({
425 "type": "object",
426 "properties": {
427 "name": {"type": "string"}
428 }
429 })),
430 result_schema: Some(json!({
431 "type": "object",
432 "properties": {
433 "id": {"type": "integer"}
434 }
435 })),
436 deprecated: false,
437 tags: vec!["users".to_string()],
438 };
439
440 let route = Route::from_metadata(metadata, ®istry)
441 .unwrap()
442 .with_jsonrpc_method(rpc_info);
443
444 assert!(route.is_jsonrpc_method());
445 assert_eq!(route.jsonrpc_method_name(), Some("user.create"));
446 assert!(route.jsonrpc_method.is_some());
447
448 let rpc = route.jsonrpc_method.as_ref().unwrap();
449 assert_eq!(rpc.method_name, "user.create");
450 assert_eq!(rpc.description.as_ref().unwrap(), "Creates a new user");
451 assert!(!rpc.deprecated);
452 }
453
454 #[test]
455 fn test_jsonrpc_method_serialization() {
456 let rpc_info = JsonRpcMethodInfo {
457 method_name: "test.method".to_string(),
458 description: Some("Test method".to_string()),
459 params_schema: Some(json!({"type": "object"})),
460 result_schema: Some(json!({"type": "string"})),
461 deprecated: false,
462 tags: vec!["test".to_string()],
463 };
464
465 let json = serde_json::to_value(&rpc_info).unwrap();
466 assert_eq!(json["method_name"], "test.method");
467 assert_eq!(json["description"], "Test method");
468
469 let deserialized: JsonRpcMethodInfo = serde_json::from_value(json).unwrap();
470 assert_eq!(deserialized.method_name, rpc_info.method_name);
471 assert_eq!(deserialized.description, rpc_info.description);
472 }
473
474 #[test]
475 fn test_route_without_jsonrpc_method_has_zero_overhead() {
476 let registry = SchemaRegistry::new();
477
478 let metadata = RouteMetadata {
479 method: "GET".to_string(),
480 path: "/status".to_string(),
481 handler_name: "status".to_string(),
482 request_schema: None,
483 response_schema: None,
484 parameter_schema: None,
485 file_params: None,
486 is_async: false,
487 cors: None,
488 body_param_name: None,
489 jsonrpc_method: None,
490 #[cfg(feature = "di")]
491 handler_dependencies: None,
492 };
493
494 let route = Route::from_metadata(metadata, ®istry).unwrap();
495
496 assert!(!route.is_jsonrpc_method());
497 assert_eq!(route.jsonrpc_method_name(), None);
498 assert!(route.jsonrpc_method.is_none());
499 }
500}