mockforge_core/openapi_routes/
generation.rs

1//! Route generation from OpenAPI specifications
2//!
3//! This module handles the generation of routes from OpenAPI specifications,
4//! including parameter extraction, path matching, and route creation.
5
6use crate::openapi::route::OpenApiRoute;
7use crate::openapi::spec::OpenApiSpec;
8use std::sync::Arc;
9
10/// Generate routes from an OpenAPI specification
11pub fn generate_routes_from_spec(spec: &Arc<OpenApiSpec>) -> Vec<OpenApiRoute> {
12    let mut routes = Vec::new();
13
14    for (path, path_item) in &spec.spec.paths.paths {
15        if let Some(item) = path_item.as_item() {
16            // Generate route for each HTTP method
17            if let Some(op) = &item.get {
18                routes.push(OpenApiRoute::from_operation("GET", path.clone(), op, spec.clone()));
19            }
20            if let Some(op) = &item.post {
21                routes.push(OpenApiRoute::from_operation("POST", path.clone(), op, spec.clone()));
22            }
23            if let Some(op) = &item.put {
24                routes.push(OpenApiRoute::from_operation("PUT", path.clone(), op, spec.clone()));
25            }
26            if let Some(op) = &item.delete {
27                routes.push(OpenApiRoute::from_operation("DELETE", path.clone(), op, spec.clone()));
28            }
29            if let Some(op) = &item.patch {
30                routes.push(OpenApiRoute::from_operation("PATCH", path.clone(), op, spec.clone()));
31            }
32            if let Some(op) = &item.head {
33                routes.push(OpenApiRoute::from_operation("HEAD", path.clone(), op, spec.clone()));
34            }
35            if let Some(op) = &item.options {
36                routes.push(OpenApiRoute::from_operation(
37                    "OPTIONS",
38                    path.clone(),
39                    op,
40                    spec.clone(),
41                ));
42            }
43            if let Some(op) = &item.trace {
44                routes.push(OpenApiRoute::from_operation("TRACE", path.clone(), op, spec.clone()));
45            }
46        }
47    }
48
49    routes
50}
51
52/// Extract path parameters from an OpenAPI path template
53pub fn extract_path_parameters(path_template: &str) -> Vec<String> {
54    let mut params = Vec::new();
55    let mut in_param = false;
56    let mut current_param = String::new();
57
58    for ch in path_template.chars() {
59        match ch {
60            '{' => {
61                in_param = true;
62                current_param.clear();
63            }
64            '}' => {
65                if in_param {
66                    params.push(current_param.clone());
67                    in_param = false;
68                }
69            }
70            ch if in_param => {
71                current_param.push(ch);
72            }
73            _ => {}
74        }
75    }
76
77    params
78}
79
80/// Convert OpenAPI path parameters to Axum path format
81pub fn convert_path_to_axum_format(path: &str) -> String {
82    // Axum v0.7+ uses {param} format, same as OpenAPI
83    path.to_string()
84}
85
86/// Validate that path parameters match between template and actual path
87pub fn validate_path_parameters(template_path: &str, actual_path: &str) -> bool {
88    // Convert OpenAPI-style parameters {param} to routing format :param
89    let routing_template = convert_path_to_axum_format(template_path);
90
91    // Use proper pattern matching to validate path structure compatibility
92    // This ensures parameters, wildcards, and exact segments are properly validated
93    route_matches_pattern(&routing_template, actual_path)
94}
95
96/// Generate a unique route key for caching/routing purposes
97pub fn generate_route_key(method: &str, path: &str) -> String {
98    format!("{}:{}", method.to_uppercase(), path)
99}
100
101/// Check if a route path matches a pattern (for routing purposes)
102pub fn route_matches_pattern(route_path: &str, request_path: &str) -> bool {
103    let route_parts: Vec<&str> = route_path.split('/').filter(|s| !s.is_empty()).collect();
104    let request_parts: Vec<&str> = request_path.split('/').filter(|s| !s.is_empty()).collect();
105
106    match_segments(&route_parts, &request_parts, 0, 0)
107}
108
109/// Recursive function to match path segments with wildcards and parameters
110fn match_segments(
111    route_parts: &[&str],
112    request_parts: &[&str],
113    route_idx: usize,
114    request_idx: usize,
115) -> bool {
116    // If we've consumed both patterns and paths, it's a match
117    if route_idx == route_parts.len() && request_idx == request_parts.len() {
118        return true;
119    }
120
121    // If we've consumed the route pattern but not the request path, no match
122    if route_idx == route_parts.len() {
123        return false;
124    }
125
126    let current_route = route_parts[route_idx];
127
128    match current_route {
129        "*" => {
130            // Single wildcard: matches any single segment
131            if request_idx < request_parts.len() {
132                // Try consuming one segment
133                if match_segments(route_parts, request_parts, route_idx + 1, request_idx + 1) {
134                    return true;
135                }
136            }
137            false
138        }
139        "**" => {
140            // Double wildcard: can match zero or more segments
141            // Try matching zero segments (skip this pattern)
142            if match_segments(route_parts, request_parts, route_idx + 1, request_idx) {
143                return true;
144            }
145            // Try matching one or more segments
146            if request_idx < request_parts.len()
147                && match_segments(route_parts, request_parts, route_idx, request_idx + 1)
148            {
149                return true;
150            }
151            false
152        }
153        route_seg if route_seg.starts_with(':') => {
154            // Parameter placeholder: matches any single segment
155            if request_idx < request_parts.len() {
156                return match_segments(route_parts, request_parts, route_idx + 1, request_idx + 1);
157            }
158            false
159        }
160        _ => {
161            // Exact match required
162            if request_idx < request_parts.len() && current_route == request_parts[request_idx] {
163                return match_segments(route_parts, request_parts, route_idx + 1, request_idx + 1);
164            }
165            false
166        }
167    }
168}
169
170/// Generate parameter extraction code for a route
171pub fn generate_parameter_extraction_code(route: &OpenApiRoute) -> String {
172    let mut code = String::new();
173
174    // Add path parameter extraction
175    for param_name in &route.parameters {
176        if param_name.starts_with(':') {
177            code.push_str(&format!(
178                "let {} = path_params.get(\"{}\").cloned().unwrap_or_default();\n",
179                param_name.trim_start_matches(':'),
180                param_name.trim_start_matches(':')
181            ));
182        }
183    }
184
185    code
186}
187
188/// Generate validation code for route parameters
189pub fn generate_parameter_validation_code(route: &OpenApiRoute) -> String {
190    let mut code = String::new();
191
192    // Add parameter validation
193    for param in &route.parameters {
194        if param.starts_with(':') {
195            code.push_str(&format!(
196                "if {}.is_empty() {{ return Err(Error::generic(\"Missing parameter: {}\")); }}\n",
197                param.trim_start_matches(':'),
198                param.trim_start_matches(':')
199            ));
200        }
201    }
202
203    code
204}
205
206/// Generate mock response generation code
207pub fn generate_mock_response_code(route: &OpenApiRoute) -> String {
208    let mut code = String::new();
209
210    code.push_str("let mut response = json!({});\n");
211
212    // Add response generation logic based on the route's operation
213    let _operation = &route.operation;
214    code.push_str("// Generate response based on OpenAPI operation\n");
215    code.push_str(&format!("// Operation: {} {}\n", route.method, route.path));
216
217    code
218}