mockforge_core/openapi_routes/
builder.rs

1//! Axum router building from OpenAPI specifications
2//!
3//! This module handles the creation of Axum routers from OpenAPI specifications,
4//! including route registration and middleware integration.
5
6use crate::openapi::OpenApiSpec;
7use crate::openapi_routes::OpenApiRouteRegistry;
8use axum::{
9    body::Body,
10    extract::State,
11    http::{Request, StatusCode},
12    middleware::Next,
13    response::Response,
14    routing::{delete, get, head, options, patch, post, put},
15    Router,
16};
17use serde_json::{Map, Value};
18use tracing;
19use url;
20
21/// Build an Axum router from an OpenAPI specification
22pub fn build_router_from_spec(spec: OpenApiSpec) -> Router {
23    let registry = OpenApiRouteRegistry::new(spec);
24    registry.build_router()
25}
26
27/// Build an Axum router from an OpenAPI specification with custom options
28pub fn build_router_from_spec_with_options(
29    spec: OpenApiSpec,
30    options: crate::openapi_routes::ValidationOptions,
31) -> Router {
32    let registry = OpenApiRouteRegistry::new_with_options(spec, options);
33    registry.build_router()
34}
35
36/// Router builder for creating complex routing configurations
37pub struct RouterBuilder {
38    registry: OpenApiRouteRegistry,
39    middleware: Option<Box<dyn Fn(Router) -> Router + Send + 'static>>,
40    custom_routes: Option<Box<dyn Fn(Router) -> Router + Send + 'static>>,
41}
42
43impl RouterBuilder {
44    /// Create a new router builder from an OpenAPI spec
45    pub fn new(spec: OpenApiSpec) -> Self {
46        let registry = OpenApiRouteRegistry::new(spec);
47        Self {
48            registry,
49            middleware: None,
50            custom_routes: None,
51        }
52    }
53
54    /// Create a new router builder with custom validation options
55    pub fn with_options(
56        spec: OpenApiSpec,
57        options: crate::openapi_routes::ValidationOptions,
58    ) -> Self {
59        let registry = OpenApiRouteRegistry::new_with_options(spec, options);
60        Self {
61            registry,
62            middleware: None,
63            custom_routes: None,
64        }
65    }
66
67    /// Add middleware to all routes
68    pub fn with_middleware<F>(mut self, middleware: F) -> Self
69    where
70        F: Fn(Router) -> Router + Send + 'static,
71    {
72        self.middleware = Some(Box::new(middleware));
73        self
74    }
75
76    /// Add custom routes alongside OpenAPI routes
77    pub fn with_custom_routes<F>(mut self, route_builder: F) -> Self
78    where
79        F: Fn(Router) -> Router + Send + 'static,
80    {
81        self.custom_routes = Some(Box::new(route_builder));
82        self
83    }
84
85    /// Build the final router
86    pub fn build(self) -> Router {
87        let mut router = self.registry.build_router();
88        if let Some(middleware) = self.middleware {
89            router = middleware(router);
90        }
91        if let Some(custom_routes) = self.custom_routes {
92            router = custom_routes(router);
93        }
94        router
95    }
96}
97
98/// Helper function to create route handlers
99pub fn create_route_handler(
100    route: &crate::openapi::route::OpenApiRoute,
101    registry: &OpenApiRouteRegistry,
102) -> Router {
103    let axum_path = route.axum_path();
104    let route_clone = route.clone();
105    let _validator = registry.clone_for_validation();
106
107    // Create a handler function that matches Axum's expectations
108    let handler = move || async move {
109        let (status, response) = route_clone.mock_response_with_status();
110        (
111            axum::http::StatusCode::from_u16(status).unwrap_or(axum::http::StatusCode::OK),
112            axum::response::Json(response),
113        )
114    };
115
116    match route.method.as_str() {
117        "GET" => Router::new().route(&axum_path, get(handler)),
118        "POST" => Router::new().route(&axum_path, post(handler)),
119        "PUT" => Router::new().route(&axum_path, put(handler)),
120        "DELETE" => Router::new().route(&axum_path, delete(handler)),
121        "PATCH" => Router::new().route(&axum_path, patch(handler)),
122        "HEAD" => Router::new().route(&axum_path, head(handler)),
123        "OPTIONS" => Router::new().route(&axum_path, options(handler)),
124        _ => Router::new().route(&axum_path, get(handler)),
125    }
126}
127
128/// Merge multiple routers into a single router
129pub fn merge_routers(routers: Vec<Router>) -> Router {
130    let mut merged = Router::new();
131
132    for router in routers {
133        merged = merged.merge(router);
134    }
135
136    merged
137}
138
139/// Middleware function to handle errors and panics
140pub async fn error_handler(request: Request<Body>, next: Next) -> Response {
141    // Extract request details before moving the request
142    let method = request.method().clone();
143    let uri = request.uri().clone();
144
145    let response = next.run(request).await;
146
147    // Enhanced error handling with more detailed logging and response transformation
148    if response.status().is_server_error() {
149        tracing::error!(
150            "Server error response: {} for request: {} {}",
151            response.status(),
152            method,
153            uri
154        );
155        // Could transform the response here if needed
156    } else if response.status().is_client_error() {
157        tracing::warn!(
158            "Client error response: {} for request: {} {}",
159            response.status(),
160            method,
161            uri
162        );
163    }
164
165    // In a production system, you might want to:
166    // - Add timeout handling
167    // - Convert custom errors to proper HTTP responses
168    // - Add error logging and monitoring
169    // - Implement circuit breaker patterns
170    // - Add panic recovery with tower::catch_panic
171
172    response
173}
174
175/// Create a router with error handling middleware
176pub fn create_router_with_error_handling(router: Router) -> Router {
177    router.layer(axum::middleware::from_fn(error_handler))
178}
179
180/// Create a router with logging middleware
181pub fn create_router_with_logging(router: Router) -> Router {
182    router.layer(axum::middleware::from_fn(request_logger))
183}
184
185/// Middleware function to validate requests against OpenAPI spec
186pub async fn validate_request(
187    State(validator): State<OpenApiRouteRegistry>,
188    mut request: Request<Body>,
189    next: Next,
190) -> Result<Response, StatusCode> {
191    // Extract request components for validation
192    let method = request.method().clone();
193    let uri = request.uri().clone();
194    let path = uri.path().to_string();
195
196    // Parse query parameters
197    let query_string = uri.query().unwrap_or("");
198    let mut query_map = Map::new();
199    for (k, v) in url::form_urlencoded::parse(query_string.as_bytes()) {
200        query_map.insert(k.to_string(), Value::String(v.to_string()));
201    }
202
203    // Parse headers
204    let headers = request.headers();
205    let mut header_map = Map::new();
206    for (name, value) in headers {
207        if let Ok(value_str) = value.to_str() {
208            header_map.insert(name.as_str().to_string(), Value::String(value_str.to_string()));
209        }
210    }
211
212    // Parse cookies from Cookie header
213    let mut cookie_map = Map::new();
214    if let Some(cookie_header) = headers.get("cookie") {
215        if let Ok(cookie_str) = cookie_header.to_str() {
216            for part in cookie_str.split(';') {
217                let part = part.trim();
218                if let Some((k, v)) = part.split_once('=') {
219                    cookie_map.insert(k.to_string(), Value::String(v.to_string()));
220                }
221            }
222        }
223    }
224
225    // Extract path parameters from the matched route
226    let path_params = validator.extract_path_parameters(&path, method.as_str());
227    let mut path_map = Map::new();
228    for (key, value) in path_params {
229        path_map.insert(key, Value::String(value));
230    }
231
232    // Parse body if present
233    let body = std::mem::take(request.body_mut());
234    let body_bytes = axum::body::to_bytes(body, usize::MAX)
235        .await
236        .map_err(|_| StatusCode::BAD_REQUEST)?;
237    let body_json = if !body_bytes.is_empty() {
238        serde_json::from_slice(&body_bytes).ok()
239    } else {
240        None
241    };
242
243    // Validate the request
244    if validator
245        .validate_request_with_all(
246            &path,
247            method.as_str(),
248            &path_map,
249            &query_map,
250            &header_map,
251            &cookie_map,
252            body_json.as_ref(),
253        )
254        .is_err()
255    {
256        // Return validation error status
257        let status_code = validator.options.validation_status.unwrap_or_else(|| {
258            std::env::var("MOCKFORGE_VALIDATION_STATUS")
259                .ok()
260                .and_then(|s| s.parse::<u16>().ok())
261                .unwrap_or(400)
262        });
263        return Err(StatusCode::from_u16(status_code).unwrap_or(StatusCode::BAD_REQUEST));
264    }
265
266    // If validation passes, continue to next middleware
267    Ok(next.run(request).await)
268}
269
270/// Middleware function to log incoming requests
271pub async fn request_logger(request: Request<Body>, next: Next) -> Result<Response, StatusCode> {
272    let method = request.method().clone();
273    let uri = request.uri().clone();
274    let version = request.version();
275
276    // Log the incoming request
277    tracing::info!("Request: {} {} {:?}", method, uri, version);
278
279    // Log headers if debug logging is enabled
280    if tracing::level_enabled!(tracing::Level::DEBUG) {
281        for (name, value) in request.headers() {
282            if let Ok(value_str) = value.to_str() {
283                tracing::debug!("Header: {}: {}", name, value_str);
284            }
285        }
286    }
287
288    let start = std::time::Instant::now();
289
290    // Call the next middleware
291    let response = next.run(request).await;
292
293    let duration = start.elapsed();
294
295    // Log the response
296    tracing::info!("Response: {} {} - {} in {:?}", method, uri, response.status(), duration);
297
298    Ok(response)
299}
300
301/// Create a router with validation middleware
302pub fn create_router_with_validation(router: Router, validator: OpenApiRouteRegistry) -> Router {
303    router.layer(axum::middleware::from_fn_with_state(validator, validate_request))
304}