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