mockforge_core/reality_continuum/
config.rs

1//! Configuration types for Reality Continuum
2//!
3//! Defines the configuration structures for blending mock and real data sources,
4//! including transition modes, schedules, and merge strategies.
5
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9/// Transition mode for blend ratio progression
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
11#[serde(rename_all = "snake_case")]
12pub enum TransitionMode {
13    /// Time-based progression using virtual clock
14    TimeBased,
15    /// Manual configuration (blend ratio set explicitly)
16    Manual,
17    /// Scheduled progression with fixed timeline
18    Scheduled,
19}
20
21impl Default for TransitionMode {
22    fn default() -> Self {
23        TransitionMode::Manual
24    }
25}
26
27/// Merge strategy for blending responses
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
29#[serde(rename_all = "snake_case")]
30pub enum MergeStrategy {
31    /// Field-level intelligent merge (deep merge objects, combine arrays)
32    FieldLevel,
33    /// Weighted selection (return mock with X% probability, real with (100-X)%)
34    Weighted,
35    /// Response body blending (merge arrays, average numeric fields)
36    BodyBlend,
37}
38
39impl Default for MergeStrategy {
40    fn default() -> Self {
41        MergeStrategy::FieldLevel
42    }
43}
44
45/// Configuration for Reality Continuum
46#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct ContinuumConfig {
48    /// Whether the continuum feature is enabled
49    #[serde(default = "default_false")]
50    pub enabled: bool,
51    /// Default blend ratio (0.0 = 100% mock, 1.0 = 100% real)
52    #[serde(default = "default_blend_ratio")]
53    pub default_ratio: f64,
54    /// Transition mode for blend ratio progression
55    #[serde(default)]
56    pub transition_mode: TransitionMode,
57    /// Time schedule for time-based transitions (optional)
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub time_schedule: Option<crate::reality_continuum::TimeSchedule>,
60    /// Merge strategy for blending responses
61    #[serde(default)]
62    pub merge_strategy: MergeStrategy,
63    /// Per-route blend ratio overrides
64    #[serde(default)]
65    pub routes: Vec<ContinuumRule>,
66    /// Group-level blend ratio overrides
67    #[serde(default)]
68    pub groups: HashMap<String, f64>,
69}
70
71fn default_false() -> bool {
72    false
73}
74
75fn default_blend_ratio() -> f64 {
76    0.0 // Start with 100% mock
77}
78
79impl Default for ContinuumConfig {
80    fn default() -> Self {
81        Self {
82            enabled: false,
83            default_ratio: 0.0,
84            transition_mode: TransitionMode::Manual,
85            time_schedule: None,
86            merge_strategy: MergeStrategy::FieldLevel,
87            routes: Vec::new(),
88            groups: HashMap::new(),
89        }
90    }
91}
92
93impl ContinuumConfig {
94    /// Create a new continuum configuration
95    pub fn new() -> Self {
96        Self::default()
97    }
98
99    /// Enable the continuum feature
100    pub fn enable(mut self) -> Self {
101        self.enabled = true;
102        self
103    }
104
105    /// Set the default blend ratio
106    pub fn with_default_ratio(mut self, ratio: f64) -> Self {
107        self.default_ratio = ratio.clamp(0.0, 1.0);
108        self
109    }
110
111    /// Set the transition mode
112    pub fn with_transition_mode(mut self, mode: TransitionMode) -> Self {
113        self.transition_mode = mode;
114        self
115    }
116
117    /// Set the time schedule
118    pub fn with_time_schedule(mut self, schedule: crate::reality_continuum::TimeSchedule) -> Self {
119        self.time_schedule = Some(schedule);
120        self
121    }
122
123    /// Set the merge strategy
124    pub fn with_merge_strategy(mut self, strategy: MergeStrategy) -> Self {
125        self.merge_strategy = strategy;
126        self
127    }
128
129    /// Add a route-specific rule
130    pub fn add_route(mut self, rule: ContinuumRule) -> Self {
131        self.routes.push(rule);
132        self
133    }
134
135    /// Set a group-level blend ratio
136    pub fn set_group_ratio(mut self, group: String, ratio: f64) -> Self {
137        self.groups.insert(group, ratio.clamp(0.0, 1.0));
138        self
139    }
140}
141
142/// Rule for per-route continuum configuration
143#[derive(Debug, Clone, Serialize, Deserialize)]
144pub struct ContinuumRule {
145    /// Path pattern to match (supports wildcards like "/api/users/*")
146    pub pattern: String,
147    /// Blend ratio for this route (0.0 = 100% mock, 1.0 = 100% real)
148    pub ratio: f64,
149    /// Optional migration group this route belongs to
150    #[serde(skip_serializing_if = "Option::is_none")]
151    pub group: Option<String>,
152    /// Whether this rule is enabled
153    #[serde(default = "default_true")]
154    pub enabled: bool,
155}
156
157fn default_true() -> bool {
158    true
159}
160
161impl ContinuumRule {
162    /// Create a new continuum rule
163    pub fn new(pattern: String, ratio: f64) -> Self {
164        Self {
165            pattern,
166            ratio: ratio.clamp(0.0, 1.0),
167            group: None,
168            enabled: true,
169        }
170    }
171
172    /// Set the migration group
173    pub fn with_group(mut self, group: String) -> Self {
174        self.group = Some(group);
175        self
176    }
177
178    /// Check if a path matches this rule's pattern
179    pub fn matches_path(&self, path: &str) -> bool {
180        if !self.enabled {
181            return false;
182        }
183
184        // Simple pattern matching - supports wildcards
185        if self.pattern.ends_with("/*") {
186            let prefix = &self.pattern[..self.pattern.len() - 2];
187            path.starts_with(prefix)
188        } else {
189            path == self.pattern || path.starts_with(&self.pattern)
190        }
191    }
192}
193
194#[cfg(test)]
195mod tests {
196    use super::*;
197
198    #[test]
199    fn test_continuum_config_default() {
200        let config = ContinuumConfig::default();
201        assert!(!config.enabled);
202        assert_eq!(config.default_ratio, 0.0);
203        assert_eq!(config.transition_mode, TransitionMode::Manual);
204    }
205
206    #[test]
207    fn test_continuum_config_builder() {
208        let config = ContinuumConfig::new()
209            .enable()
210            .with_default_ratio(0.5)
211            .with_transition_mode(TransitionMode::TimeBased);
212
213        assert!(config.enabled);
214        assert_eq!(config.default_ratio, 0.5);
215        assert_eq!(config.transition_mode, TransitionMode::TimeBased);
216    }
217
218    #[test]
219    fn test_continuum_rule_matching() {
220        let rule = ContinuumRule::new("/api/users/*".to_string(), 0.5);
221        assert!(rule.matches_path("/api/users/123"));
222        assert!(rule.matches_path("/api/users/456"));
223        assert!(!rule.matches_path("/api/orders/123"));
224
225        let exact_rule = ContinuumRule::new("/api/health".to_string(), 0.0);
226        assert!(exact_rule.matches_path("/api/health"));
227        assert!(!exact_rule.matches_path("/api/health/check"));
228    }
229
230    #[test]
231    fn test_ratio_clamping() {
232        let rule = ContinuumRule::new("/test".to_string(), 1.5);
233        assert_eq!(rule.ratio, 1.0);
234
235        let rule = ContinuumRule::new("/test".to_string(), -0.5);
236        assert_eq!(rule.ratio, 0.0);
237    }
238}