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