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