Skip to main content

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