mockforge_http/consistency/
middleware.rs

1//! Consistency middleware for HTTP
2//!
3//! This middleware ensures HTTP requests/responses use the unified state
4//! from the consistency engine (persona, scenario, reality level, etc.)
5
6use crate::consistency::HttpAdapter;
7use axum::{body::Body, extract::Request, http::Response, middleware::Next};
8use mockforge_core::consistency::ConsistencyEngine;
9use mockforge_core::request_logger::RealityTraceMetadata;
10use std::sync::Arc;
11use tracing::debug;
12use uuid::Uuid;
13
14/// Consistency middleware state
15#[derive(Clone)]
16pub struct ConsistencyMiddlewareState {
17    /// Consistency engine
18    pub engine: Arc<ConsistencyEngine>,
19    /// HTTP adapter
20    pub adapter: Arc<HttpAdapter>,
21}
22
23/// Consistency middleware
24///
25/// This middleware:
26/// 1. Extracts workspace ID from request (header, query param, or default)
27/// 2. Gets unified state from consistency engine
28/// 3. Inserts state into request extensions for handlers to use
29/// 4. Ensures responses reflect the unified state
30pub async fn consistency_middleware(req: Request, next: Next) -> Response<Body> {
31    // Extract workspace ID from request
32    // Priority: X-MockForge-Workspace header > query param > default
33    let workspace_id = req
34        .headers()
35        .get("X-MockForge-Workspace")
36        .and_then(|h| h.to_str().ok())
37        .map(|s| s.to_string())
38        .or_else(|| {
39            req.uri().query().and_then(|q| {
40                q.split('&').find_map(|pair| {
41                    let mut parts = pair.splitn(2, '=');
42                    if parts.next() == Some("workspace") {
43                        parts.next().and_then(|v| {
44                            urlencoding::decode(v).ok().map(|decoded| decoded.to_string())
45                        })
46                    } else {
47                        None
48                    }
49                })
50            })
51        })
52        .unwrap_or_else(|| "default".to_string());
53
54    // Get state from extensions (set by router)
55    let state = req.extensions().get::<ConsistencyMiddlewareState>();
56
57    if let Some(state) = state {
58        // Get unified state for workspace
59        if let Some(unified_state) = state.engine.get_state(&workspace_id).await {
60            // Extract values for headers before moving unified_state
61            let persona_id = unified_state.active_persona.as_ref().map(|p| p.id.clone());
62            let scenario_id = unified_state.active_scenario.clone();
63            let reality_level = unified_state.reality_level.value();
64            let reality_ratio = unified_state.reality_continuum_ratio;
65            // Note: ChaosScenario is now serde_json::Value, so we extract the name field
66            let chaos_rules: Vec<String> = unified_state
67                .active_chaos_rules
68                .iter()
69                .filter_map(|r| r.get("name").and_then(|v| v.as_str()).map(|s| s.to_string()))
70                .collect();
71            let request_id = uuid::Uuid::new_v4().to_string();
72
73            // Build reality trace metadata from unified state
74            // Use the path from the request URI for path-specific blend ratio calculation
75            let path = req.uri().path();
76            let reality_metadata =
77                RealityTraceMetadata::from_unified_state(&unified_state, reality_ratio, path);
78
79            // Insert unified state and reality metadata into request extensions for handlers
80            let mut req = req;
81            req.extensions_mut().insert(unified_state);
82            req.extensions_mut().insert(reality_metadata);
83
84            // Continue with request processing
85            let mut response = next.run(req).await;
86
87            // Add X-Ray headers to response for browser extension
88            response
89                .headers_mut()
90                .insert("X-MockForge-Workspace", workspace_id.parse().unwrap());
91            response
92                .headers_mut()
93                .insert("X-MockForge-Request-ID", request_id.parse().unwrap());
94            if let Some(ref persona_id) = persona_id {
95                response
96                    .headers_mut()
97                    .insert("X-MockForge-Persona", persona_id.parse().unwrap());
98            }
99            if let Some(ref scenario_id) = scenario_id {
100                response
101                    .headers_mut()
102                    .insert("X-MockForge-Scenario", scenario_id.parse().unwrap());
103            }
104            response
105                .headers_mut()
106                .insert("X-MockForge-Reality-Level", reality_level.to_string().parse().unwrap());
107            response
108                .headers_mut()
109                .insert("X-MockForge-Reality-Ratio", reality_ratio.to_string().parse().unwrap());
110            if !chaos_rules.is_empty() {
111                response
112                    .headers_mut()
113                    .insert("X-MockForge-Chaos-Rules", chaos_rules.join(",").parse().unwrap());
114            }
115
116            return response;
117        } else {
118            debug!("No unified state found for workspace {}", workspace_id);
119        }
120    }
121
122    // Continue without unified state if not available
123    next.run(req).await
124}