mockforge_core/reality_continuum/
engine.rs

1//! Reality Continuum Engine
2//!
3//! Manages blend ratios for gradually transitioning from mock to real data sources.
4//! Supports time-based progression, manual configuration, and per-route/group/global settings.
5
6use crate::reality_continuum::{
7    ContinuumConfig, ContinuumRule, ResponseBlender, TimeSchedule, TransitionMode,
8};
9use chrono::{DateTime, Utc};
10use std::collections::HashMap;
11use std::sync::Arc;
12use tokio::sync::RwLock;
13use tracing::{debug, info, warn};
14
15/// Reality Continuum Engine
16///
17/// Manages blend ratios for routes, groups, and global settings.
18/// Calculates current blend ratios based on transition mode and time.
19#[derive(Debug, Clone)]
20pub struct RealityContinuumEngine {
21    /// Configuration
22    config: Arc<RwLock<ContinuumConfig>>,
23    /// Response blender
24    blender: ResponseBlender,
25    /// Optional virtual clock for time-based progression
26    virtual_clock: Option<Arc<crate::time_travel::VirtualClock>>,
27    /// Manual blend ratio overrides (path -> ratio)
28    manual_overrides: Arc<RwLock<HashMap<String, f64>>>,
29}
30
31impl RealityContinuumEngine {
32    /// Create a new continuum engine with the given configuration
33    pub fn new(config: ContinuumConfig) -> Self {
34        let blender = ResponseBlender::new(config.merge_strategy);
35        Self {
36            config: Arc::new(RwLock::new(config)),
37            blender,
38            virtual_clock: None,
39            manual_overrides: Arc::new(RwLock::new(HashMap::new())),
40        }
41    }
42
43    /// Create a new continuum engine with virtual clock integration
44    pub fn with_virtual_clock(
45        config: ContinuumConfig,
46        virtual_clock: Arc<crate::time_travel::VirtualClock>,
47    ) -> Self {
48        let blender = ResponseBlender::new(config.merge_strategy);
49        Self {
50            config: Arc::new(RwLock::new(config)),
51            blender,
52            virtual_clock: Some(virtual_clock),
53            manual_overrides: Arc::new(RwLock::new(HashMap::new())),
54        }
55    }
56
57    /// Set the virtual clock (can be called after construction)
58    pub fn set_virtual_clock(&mut self, virtual_clock: Arc<crate::time_travel::VirtualClock>) {
59        self.virtual_clock = Some(virtual_clock);
60    }
61
62    /// Get the current blend ratio for a path
63    ///
64    /// Checks in order:
65    /// 1. Manual overrides
66    /// 2. Route-specific rules
67    /// 3. Group-level overrides
68    /// 4. Time-based schedule (if enabled)
69    /// 5. Default ratio
70    pub async fn get_blend_ratio(&self, path: &str) -> f64 {
71        // Check manual overrides first
72        {
73            let overrides = self.manual_overrides.read().await;
74            if let Some(&ratio) = overrides.get(path) {
75                debug!("Using manual override for {}: {}", path, ratio);
76                return ratio;
77            }
78        }
79
80        let config = self.config.read().await;
81
82        // Check route-specific rules
83        for rule in &config.routes {
84            if rule.matches_path(path) {
85                debug!("Using route rule for {}: {}", path, rule.ratio);
86                return rule.ratio;
87            }
88        }
89
90        // Check group-level overrides (if path belongs to a group)
91        // Note: This requires integration with proxy config to determine groups
92        // For now, we'll check if any route rule has a group and matches
93        for rule in &config.routes {
94            if let Some(ref group) = rule.group {
95                if rule.matches_path(path) {
96                    if let Some(&group_ratio) = config.groups.get(group) {
97                        debug!(
98                            "Using group override for {} (group {}): {}",
99                            path, group, group_ratio
100                        );
101                        return group_ratio;
102                    }
103                }
104            }
105        }
106
107        // Check time-based schedule if enabled
108        if config.transition_mode == TransitionMode::TimeBased
109            || config.transition_mode == TransitionMode::Scheduled
110        {
111            if let Some(ref schedule) = config.time_schedule {
112                let current_time = self.get_current_time().await;
113                let ratio = schedule.calculate_ratio(current_time);
114                debug!("Using time-based ratio for {}: {} (time: {})", path, ratio, current_time);
115                return ratio;
116            }
117        }
118
119        // Return default ratio
120        debug!("Using default ratio for {}: {}", path, config.default_ratio);
121        config.default_ratio
122    }
123
124    /// Set a manual blend ratio override for a path
125    pub async fn set_blend_ratio(&self, path: &str, ratio: f64) {
126        let ratio = ratio.clamp(0.0, 1.0);
127        let mut overrides = self.manual_overrides.write().await;
128        overrides.insert(path.to_string(), ratio);
129        info!("Set manual blend ratio for {}: {}", path, ratio);
130    }
131
132    /// Remove a manual blend ratio override
133    pub async fn remove_blend_ratio(&self, path: &str) {
134        let mut overrides = self.manual_overrides.write().await;
135        if overrides.remove(path).is_some() {
136            info!("Removed manual blend ratio override for {}", path);
137        }
138    }
139
140    /// Set blend ratio for a group
141    pub async fn set_group_ratio(&self, group: &str, ratio: f64) {
142        let ratio = ratio.clamp(0.0, 1.0);
143        let mut config = self.config.write().await;
144        config.groups.insert(group.to_string(), ratio);
145        info!("Set group blend ratio for {}: {}", group, ratio);
146    }
147
148    /// Update blend ratios based on current time
149    ///
150    /// This should be called periodically when using time-based progression.
151    pub async fn update_from_time(&self, _time: DateTime<Utc>) {
152        // The blend ratio calculation happens on-demand in get_blend_ratio,
153        // so this method is mainly for logging/observability purposes
154        debug!("Continuum engine updated from time: {}", _time);
155    }
156
157    /// Get the response blender
158    pub fn blender(&self) -> &ResponseBlender {
159        &self.blender
160    }
161
162    /// Get the current configuration
163    pub async fn get_config(&self) -> ContinuumConfig {
164        self.config.read().await.clone()
165    }
166
167    /// Update the configuration
168    pub async fn update_config(&self, config: ContinuumConfig) {
169        let mut current_config = self.config.write().await;
170        *current_config = config;
171        info!("Continuum configuration updated");
172    }
173
174    /// Check if continuum is enabled
175    pub async fn is_enabled(&self) -> bool {
176        self.config.read().await.enabled
177    }
178
179    /// Enable or disable continuum
180    pub async fn set_enabled(&self, enabled: bool) {
181        let mut config = self.config.write().await;
182        config.enabled = enabled;
183        if enabled {
184            info!("Reality Continuum enabled");
185        } else {
186            info!("Reality Continuum disabled");
187        }
188    }
189
190    /// Get the time schedule
191    pub async fn get_time_schedule(&self) -> Option<TimeSchedule> {
192        self.config.read().await.time_schedule.clone()
193    }
194
195    /// Update the time schedule
196    pub async fn set_time_schedule(&self, schedule: TimeSchedule) {
197        let mut config = self.config.write().await;
198        config.time_schedule = Some(schedule);
199        config.transition_mode = TransitionMode::TimeBased;
200        info!("Time schedule updated");
201    }
202
203    /// Get current time (virtual or real)
204    async fn get_current_time(&self) -> DateTime<Utc> {
205        if let Some(ref clock) = self.virtual_clock {
206            clock.now()
207        } else {
208            Utc::now()
209        }
210    }
211
212    /// Advance the blend ratio manually (for testing/debugging)
213    ///
214    /// This increments the default ratio by a small amount.
215    pub async fn advance_ratio(&self, increment: f64) {
216        let mut config = self.config.write().await;
217        let new_ratio = (config.default_ratio + increment).clamp(0.0, 1.0);
218        config.default_ratio = new_ratio;
219        info!("Advanced default blend ratio to {}", new_ratio);
220    }
221
222    /// Get all manual overrides
223    pub async fn get_manual_overrides(&self) -> HashMap<String, f64> {
224        self.manual_overrides.read().await.clone()
225    }
226
227    /// Clear all manual overrides
228    pub async fn clear_manual_overrides(&self) {
229        let mut overrides = self.manual_overrides.write().await;
230        overrides.clear();
231        info!("Cleared all manual blend ratio overrides");
232    }
233}
234
235impl Default for RealityContinuumEngine {
236    fn default() -> Self {
237        Self::new(ContinuumConfig::default())
238    }
239}
240
241#[cfg(test)]
242mod tests {
243    use super::*;
244
245    #[tokio::test]
246    async fn test_get_blend_ratio_default() {
247        let engine = RealityContinuumEngine::new(ContinuumConfig::default());
248        let ratio = engine.get_blend_ratio("/api/test").await;
249        assert_eq!(ratio, 0.0); // Default is 0.0 (100% mock)
250    }
251
252    #[tokio::test]
253    async fn test_set_get_blend_ratio() {
254        let engine = RealityContinuumEngine::new(ContinuumConfig::default());
255        engine.set_blend_ratio("/api/test", 0.75).await;
256        let ratio = engine.get_blend_ratio("/api/test").await;
257        assert_eq!(ratio, 0.75);
258    }
259
260    #[tokio::test]
261    async fn test_route_rule_matching() {
262        let mut config = ContinuumConfig::default();
263        config.routes.push(ContinuumRule::new("/api/users/*".to_string(), 0.5));
264        let engine = RealityContinuumEngine::new(config);
265
266        let ratio = engine.get_blend_ratio("/api/users/123").await;
267        assert_eq!(ratio, 0.5);
268    }
269
270    #[tokio::test]
271    async fn test_group_ratio() {
272        let mut config = ContinuumConfig::default();
273        config.groups.insert("api-v1".to_string(), 0.3);
274        config.routes.push(
275            ContinuumRule::new("/api/users/*".to_string(), 0.5).with_group("api-v1".to_string()),
276        );
277        let engine = RealityContinuumEngine::new(config);
278
279        // Group ratio should override route ratio
280        let ratio = engine.get_blend_ratio("/api/users/123").await;
281        assert_eq!(ratio, 0.3);
282    }
283
284    #[tokio::test]
285    async fn test_time_based_ratio() {
286        let start = Utc::now();
287        let end = start + chrono::Duration::days(30);
288        let schedule = TimeSchedule::new(start, end, 0.0, 1.0);
289
290        let mut config = ContinuumConfig::default();
291        config.transition_mode = TransitionMode::TimeBased;
292        config.time_schedule = Some(schedule);
293        let engine = RealityContinuumEngine::new(config);
294
295        // At start time, should return start_ratio
296        let ratio = engine.get_blend_ratio("/api/test").await;
297        // Should be close to 0.0 (start_ratio)
298        assert!(ratio < 0.1);
299    }
300
301    #[tokio::test]
302    async fn test_remove_blend_ratio() {
303        let engine = RealityContinuumEngine::new(ContinuumConfig::default());
304        engine.set_blend_ratio("/api/test", 0.75).await;
305        assert_eq!(engine.get_blend_ratio("/api/test").await, 0.75);
306
307        engine.remove_blend_ratio("/api/test").await;
308        assert_eq!(engine.get_blend_ratio("/api/test").await, 0.0); // Back to default
309    }
310
311    #[tokio::test]
312    async fn test_group_ratio_override() {
313        let mut config = ContinuumConfig::default();
314        config.groups.insert("api-v1".to_string(), 0.8);
315        config.routes.push(
316            ContinuumRule::new("/api/users/*".to_string(), 0.5).with_group("api-v1".to_string()),
317        );
318        let engine = RealityContinuumEngine::new(config);
319
320        // Group ratio should override route ratio
321        let ratio = engine.get_blend_ratio("/api/users/123").await;
322        assert_eq!(ratio, 0.8);
323    }
324
325    #[tokio::test]
326    async fn test_enable_disable() {
327        let engine = RealityContinuumEngine::new(ContinuumConfig::default());
328        assert!(!engine.is_enabled().await);
329
330        engine.set_enabled(true).await;
331        assert!(engine.is_enabled().await);
332
333        engine.set_enabled(false).await;
334        assert!(!engine.is_enabled().await);
335    }
336
337    #[tokio::test]
338    async fn test_advance_ratio() {
339        let engine = RealityContinuumEngine::new(ContinuumConfig::default());
340        assert_eq!(engine.get_config().await.default_ratio, 0.0);
341
342        engine.advance_ratio(0.2).await;
343        assert_eq!(engine.get_config().await.default_ratio, 0.2);
344
345        engine.advance_ratio(0.5).await;
346        assert_eq!(engine.get_config().await.default_ratio, 0.7);
347
348        // Should clamp at 1.0
349        engine.advance_ratio(0.5).await;
350        assert_eq!(engine.get_config().await.default_ratio, 1.0);
351    }
352
353    #[tokio::test]
354    async fn test_clear_manual_overrides() {
355        let engine = RealityContinuumEngine::new(ContinuumConfig::default());
356        engine.set_blend_ratio("/api/test1", 0.5).await;
357        engine.set_blend_ratio("/api/test2", 0.7).await;
358
359        let overrides = engine.get_manual_overrides().await;
360        assert_eq!(overrides.len(), 2);
361
362        engine.clear_manual_overrides().await;
363        let overrides = engine.get_manual_overrides().await;
364        assert_eq!(overrides.len(), 0);
365    }
366}