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 #[allow(clippy::items_after_statements)]
113 pub fn from_metadata(metadata: RouteMetadata, registry: &SchemaRegistry) -> Result<Self, String> {
114 let method = metadata.method.parse()?;
115
116 fn is_empty_schema(schema: &Value) -> bool {
117 matches!(schema, Value::Object(map) if map.is_empty())
118 }
119
120 let request_validator = metadata
121 .request_schema
122 .as_ref()
123 .filter(|schema| !is_empty_schema(schema))
124 .map(|schema| registry.get_or_compile(schema))
125 .transpose()?;
126
127 let response_validator = metadata
128 .response_schema
129 .as_ref()
130 .filter(|schema| !is_empty_schema(schema))
131 .map(|schema| registry.get_or_compile(schema))
132 .transpose()?;
133
134 let final_parameter_schema = match (
135 crate::type_hints::auto_generate_parameter_schema(&metadata.path),
136 metadata.parameter_schema,
137 ) {
138 (Some(auto_schema), Some(explicit_schema)) => {
139 if is_empty_schema(&explicit_schema) {
140 Some(auto_schema)
141 } else {
142 Some(crate::type_hints::merge_parameter_schemas(
143 &auto_schema,
144 &explicit_schema,
145 ))
146 }
147 }
148 (Some(auto_schema), None) => Some(auto_schema),
149 (None, Some(explicit_schema)) => (!is_empty_schema(&explicit_schema)).then_some(explicit_schema),
150 (None, None) => None,
151 };
152
153 let parameter_validator = final_parameter_schema.map(ParameterValidator::new).transpose()?;
154
155 let expects_json_body = request_validator.is_some();
156
157 let jsonrpc_method = metadata
158 .jsonrpc_method
159 .as_ref()
160 .and_then(|json_value| serde_json::from_value(json_value.clone()).ok());
161
162 Ok(Self {
163 method,
164 path: metadata.path,
165 handler_name: metadata.handler_name,
166 request_validator,
167 response_validator,
168 parameter_validator,
169 file_params: metadata.file_params,
170 is_async: metadata.is_async,
171 cors: metadata.cors,
172 expects_json_body,
173 #[cfg(feature = "di")]
174 handler_dependencies: metadata.handler_dependencies.unwrap_or_default(),
175 jsonrpc_method,
176 })
177 }
178
179 #[must_use]
198 pub fn with_jsonrpc_method(mut self, info: JsonRpcMethodInfo) -> Self {
199 self.jsonrpc_method = Some(info);
200 self
201 }
202
203 #[must_use]
205 pub const fn is_jsonrpc_method(&self) -> bool {
206 self.jsonrpc_method.is_some()
207 }
208
209 #[must_use]
211 pub fn jsonrpc_method_name(&self) -> Option<&str> {
212 self.jsonrpc_method.as_ref().map(|m| m.method_name.as_str())
213 }
214}
215
216pub struct Router {
218 routes: HashMap<String, HashMap<Method, Route>>,
219}
220
221impl Router {
222 #[must_use]
224 pub fn new() -> Self {
225 Self { routes: HashMap::new() }
226 }
227
228 pub fn add_route(&mut self, route: Route) {
230 let path_routes = self.routes.entry(route.path.clone()).or_default();
231 path_routes.insert(route.method.clone(), route);
232 }
233
234 #[must_use]
236 pub fn find_route(&self, method: &Method, path: &str) -> Option<&Route> {
237 self.routes.get(path)?.get(method)
238 }
239
240 #[must_use]
242 pub fn routes(&self) -> Vec<&Route> {
243 self.routes.values().flat_map(|methods| methods.values()).collect()
244 }
245
246 #[must_use]
248 pub fn route_count(&self) -> usize {
249 self.routes.values().map(std::collections::HashMap::len).sum()
250 }
251}
252
253impl Default for Router {
254 fn default() -> Self {
255 Self::new()
256 }
257}
258
259#[cfg(test)]
260mod tests {
261 use super::*;
262 use serde_json::json;
263
264 #[test]
265 fn test_router_add_and_find() {
266 let mut router = Router::new();
267 let registry = SchemaRegistry::new();
268
269 let metadata = RouteMetadata {
270 method: "GET".to_string(),
271 path: "/users".to_string(),
272 handler_name: "get_users".to_string(),
273 request_schema: None,
274 response_schema: None,
275 parameter_schema: None,
276 file_params: None,
277 is_async: true,
278 cors: None,
279 body_param_name: None,
280 jsonrpc_method: None,
281 static_response: None,
282 #[cfg(feature = "di")]
283 handler_dependencies: None,
284 };
285
286 let route = Route::from_metadata(metadata, ®istry).unwrap();
287 router.add_route(route);
288
289 assert_eq!(router.route_count(), 1);
290 assert!(router.find_route(&Method::Get, "/users").is_some());
291 assert!(router.find_route(&Method::Post, "/users").is_none());
292 }
293
294 #[test]
295 fn test_route_with_validators() {
296 let registry = SchemaRegistry::new();
297
298 let metadata = RouteMetadata {
299 method: "POST".to_string(),
300 path: "/users".to_string(),
301 handler_name: "create_user".to_string(),
302 request_schema: Some(json!({
303 "type": "object",
304 "properties": {
305 "name": {"type": "string"}
306 },
307 "required": ["name"]
308 })),
309 response_schema: None,
310 parameter_schema: None,
311 file_params: None,
312 is_async: true,
313 cors: None,
314 body_param_name: None,
315 jsonrpc_method: None,
316 static_response: None,
317 #[cfg(feature = "di")]
318 handler_dependencies: None,
319 };
320
321 let route = Route::from_metadata(metadata, ®istry).unwrap();
322 assert!(route.request_validator.is_some());
323 assert!(route.response_validator.is_none());
324 }
325
326 #[test]
327 fn test_schema_deduplication_in_routes() {
328 let registry = SchemaRegistry::new();
329
330 let shared_schema = json!({
331 "type": "object",
332 "properties": {
333 "id": {"type": "integer"}
334 }
335 });
336
337 let metadata1 = RouteMetadata {
338 method: "POST".to_string(),
339 path: "/items".to_string(),
340 handler_name: "create_item".to_string(),
341 request_schema: Some(shared_schema.clone()),
342 response_schema: None,
343 parameter_schema: None,
344 file_params: None,
345 is_async: true,
346 cors: None,
347 body_param_name: None,
348 jsonrpc_method: None,
349 static_response: None,
350 #[cfg(feature = "di")]
351 handler_dependencies: None,
352 };
353
354 let metadata2 = RouteMetadata {
355 method: "PUT".to_string(),
356 path: "/items/{id}".to_string(),
357 handler_name: "update_item".to_string(),
358 request_schema: Some(shared_schema),
359 response_schema: None,
360 parameter_schema: None,
361 file_params: None,
362 is_async: true,
363 cors: None,
364 body_param_name: None,
365 jsonrpc_method: None,
366 static_response: None,
367 #[cfg(feature = "di")]
368 handler_dependencies: None,
369 };
370
371 let route1 = Route::from_metadata(metadata1, ®istry).unwrap();
372 let route2 = Route::from_metadata(metadata2, ®istry).unwrap();
373
374 assert!(route1.request_validator.is_some());
375 assert!(route2.request_validator.is_some());
376
377 let validator1 = route1.request_validator.as_ref().unwrap();
378 let validator2 = route2.request_validator.as_ref().unwrap();
379 assert!(Arc::ptr_eq(validator1, validator2));
380
381 assert_eq!(registry.schema_count(), 1);
382 }
383
384 #[test]
385 fn test_jsonrpc_method_info() {
386 let rpc_info = JsonRpcMethodInfo {
387 method_name: "user.create".to_string(),
388 description: Some("Creates a new user account".to_string()),
389 params_schema: Some(json!({
390 "type": "object",
391 "properties": {
392 "name": {"type": "string"},
393 "email": {"type": "string"}
394 },
395 "required": ["name", "email"]
396 })),
397 result_schema: Some(json!({
398 "type": "object",
399 "properties": {
400 "id": {"type": "integer"},
401 "name": {"type": "string"},
402 "email": {"type": "string"}
403 }
404 })),
405 deprecated: false,
406 tags: vec!["users".to_string(), "admin".to_string()],
407 };
408
409 assert_eq!(rpc_info.method_name, "user.create");
410 assert_eq!(rpc_info.description.as_ref().unwrap(), "Creates a new user account");
411 assert!(rpc_info.params_schema.is_some());
412 assert!(rpc_info.result_schema.is_some());
413 assert!(!rpc_info.deprecated);
414 assert_eq!(rpc_info.tags.len(), 2);
415 assert!(rpc_info.tags.contains(&"users".to_string()));
416 }
417
418 #[test]
419 fn test_route_with_jsonrpc_method() {
420 let registry = SchemaRegistry::new();
421
422 let metadata = RouteMetadata {
423 method: "POST".to_string(),
424 path: "/user/create".to_string(),
425 handler_name: "create_user".to_string(),
426 request_schema: Some(json!({
427 "type": "object",
428 "properties": {
429 "name": {"type": "string"}
430 },
431 "required": ["name"]
432 })),
433 response_schema: Some(json!({
434 "type": "object",
435 "properties": {
436 "id": {"type": "integer"}
437 }
438 })),
439 parameter_schema: None,
440 file_params: None,
441 is_async: true,
442 cors: None,
443 body_param_name: None,
444 jsonrpc_method: None,
445 static_response: None,
446 #[cfg(feature = "di")]
447 handler_dependencies: None,
448 };
449
450 let rpc_info = JsonRpcMethodInfo {
451 method_name: "user.create".to_string(),
452 description: Some("Creates a new user".to_string()),
453 params_schema: Some(json!({
454 "type": "object",
455 "properties": {
456 "name": {"type": "string"}
457 }
458 })),
459 result_schema: Some(json!({
460 "type": "object",
461 "properties": {
462 "id": {"type": "integer"}
463 }
464 })),
465 deprecated: false,
466 tags: vec!["users".to_string()],
467 };
468
469 let route = Route::from_metadata(metadata, ®istry)
470 .unwrap()
471 .with_jsonrpc_method(rpc_info);
472
473 assert!(route.is_jsonrpc_method());
474 assert_eq!(route.jsonrpc_method_name(), Some("user.create"));
475 assert!(route.jsonrpc_method.is_some());
476
477 let rpc = route.jsonrpc_method.as_ref().unwrap();
478 assert_eq!(rpc.method_name, "user.create");
479 assert_eq!(rpc.description.as_ref().unwrap(), "Creates a new user");
480 assert!(!rpc.deprecated);
481 }
482
483 #[test]
484 fn test_jsonrpc_method_serialization() {
485 let rpc_info = JsonRpcMethodInfo {
486 method_name: "test.method".to_string(),
487 description: Some("Test method".to_string()),
488 params_schema: Some(json!({"type": "object"})),
489 result_schema: Some(json!({"type": "string"})),
490 deprecated: false,
491 tags: vec!["test".to_string()],
492 };
493
494 let json = serde_json::to_value(&rpc_info).unwrap();
495 assert_eq!(json["method_name"], "test.method");
496 assert_eq!(json["description"], "Test method");
497
498 let deserialized: JsonRpcMethodInfo = serde_json::from_value(json).unwrap();
499 assert_eq!(deserialized.method_name, rpc_info.method_name);
500 assert_eq!(deserialized.description, rpc_info.description);
501 }
502
503 #[test]
504 fn test_route_without_jsonrpc_method_has_zero_overhead() {
505 let registry = SchemaRegistry::new();
506
507 let metadata = RouteMetadata {
508 method: "GET".to_string(),
509 path: "/status".to_string(),
510 handler_name: "status".to_string(),
511 request_schema: None,
512 response_schema: None,
513 parameter_schema: None,
514 file_params: None,
515 is_async: false,
516 cors: None,
517 body_param_name: None,
518 jsonrpc_method: None,
519 static_response: None,
520 #[cfg(feature = "di")]
521 handler_dependencies: None,
522 };
523
524 let route = Route::from_metadata(metadata, ®istry).unwrap();
525
526 assert!(!route.is_jsonrpc_method());
527 assert_eq!(route.jsonrpc_method_name(), None);
528 assert!(route.jsonrpc_method.is_none());
529 }
530}