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 #[cfg(feature = "di")]
282 handler_dependencies: None,
283 };
284
285 let route = Route::from_metadata(metadata, ®istry).unwrap();
286 router.add_route(route);
287
288 assert_eq!(router.route_count(), 1);
289 assert!(router.find_route(&Method::Get, "/users").is_some());
290 assert!(router.find_route(&Method::Post, "/users").is_none());
291 }
292
293 #[test]
294 fn test_route_with_validators() {
295 let registry = SchemaRegistry::new();
296
297 let metadata = RouteMetadata {
298 method: "POST".to_string(),
299 path: "/users".to_string(),
300 handler_name: "create_user".to_string(),
301 request_schema: Some(json!({
302 "type": "object",
303 "properties": {
304 "name": {"type": "string"}
305 },
306 "required": ["name"]
307 })),
308 response_schema: None,
309 parameter_schema: None,
310 file_params: None,
311 is_async: true,
312 cors: None,
313 body_param_name: None,
314 jsonrpc_method: None,
315 #[cfg(feature = "di")]
316 handler_dependencies: None,
317 };
318
319 let route = Route::from_metadata(metadata, ®istry).unwrap();
320 assert!(route.request_validator.is_some());
321 assert!(route.response_validator.is_none());
322 }
323
324 #[test]
325 fn test_schema_deduplication_in_routes() {
326 let registry = SchemaRegistry::new();
327
328 let shared_schema = json!({
329 "type": "object",
330 "properties": {
331 "id": {"type": "integer"}
332 }
333 });
334
335 let metadata1 = RouteMetadata {
336 method: "POST".to_string(),
337 path: "/items".to_string(),
338 handler_name: "create_item".to_string(),
339 request_schema: Some(shared_schema.clone()),
340 response_schema: None,
341 parameter_schema: None,
342 file_params: None,
343 is_async: true,
344 cors: None,
345 body_param_name: None,
346 jsonrpc_method: None,
347 #[cfg(feature = "di")]
348 handler_dependencies: None,
349 };
350
351 let metadata2 = RouteMetadata {
352 method: "PUT".to_string(),
353 path: "/items/{id}".to_string(),
354 handler_name: "update_item".to_string(),
355 request_schema: Some(shared_schema),
356 response_schema: None,
357 parameter_schema: None,
358 file_params: None,
359 is_async: true,
360 cors: None,
361 body_param_name: None,
362 jsonrpc_method: None,
363 #[cfg(feature = "di")]
364 handler_dependencies: None,
365 };
366
367 let route1 = Route::from_metadata(metadata1, ®istry).unwrap();
368 let route2 = Route::from_metadata(metadata2, ®istry).unwrap();
369
370 assert!(route1.request_validator.is_some());
371 assert!(route2.request_validator.is_some());
372
373 let validator1 = route1.request_validator.as_ref().unwrap();
374 let validator2 = route2.request_validator.as_ref().unwrap();
375 assert!(Arc::ptr_eq(validator1, validator2));
376
377 assert_eq!(registry.schema_count(), 1);
378 }
379
380 #[test]
381 fn test_jsonrpc_method_info() {
382 let rpc_info = JsonRpcMethodInfo {
383 method_name: "user.create".to_string(),
384 description: Some("Creates a new user account".to_string()),
385 params_schema: Some(json!({
386 "type": "object",
387 "properties": {
388 "name": {"type": "string"},
389 "email": {"type": "string"}
390 },
391 "required": ["name", "email"]
392 })),
393 result_schema: Some(json!({
394 "type": "object",
395 "properties": {
396 "id": {"type": "integer"},
397 "name": {"type": "string"},
398 "email": {"type": "string"}
399 }
400 })),
401 deprecated: false,
402 tags: vec!["users".to_string(), "admin".to_string()],
403 };
404
405 assert_eq!(rpc_info.method_name, "user.create");
406 assert_eq!(rpc_info.description.as_ref().unwrap(), "Creates a new user account");
407 assert!(rpc_info.params_schema.is_some());
408 assert!(rpc_info.result_schema.is_some());
409 assert!(!rpc_info.deprecated);
410 assert_eq!(rpc_info.tags.len(), 2);
411 assert!(rpc_info.tags.contains(&"users".to_string()));
412 }
413
414 #[test]
415 fn test_route_with_jsonrpc_method() {
416 let registry = SchemaRegistry::new();
417
418 let metadata = RouteMetadata {
419 method: "POST".to_string(),
420 path: "/user/create".to_string(),
421 handler_name: "create_user".to_string(),
422 request_schema: Some(json!({
423 "type": "object",
424 "properties": {
425 "name": {"type": "string"}
426 },
427 "required": ["name"]
428 })),
429 response_schema: Some(json!({
430 "type": "object",
431 "properties": {
432 "id": {"type": "integer"}
433 }
434 })),
435 parameter_schema: None,
436 file_params: None,
437 is_async: true,
438 cors: None,
439 body_param_name: None,
440 jsonrpc_method: None,
441 #[cfg(feature = "di")]
442 handler_dependencies: None,
443 };
444
445 let rpc_info = JsonRpcMethodInfo {
446 method_name: "user.create".to_string(),
447 description: Some("Creates a new user".to_string()),
448 params_schema: Some(json!({
449 "type": "object",
450 "properties": {
451 "name": {"type": "string"}
452 }
453 })),
454 result_schema: Some(json!({
455 "type": "object",
456 "properties": {
457 "id": {"type": "integer"}
458 }
459 })),
460 deprecated: false,
461 tags: vec!["users".to_string()],
462 };
463
464 let route = Route::from_metadata(metadata, ®istry)
465 .unwrap()
466 .with_jsonrpc_method(rpc_info);
467
468 assert!(route.is_jsonrpc_method());
469 assert_eq!(route.jsonrpc_method_name(), Some("user.create"));
470 assert!(route.jsonrpc_method.is_some());
471
472 let rpc = route.jsonrpc_method.as_ref().unwrap();
473 assert_eq!(rpc.method_name, "user.create");
474 assert_eq!(rpc.description.as_ref().unwrap(), "Creates a new user");
475 assert!(!rpc.deprecated);
476 }
477
478 #[test]
479 fn test_jsonrpc_method_serialization() {
480 let rpc_info = JsonRpcMethodInfo {
481 method_name: "test.method".to_string(),
482 description: Some("Test method".to_string()),
483 params_schema: Some(json!({"type": "object"})),
484 result_schema: Some(json!({"type": "string"})),
485 deprecated: false,
486 tags: vec!["test".to_string()],
487 };
488
489 let json = serde_json::to_value(&rpc_info).unwrap();
490 assert_eq!(json["method_name"], "test.method");
491 assert_eq!(json["description"], "Test method");
492
493 let deserialized: JsonRpcMethodInfo = serde_json::from_value(json).unwrap();
494 assert_eq!(deserialized.method_name, rpc_info.method_name);
495 assert_eq!(deserialized.description, rpc_info.description);
496 }
497
498 #[test]
499 fn test_route_without_jsonrpc_method_has_zero_overhead() {
500 let registry = SchemaRegistry::new();
501
502 let metadata = RouteMetadata {
503 method: "GET".to_string(),
504 path: "/status".to_string(),
505 handler_name: "status".to_string(),
506 request_schema: None,
507 response_schema: None,
508 parameter_schema: None,
509 file_params: None,
510 is_async: false,
511 cors: None,
512 body_param_name: None,
513 jsonrpc_method: None,
514 #[cfg(feature = "di")]
515 handler_dependencies: None,
516 };
517
518 let route = Route::from_metadata(metadata, ®istry).unwrap();
519
520 assert!(!route.is_jsonrpc_method());
521 assert_eq!(route.jsonrpc_method_name(), None);
522 assert!(route.jsonrpc_method.is_none());
523 }
524}