mockforge_core/openapi_routes/
registry.rs

1//! OpenAPI route registry and management
2//!
3//! This module provides the main OpenApiRouteRegistry struct and related
4//! functionality for managing OpenAPI-based routes.
5
6use super::validation::{ValidationMode, ValidationOptions};
7use crate::ai_response::RequestContext;
8use crate::openapi::response::AiGenerator;
9use crate::openapi::route::OpenApiRoute;
10use crate::openapi::spec::OpenApiSpec;
11use axum::extract::Json;
12use axum::http::HeaderMap;
13use openapiv3::{PathItem, ReferenceOr};
14use serde_json::Value;
15use std::collections::{HashMap, HashSet};
16use std::sync::Arc;
17use url::Url;
18
19/// OpenAPI route registry that manages generated routes
20#[derive(Debug, Clone)]
21pub struct OpenApiRouteRegistry {
22    /// The OpenAPI specification
23    spec: Arc<OpenApiSpec>,
24    /// Generated routes
25    routes: Vec<OpenApiRoute>,
26    /// Validation options
27    options: ValidationOptions,
28}
29
30#[cfg(test)]
31mod tests {
32    use super::*;
33
34    fn registry_from_yaml(yaml: &str) -> OpenApiRouteRegistry {
35        let spec = OpenApiSpec::from_string(yaml, Some("yaml")).expect("parse spec");
36        OpenApiRouteRegistry::new_with_env(spec)
37    }
38
39    #[test]
40    fn generates_routes_from_components_path_items() {
41        let yaml = r#"
42openapi: 3.1.0
43info:
44  title: Test API
45  version: "1.0.0"
46paths:
47  /users:
48    $ref: '#/components/pathItems/UserCollection'
49components:
50  pathItems:
51    UserCollection:
52      get:
53        operationId: listUsers
54        responses:
55          '200':
56            description: ok
57            content:
58              application/json:
59                schema:
60                  type: array
61                  items:
62                    type: string
63        "#;
64
65        let registry = registry_from_yaml(yaml);
66        let routes = registry.routes();
67        assert_eq!(routes.len(), 1);
68        assert_eq!(routes[0].method, "GET");
69        assert_eq!(routes[0].path, "/users");
70    }
71
72    #[test]
73    fn generates_routes_from_paths_references() {
74        let yaml = r#"
75openapi: 3.0.3
76info:
77  title: PathRef API
78  version: "1.0.0"
79paths:
80  /users:
81    get:
82      operationId: getUsers
83      responses:
84        '200':
85          description: ok
86  /all-users:
87    $ref: '#/paths/~1users'
88        "#;
89
90        let registry = registry_from_yaml(yaml);
91        let routes = registry.routes();
92        assert_eq!(routes.len(), 2);
93
94        let mut paths: Vec<(&str, &str)> = routes
95            .iter()
96            .map(|route| (route.method.as_str(), route.path.as_str()))
97            .collect();
98        paths.sort();
99
100        assert_eq!(paths, vec![("GET", "/all-users"), ("GET", "/users")]);
101    }
102
103    #[test]
104    fn generates_routes_with_server_base_path() {
105        let yaml = r#"
106openapi: 3.0.3
107info:
108  title: Base Path API
109  version: "1.0.0"
110servers:
111  - url: https://api.example.com/api/v1
112paths:
113  /users:
114    get:
115      operationId: getUsers
116      responses:
117        '200':
118          description: ok
119        "#;
120
121        let registry = registry_from_yaml(yaml);
122        let paths: Vec<String> = registry.routes().iter().map(|route| route.path.clone()).collect();
123        assert!(paths.contains(&"/api/v1/users".to_string()));
124        assert!(!paths.contains(&"/users".to_string()));
125    }
126
127    #[test]
128    fn generates_routes_with_relative_server_base_path() {
129        let yaml = r#"
130openapi: 3.0.3
131info:
132  title: Relative Base Path API
133  version: "1.0.0"
134servers:
135  - url: /api/v2
136paths:
137  /orders:
138    post:
139      operationId: createOrder
140      responses:
141        '201':
142          description: created
143        "#;
144
145        let registry = registry_from_yaml(yaml);
146        let paths: Vec<String> = registry.routes().iter().map(|route| route.path.clone()).collect();
147        assert!(paths.contains(&"/api/v2/orders".to_string()));
148        assert!(!paths.contains(&"/orders".to_string()));
149    }
150}
151
152impl OpenApiRouteRegistry {
153    /// Create a new registry from an OpenAPI spec with default options
154    pub fn new(spec: OpenApiSpec) -> Self {
155        Self::new_with_env(spec)
156    }
157
158    /// Create a new registry from an OpenAPI spec with environment-based options
159    ///
160    /// Options are read from environment variables:
161    /// - `MOCKFORGE_REQUEST_VALIDATION`: "off"/"warn"/"enforce" (default: "enforce")
162    /// - `MOCKFORGE_AGGREGATE_ERRORS`: "1"/"true" to aggregate errors (default: true)
163    /// - `MOCKFORGE_RESPONSE_VALIDATION`: "1"/"true" to validate responses (default: false)
164    /// - `MOCKFORGE_RESPONSE_TEMPLATE_EXPAND`: "1"/"true" to expand templates (default: false)
165    /// - `MOCKFORGE_VALIDATION_STATUS`: HTTP status code for validation failures (optional)
166    pub fn new_with_env(spec: OpenApiSpec) -> Self {
167        tracing::debug!("Creating OpenAPI route registry");
168        let spec = Arc::new(spec);
169        let routes = Self::generate_routes(&spec);
170        let options = ValidationOptions {
171            request_mode: match std::env::var("MOCKFORGE_REQUEST_VALIDATION")
172                .unwrap_or_else(|_| "enforce".into())
173                .to_ascii_lowercase()
174                .as_str()
175            {
176                "off" | "disable" | "disabled" => ValidationMode::Disabled,
177                "warn" | "warning" => ValidationMode::Warn,
178                _ => ValidationMode::Enforce,
179            },
180            aggregate_errors: std::env::var("MOCKFORGE_AGGREGATE_ERRORS")
181                .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
182                .unwrap_or(true),
183            validate_responses: std::env::var("MOCKFORGE_RESPONSE_VALIDATION")
184                .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
185                .unwrap_or(false),
186            overrides: HashMap::new(),
187            admin_skip_prefixes: Vec::new(),
188            response_template_expand: std::env::var("MOCKFORGE_RESPONSE_TEMPLATE_EXPAND")
189                .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
190                .unwrap_or(false),
191            validation_status: std::env::var("MOCKFORGE_VALIDATION_STATUS")
192                .ok()
193                .and_then(|s| s.parse::<u16>().ok()),
194        };
195        Self {
196            spec,
197            routes,
198            options,
199        }
200    }
201
202    /// Create a new registry from an OpenAPI spec with explicit validation options
203    ///
204    /// # Arguments
205    /// * `spec` - OpenAPI specification
206    /// * `options` - Validation options to use
207    pub fn new_with_options(spec: OpenApiSpec, options: ValidationOptions) -> Self {
208        tracing::debug!("Creating OpenAPI route registry with custom options");
209        let spec = Arc::new(spec);
210        let routes = Self::generate_routes(&spec);
211        Self {
212            spec,
213            routes,
214            options,
215        }
216    }
217
218    /// Generate routes from the OpenAPI specification
219    fn generate_routes(spec: &Arc<OpenApiSpec>) -> Vec<OpenApiRoute> {
220        tracing::debug!(
221            "Generating routes from OpenAPI spec with {} paths",
222            spec.spec.paths.paths.len()
223        );
224        let base_paths = Self::collect_base_paths(spec);
225
226        // Optimize: Use parallel iteration for route generation when beneficial
227        #[cfg(feature = "rayon")]
228        {
229            use rayon::prelude::*;
230            let path_items: Vec<_> = spec.spec.paths.paths.iter().collect();
231
232            // Use parallel processing for large specs (100+ paths)
233            if path_items.len() > 100 {
234                tracing::debug!("Using parallel route generation for {} paths", path_items.len());
235                let routes: Vec<Vec<OpenApiRoute>> = path_items
236                    .par_iter()
237                    .map(|(path, path_item)| {
238                        let mut routes = Vec::new();
239                        let mut visited = HashSet::new();
240                        if let Some(item) = Self::resolve_path_item(path_item, spec, &mut visited) {
241                            Self::collect_routes_for_path(&mut routes, path, &item, spec, &base_paths);
242                        } else {
243                            tracing::warn!(
244                                "Skipping path {} because the referenced PathItem could not be resolved",
245                                path
246                            );
247                        }
248                        routes
249                    })
250                    .collect();
251
252                let mut all_routes = Vec::new();
253                for route_batch in routes {
254                    all_routes.extend(route_batch);
255                }
256                tracing::debug!(
257                    "Generated {} total routes from OpenAPI spec (parallel)",
258                    all_routes.len()
259                );
260                return all_routes;
261            }
262        }
263
264        // Sequential processing for smaller specs or when rayon is not available
265        let mut routes = Vec::new();
266        for (path, path_item) in &spec.spec.paths.paths {
267            tracing::debug!("Processing path: {}", path);
268            let mut visited = HashSet::new();
269            if let Some(item) = Self::resolve_path_item(path_item, spec, &mut visited) {
270                Self::collect_routes_for_path(&mut routes, path, &item, spec, &base_paths);
271            } else {
272                tracing::warn!(
273                    "Skipping path {} because the referenced PathItem could not be resolved",
274                    path
275                );
276            }
277        }
278
279        tracing::debug!("Generated {} total routes from OpenAPI spec", routes.len());
280        routes
281    }
282
283    fn collect_routes_for_path(
284        routes: &mut Vec<OpenApiRoute>,
285        path: &str,
286        item: &PathItem,
287        spec: &Arc<OpenApiSpec>,
288        base_paths: &[String],
289    ) {
290        if let Some(op) = &item.get {
291            tracing::debug!("  Adding GET route for path: {}", path);
292            Self::push_routes_for_method(routes, "GET", path, op, spec, base_paths);
293        }
294        if let Some(op) = &item.post {
295            Self::push_routes_for_method(routes, "POST", path, op, spec, base_paths);
296        }
297        if let Some(op) = &item.put {
298            Self::push_routes_for_method(routes, "PUT", path, op, spec, base_paths);
299        }
300        if let Some(op) = &item.delete {
301            Self::push_routes_for_method(routes, "DELETE", path, op, spec, base_paths);
302        }
303        if let Some(op) = &item.patch {
304            Self::push_routes_for_method(routes, "PATCH", path, op, spec, base_paths);
305        }
306        if let Some(op) = &item.head {
307            Self::push_routes_for_method(routes, "HEAD", path, op, spec, base_paths);
308        }
309        if let Some(op) = &item.options {
310            Self::push_routes_for_method(routes, "OPTIONS", path, op, spec, base_paths);
311        }
312        if let Some(op) = &item.trace {
313            Self::push_routes_for_method(routes, "TRACE", path, op, spec, base_paths);
314        }
315    }
316
317    fn push_routes_for_method(
318        routes: &mut Vec<OpenApiRoute>,
319        method: &str,
320        path: &str,
321        operation: &openapiv3::Operation,
322        spec: &Arc<OpenApiSpec>,
323        base_paths: &[String],
324    ) {
325        for base in base_paths {
326            let full_path = Self::join_base_path(base, path);
327            routes.push(OpenApiRoute::from_operation(method, full_path, operation, spec.clone()));
328        }
329    }
330
331    fn collect_base_paths(spec: &Arc<OpenApiSpec>) -> Vec<String> {
332        let mut base_paths = Vec::new();
333
334        for server in spec.servers() {
335            if let Some(base_path) = Self::extract_base_path(server.url.as_str()) {
336                if !base_paths.contains(&base_path) {
337                    base_paths.push(base_path);
338                }
339            }
340        }
341
342        if base_paths.is_empty() {
343            base_paths.push(String::new());
344        }
345
346        base_paths
347    }
348
349    fn extract_base_path(raw_url: &str) -> Option<String> {
350        let trimmed = raw_url.trim();
351        if trimmed.is_empty() {
352            return None;
353        }
354
355        if trimmed.starts_with('/') {
356            return Some(Self::normalize_base_path(trimmed));
357        }
358
359        if let Ok(parsed) = Url::parse(trimmed) {
360            return Some(Self::normalize_base_path(parsed.path()));
361        }
362
363        None
364    }
365
366    fn normalize_base_path(path: &str) -> String {
367        let trimmed = path.trim();
368        if trimmed.is_empty() || trimmed == "/" {
369            String::new()
370        } else {
371            let mut normalized = trimmed.trim_end_matches('/').to_string();
372            if !normalized.starts_with('/') {
373                normalized.insert(0, '/');
374            }
375            normalized
376        }
377    }
378
379    fn join_base_path(base: &str, path: &str) -> String {
380        let trimmed_path = path.trim_start_matches('/');
381
382        if base.is_empty() {
383            if trimmed_path.is_empty() {
384                "/".to_string()
385            } else {
386                format!("/{}", trimmed_path)
387            }
388        } else if trimmed_path.is_empty() {
389            base.to_string()
390        } else {
391            format!("{}/{}", base, trimmed_path)
392        }
393    }
394
395    fn resolve_path_item(
396        value: &ReferenceOr<PathItem>,
397        spec: &Arc<OpenApiSpec>,
398        visited: &mut HashSet<String>,
399    ) -> Option<PathItem> {
400        match value {
401            ReferenceOr::Item(item) => Some(item.clone()),
402            ReferenceOr::Reference { reference } => {
403                Self::resolve_path_item_reference(reference, spec, visited)
404            }
405        }
406    }
407
408    fn resolve_path_item_reference(
409        reference: &str,
410        spec: &Arc<OpenApiSpec>,
411        visited: &mut HashSet<String>,
412    ) -> Option<PathItem> {
413        if !visited.insert(reference.to_string()) {
414            tracing::warn!("Detected recursive path item reference: {}", reference);
415            return None;
416        }
417
418        if let Some(name) = reference.strip_prefix("#/components/pathItems/") {
419            return Self::resolve_component_path_item(name, spec, visited);
420        }
421
422        if let Some(pointer) = reference.strip_prefix("#/paths/") {
423            let decoded_path = Self::decode_json_pointer(pointer);
424            if let Some(next) = spec.spec.paths.paths.get(&decoded_path) {
425                return Self::resolve_path_item(next, spec, visited);
426            }
427            tracing::warn!(
428                "Path reference {} resolved to missing path '{}'",
429                reference,
430                decoded_path
431            );
432            return None;
433        }
434
435        tracing::warn!("Unsupported path item reference: {}", reference);
436        None
437    }
438
439    fn resolve_component_path_item(
440        name: &str,
441        spec: &Arc<OpenApiSpec>,
442        visited: &mut HashSet<String>,
443    ) -> Option<PathItem> {
444        let raw = spec.raw_document.as_ref()?;
445        let components = raw.get("components")?.as_object()?;
446        let path_items = components.get("pathItems")?.as_object()?;
447        let item_value = path_items.get(name)?;
448
449        if let Some(reference) = item_value
450            .as_object()
451            .and_then(|obj| obj.get("$ref"))
452            .and_then(|value| value.as_str())
453        {
454            tracing::debug!(
455                "Resolving components.pathItems entry '{}' via reference {}",
456                name,
457                reference
458            );
459            return Self::resolve_path_item_reference(reference, spec, visited);
460        }
461
462        match serde_json::from_value(item_value.clone()) {
463            Ok(item) => Some(item),
464            Err(err) => {
465                tracing::warn!(
466                    "Failed to deserialize components.pathItems entry '{}' as a PathItem: {}",
467                    name,
468                    err
469                );
470                None
471            }
472        }
473    }
474
475    fn decode_json_pointer(pointer: &str) -> String {
476        let segments: Vec<String> = pointer
477            .split('/')
478            .map(|segment| segment.replace("~1", "/").replace("~0", "~"))
479            .collect();
480        segments.join("/")
481    }
482
483    /// Get all generated routes
484    pub fn routes(&self) -> &[OpenApiRoute] {
485        &self.routes
486    }
487
488    /// Get the OpenAPI specification used to generate routes
489    pub fn spec(&self) -> &OpenApiSpec {
490        &self.spec
491    }
492
493    /// Get immutable reference to validation options
494    pub fn options(&self) -> &ValidationOptions {
495        &self.options
496    }
497
498    /// Get mutable reference to validation options for runtime configuration changes
499    pub fn options_mut(&mut self) -> &mut ValidationOptions {
500        &mut self.options
501    }
502
503    /// Build an Axum router from the generated routes
504    pub fn build_router(&self) -> axum::Router {
505        use axum::routing::{delete, get, patch, post, put};
506
507        let mut router = axum::Router::new();
508        tracing::debug!("Building router from {} routes", self.routes.len());
509
510        for route in &self.routes {
511            tracing::debug!("Adding route: {} {}", route.method, route.path);
512            tracing::debug!(
513                "Route operation responses: {:?}",
514                route.operation.responses.responses.keys().collect::<Vec<_>>()
515            );
516
517            let route_clone = route.clone();
518            let handler = move || {
519                let route = route_clone.clone();
520                async move {
521                    tracing::debug!("Handling request for route: {} {}", route.method, route.path);
522                    let (status, response) = route.mock_response_with_status();
523                    tracing::debug!("Generated response with status: {}", status);
524                    (
525                        axum::http::StatusCode::from_u16(status)
526                            .unwrap_or(axum::http::StatusCode::OK),
527                        axum::response::Json(response),
528                    )
529                }
530            };
531
532            match route.method.as_str() {
533                "GET" => {
534                    tracing::debug!("Registering GET route: {}", route.path);
535                    router = router.route(&route.path, get(handler));
536                }
537                "POST" => {
538                    tracing::debug!("Registering POST route: {}", route.path);
539                    router = router.route(&route.path, post(handler));
540                }
541                "PUT" => {
542                    tracing::debug!("Registering PUT route: {}", route.path);
543                    router = router.route(&route.path, put(handler));
544                }
545                "DELETE" => {
546                    tracing::debug!("Registering DELETE route: {}", route.path);
547                    router = router.route(&route.path, delete(handler));
548                }
549                "PATCH" => {
550                    tracing::debug!("Registering PATCH route: {}", route.path);
551                    router = router.route(&route.path, patch(handler));
552                }
553                _ => tracing::warn!("Unsupported HTTP method: {}", route.method),
554            }
555        }
556
557        router
558    }
559
560    /// Build router with latency and failure injection support
561    ///
562    /// # Arguments
563    /// * `latency_injector` - Latency injector for simulating network delays
564    /// * `failure_injector` - Optional failure injector for simulating errors
565    ///
566    /// # Returns
567    /// Axum router with chaos engineering capabilities
568    pub fn build_router_with_injectors(
569        &self,
570        latency_injector: crate::latency::LatencyInjector,
571        failure_injector: Option<crate::failure_injection::FailureInjector>,
572    ) -> axum::Router {
573        use axum::routing::{delete, get, patch, post, put};
574
575        let mut router = axum::Router::new();
576        tracing::debug!("Building router with injectors from {} routes", self.routes.len());
577
578        for route in &self.routes {
579            tracing::debug!("Adding route with injectors: {} {}", route.method, route.path);
580
581            let route_clone = route.clone();
582            let latency_injector_clone = latency_injector.clone();
583            let failure_injector_clone = failure_injector.clone();
584
585            let handler = move || {
586                let route = route_clone.clone();
587                let latency_injector = latency_injector_clone.clone();
588                let failure_injector = failure_injector_clone.clone();
589
590                async move {
591                    tracing::debug!(
592                        "Handling request with injectors for route: {} {}",
593                        route.method,
594                        route.path
595                    );
596
597                    // Extract tags from the operation
598                    let tags = route.operation.tags.clone();
599
600                    // Inject latency if configured
601                    if let Err(e) = latency_injector.inject_latency(&tags).await {
602                        tracing::warn!("Failed to inject latency: {}", e);
603                    }
604
605                    // Check for failure injection
606                    if let Some(ref injector) = failure_injector {
607                        if injector.should_inject_failure(&tags) {
608                            // Return a failure response
609                            return (
610                                axum::http::StatusCode::INTERNAL_SERVER_ERROR,
611                                axum::response::Json(serde_json::json!({
612                                    "error": "Injected failure",
613                                    "code": 500
614                                })),
615                            );
616                        }
617                    }
618
619                    // Generate normal response
620                    let (status, response) = route.mock_response_with_status();
621                    (
622                        axum::http::StatusCode::from_u16(status)
623                            .unwrap_or(axum::http::StatusCode::OK),
624                        axum::response::Json(response),
625                    )
626                }
627            };
628
629            match route.method.as_str() {
630                "GET" => router = router.route(&route.path, get(handler)),
631                "POST" => router = router.route(&route.path, post(handler)),
632                "PUT" => router = router.route(&route.path, put(handler)),
633                "DELETE" => router = router.route(&route.path, delete(handler)),
634                "PATCH" => router = router.route(&route.path, patch(handler)),
635                _ => tracing::warn!("Unsupported HTTP method: {}", route.method),
636            }
637        }
638
639        router
640    }
641
642    /// Extract path parameters from a request path by matching against known routes
643    ///
644    /// # Arguments
645    /// * `path` - Request path (e.g., "/users/123")
646    /// * `method` - HTTP method (e.g., "GET")
647    ///
648    /// # Returns
649    /// Map of parameter names to values extracted from the path
650    pub fn extract_path_parameters(&self, path: &str, method: &str) -> HashMap<String, String> {
651        for route in &self.routes {
652            if route.method != method {
653                continue;
654            }
655
656            if let Some(params) = self.match_path_to_route(path, &route.path) {
657                return params;
658            }
659        }
660        HashMap::new()
661    }
662
663    /// Match a request path against a route pattern and extract parameters
664    fn match_path_to_route(
665        &self,
666        request_path: &str,
667        route_pattern: &str,
668    ) -> Option<HashMap<String, String>> {
669        let mut params = HashMap::new();
670
671        // Split both paths into segments
672        let request_segments: Vec<&str> = request_path.trim_start_matches('/').split('/').collect();
673        let pattern_segments: Vec<&str> =
674            route_pattern.trim_start_matches('/').split('/').collect();
675
676        if request_segments.len() != pattern_segments.len() {
677            return None;
678        }
679
680        for (req_seg, pat_seg) in request_segments.iter().zip(pattern_segments.iter()) {
681            if pat_seg.starts_with('{') && pat_seg.ends_with('}') {
682                // This is a parameter
683                let param_name = &pat_seg[1..pat_seg.len() - 1];
684                params.insert(param_name.to_string(), req_seg.to_string());
685            } else if req_seg != pat_seg {
686                // Static segment doesn't match
687                return None;
688            }
689        }
690
691        Some(params)
692    }
693
694    /// Build router with AI generator support for dynamic response generation
695    ///
696    /// # Arguments
697    /// * `ai_generator` - Optional AI generator for creating dynamic responses based on request context
698    ///
699    /// # Returns
700    /// Axum router with AI-powered response generation
701    pub fn build_router_with_ai(
702        &self,
703        ai_generator: Option<std::sync::Arc<dyn AiGenerator + Send + Sync>>,
704    ) -> axum::Router {
705        use axum::routing::{delete, get, patch, post, put};
706
707        let mut router = axum::Router::new();
708        tracing::debug!("Building router with AI support from {} routes", self.routes.len());
709
710        for route in &self.routes {
711            tracing::debug!("Adding AI-enabled route: {} {}", route.method, route.path);
712
713            let route_clone = route.clone();
714            let ai_generator_clone = ai_generator.clone();
715
716            // Create async handler that extracts request data and builds context
717            let handler = move |headers: HeaderMap, body: Option<Json<Value>>| {
718                let route = route_clone.clone();
719                let ai_generator = ai_generator_clone.clone();
720
721                async move {
722                    tracing::debug!(
723                        "Handling AI request for route: {} {}",
724                        route.method,
725                        route.path
726                    );
727
728                    // Build request context
729                    let mut context = RequestContext::new(route.method.clone(), route.path.clone());
730
731                    // Extract headers
732                    context.headers = headers
733                        .iter()
734                        .map(|(k, v)| {
735                            (k.to_string(), Value::String(v.to_str().unwrap_or("").to_string()))
736                        })
737                        .collect();
738
739                    // Extract body if present
740                    context.body = body.map(|Json(b)| b);
741
742                    // Generate AI response if AI generator is available and route has AI config
743                    let (status, response) = if let (Some(generator), Some(_ai_config)) =
744                        (ai_generator, &route.ai_config)
745                    {
746                        route
747                            .mock_response_with_status_async(&context, Some(generator.as_ref()))
748                            .await
749                    } else {
750                        // No AI support, use static response
751                        route.mock_response_with_status()
752                    };
753
754                    (
755                        axum::http::StatusCode::from_u16(status)
756                            .unwrap_or(axum::http::StatusCode::OK),
757                        axum::response::Json(response),
758                    )
759                }
760            };
761
762            match route.method.as_str() {
763                "GET" => {
764                    router = router.route(&route.path, get(handler));
765                }
766                "POST" => {
767                    router = router.route(&route.path, post(handler));
768                }
769                "PUT" => {
770                    router = router.route(&route.path, put(handler));
771                }
772                "DELETE" => {
773                    router = router.route(&route.path, delete(handler));
774                }
775                "PATCH" => {
776                    router = router.route(&route.path, patch(handler));
777                }
778                _ => tracing::warn!("Unsupported HTTP method for AI: {}", route.method),
779            }
780        }
781
782        router
783    }
784}