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