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 crate::protocol_abstraction::Protocol;
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10/// Transition mode for blend ratio progression
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
12#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
13#[serde(rename_all = "snake_case")]
14#[derive(Default)]
15pub enum TransitionMode {
16    /// Time-based progression using virtual clock
17    TimeBased,
18    /// Manual configuration (blend ratio set explicitly)
19    #[default]
20    Manual,
21    /// Scheduled progression with fixed timeline
22    Scheduled,
23}
24
25/// Merge strategy for blending responses
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
27#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
28#[serde(rename_all = "snake_case")]
29#[derive(Default)]
30pub enum MergeStrategy {
31    /// Field-level intelligent merge (deep merge objects, combine arrays)
32    #[default]
33    FieldLevel,
34    /// Weighted selection (return mock with X% probability, real with (100-X)%)
35    Weighted,
36    /// Response body blending (merge arrays, average numeric fields)
37    BodyBlend,
38}
39
40/// Configuration for Reality Continuum
41#[derive(Debug, Clone, Serialize, Deserialize)]
42#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
43pub struct ContinuumConfig {
44    /// Whether the continuum feature is enabled
45    #[serde(default = "default_false")]
46    pub enabled: bool,
47    /// Default blend ratio (0.0 = 100% mock, 1.0 = 100% real)
48    #[serde(default = "default_blend_ratio")]
49    pub default_ratio: f64,
50    /// Transition mode for blend ratio progression
51    #[serde(default)]
52    pub transition_mode: TransitionMode,
53    /// Time schedule for time-based transitions (optional)
54    #[serde(skip_serializing_if = "Option::is_none")]
55    pub time_schedule: Option<crate::reality_continuum::TimeSchedule>,
56    /// Merge strategy for blending responses
57    #[serde(default)]
58    pub merge_strategy: MergeStrategy,
59    /// Per-route blend ratio overrides
60    #[serde(default)]
61    pub routes: Vec<ContinuumRule>,
62    /// Group-level blend ratio overrides
63    #[serde(default)]
64    pub groups: HashMap<String, f64>,
65    /// Field-level reality mixing configuration
66    #[serde(skip_serializing_if = "Option::is_none")]
67    pub field_mixing: Option<crate::reality_continuum::FieldRealityConfig>,
68    /// Cross-protocol state sharing configuration
69    #[serde(skip_serializing_if = "Option::is_none")]
70    pub cross_protocol_state: Option<CrossProtocolStateConfig>,
71}
72
73fn default_false() -> bool {
74    false
75}
76
77fn default_blend_ratio() -> f64 {
78    0.0 // Start with 100% mock
79}
80
81impl Default for ContinuumConfig {
82    fn default() -> Self {
83        Self {
84            enabled: false,
85            default_ratio: 0.0,
86            transition_mode: TransitionMode::Manual,
87            time_schedule: None,
88            merge_strategy: MergeStrategy::FieldLevel,
89            routes: Vec::new(),
90            groups: HashMap::new(),
91            field_mixing: None,
92            cross_protocol_state: None,
93        }
94    }
95}
96
97impl ContinuumConfig {
98    /// Create a new continuum configuration
99    pub fn new() -> Self {
100        Self::default()
101    }
102
103    /// Enable the continuum feature
104    pub fn enable(mut self) -> Self {
105        self.enabled = true;
106        self
107    }
108
109    /// Set the default blend ratio
110    pub fn with_default_ratio(mut self, ratio: f64) -> Self {
111        self.default_ratio = ratio.clamp(0.0, 1.0);
112        self
113    }
114
115    /// Set the transition mode
116    pub fn with_transition_mode(mut self, mode: TransitionMode) -> Self {
117        self.transition_mode = mode;
118        self
119    }
120
121    /// Set the time schedule
122    pub fn with_time_schedule(mut self, schedule: crate::reality_continuum::TimeSchedule) -> Self {
123        self.time_schedule = Some(schedule);
124        self
125    }
126
127    /// Set the merge strategy
128    pub fn with_merge_strategy(mut self, strategy: MergeStrategy) -> Self {
129        self.merge_strategy = strategy;
130        self
131    }
132
133    /// Add a route-specific rule
134    pub fn add_route(mut self, rule: ContinuumRule) -> Self {
135        self.routes.push(rule);
136        self
137    }
138
139    /// Set a group-level blend ratio
140    pub fn set_group_ratio(mut self, group: String, ratio: f64) -> Self {
141        self.groups.insert(group, ratio.clamp(0.0, 1.0));
142        self
143    }
144
145    /// Set cross-protocol state configuration
146    pub fn with_cross_protocol_state(mut self, config: CrossProtocolStateConfig) -> Self {
147        self.cross_protocol_state = Some(config);
148        self
149    }
150}
151
152/// Cross-protocol state sharing configuration
153///
154/// Ensures that HTTP, WebSocket, gRPC, TCP, and webhooks all use the same
155/// backing persona graph and unified state when configured.
156#[derive(Debug, Clone, Serialize, Deserialize)]
157#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
158pub struct CrossProtocolStateConfig {
159    /// State model identifier (e.g., "ecommerce_v1", "finance_v1")
160    ///
161    /// This identifies the shared state model that defines how personas
162    /// and entities are related across protocols.
163    pub state_model: String,
164
165    /// List of protocols that should share state
166    ///
167    /// When a protocol is included, it will use the same persona graph
168    /// and unified state as other protocols in this list.
169    #[serde(default)]
170    pub share_state_across: Vec<Protocol>,
171
172    /// Whether cross-protocol state sharing is enabled
173    #[serde(default = "default_true")]
174    pub enabled: bool,
175}
176
177impl Default for CrossProtocolStateConfig {
178    fn default() -> Self {
179        Self {
180            state_model: "default".to_string(),
181            share_state_across: vec![Protocol::Http, Protocol::WebSocket, Protocol::Grpc],
182            enabled: true,
183        }
184    }
185}
186
187impl CrossProtocolStateConfig {
188    /// Create a new cross-protocol state configuration
189    pub fn new(state_model: String) -> Self {
190        Self {
191            state_model,
192            share_state_across: Vec::new(),
193            enabled: true,
194        }
195    }
196
197    /// Add a protocol to share state across
198    pub fn add_protocol(mut self, protocol: Protocol) -> Self {
199        if !self.share_state_across.contains(&protocol) {
200            self.share_state_across.push(protocol);
201        }
202        self
203    }
204
205    /// Set the list of protocols to share state across
206    pub fn with_protocols(mut self, protocols: Vec<Protocol>) -> Self {
207        self.share_state_across = protocols;
208        self
209    }
210
211    /// Check if a protocol should share state
212    pub fn should_share_state(&self, protocol: &Protocol) -> bool {
213        self.enabled && self.share_state_across.contains(protocol)
214    }
215}
216
217/// Rule for per-route continuum configuration
218#[derive(Debug, Clone, Serialize, Deserialize)]
219#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
220pub struct ContinuumRule {
221    /// Path pattern to match (supports wildcards like "/api/users/*")
222    pub pattern: String,
223    /// Blend ratio for this route (0.0 = 100% mock, 1.0 = 100% real)
224    pub ratio: f64,
225    /// Optional migration group this route belongs to
226    #[serde(skip_serializing_if = "Option::is_none")]
227    pub group: Option<String>,
228    /// Whether this rule is enabled
229    #[serde(default = "default_true")]
230    pub enabled: bool,
231}
232
233fn default_true() -> bool {
234    true
235}
236
237impl ContinuumRule {
238    /// Create a new continuum rule
239    pub fn new(pattern: String, ratio: f64) -> Self {
240        Self {
241            pattern,
242            ratio: ratio.clamp(0.0, 1.0),
243            group: None,
244            enabled: true,
245        }
246    }
247
248    /// Set the migration group
249    pub fn with_group(mut self, group: String) -> Self {
250        self.group = Some(group);
251        self
252    }
253
254    /// Check if a path matches this rule's pattern
255    pub fn matches_path(&self, path: &str) -> bool {
256        if !self.enabled {
257            return false;
258        }
259
260        // Simple pattern matching - supports wildcards
261        if self.pattern.ends_with("/*") {
262            let prefix = &self.pattern[..self.pattern.len() - 2];
263            path.starts_with(prefix)
264        } else {
265            path == self.pattern || path.starts_with(&self.pattern)
266        }
267    }
268}
269
270#[cfg(test)]
271mod tests {
272    use super::*;
273
274    #[test]
275    fn test_continuum_config_default() {
276        let config = ContinuumConfig::default();
277        assert!(!config.enabled);
278        assert_eq!(config.default_ratio, 0.0);
279        assert_eq!(config.transition_mode, TransitionMode::Manual);
280    }
281
282    #[test]
283    fn test_continuum_config_builder() {
284        let config = ContinuumConfig::new()
285            .enable()
286            .with_default_ratio(0.5)
287            .with_transition_mode(TransitionMode::TimeBased);
288
289        assert!(config.enabled);
290        assert_eq!(config.default_ratio, 0.5);
291        assert_eq!(config.transition_mode, TransitionMode::TimeBased);
292    }
293
294    #[test]
295    fn test_continuum_rule_matching() {
296        let rule = ContinuumRule::new("/api/users/*".to_string(), 0.5);
297        assert!(rule.matches_path("/api/users/123"));
298        assert!(rule.matches_path("/api/users/456"));
299        assert!(!rule.matches_path("/api/orders/123"));
300
301        let exact_rule = ContinuumRule::new("/api/health".to_string(), 0.0);
302        assert!(exact_rule.matches_path("/api/health"));
303        assert!(!exact_rule.matches_path("/api/health/check"));
304    }
305
306    #[test]
307    fn test_ratio_clamping() {
308        let rule = ContinuumRule::new("/test".to_string(), 1.5);
309        assert_eq!(rule.ratio, 1.0);
310
311        let rule = ContinuumRule::new("/test".to_string(), -0.5);
312        assert_eq!(rule.ratio, 0.0);
313    }
314}