mockforge_core/reality_continuum/
config.rs1use crate::protocol_abstraction::Protocol;
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10#[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 TimeBased,
18 #[default]
20 Manual,
21 Scheduled,
23}
24
25#[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 #[default]
33 FieldLevel,
34 Weighted,
36 BodyBlend,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
42#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
43pub struct ContinuumConfig {
44 #[serde(default = "default_false")]
46 pub enabled: bool,
47 #[serde(default = "default_blend_ratio")]
49 pub default_ratio: f64,
50 #[serde(default)]
52 pub transition_mode: TransitionMode,
53 #[serde(skip_serializing_if = "Option::is_none")]
55 pub time_schedule: Option<crate::reality_continuum::TimeSchedule>,
56 #[serde(default)]
58 pub merge_strategy: MergeStrategy,
59 #[serde(default)]
61 pub routes: Vec<ContinuumRule>,
62 #[serde(default)]
64 pub groups: HashMap<String, f64>,
65 #[serde(skip_serializing_if = "Option::is_none")]
67 pub field_mixing: Option<crate::reality_continuum::FieldRealityConfig>,
68 #[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 }
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 pub fn new() -> Self {
100 Self::default()
101 }
102
103 pub fn enable(mut self) -> Self {
105 self.enabled = true;
106 self
107 }
108
109 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 pub fn with_transition_mode(mut self, mode: TransitionMode) -> Self {
117 self.transition_mode = mode;
118 self
119 }
120
121 pub fn with_time_schedule(mut self, schedule: crate::reality_continuum::TimeSchedule) -> Self {
123 self.time_schedule = Some(schedule);
124 self
125 }
126
127 pub fn with_merge_strategy(mut self, strategy: MergeStrategy) -> Self {
129 self.merge_strategy = strategy;
130 self
131 }
132
133 pub fn add_route(mut self, rule: ContinuumRule) -> Self {
135 self.routes.push(rule);
136 self
137 }
138
139 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 pub fn with_cross_protocol_state(mut self, config: CrossProtocolStateConfig) -> Self {
147 self.cross_protocol_state = Some(config);
148 self
149 }
150}
151
152#[derive(Debug, Clone, Serialize, Deserialize)]
157#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
158pub struct CrossProtocolStateConfig {
159 pub state_model: String,
164
165 #[serde(default)]
170 pub share_state_across: Vec<Protocol>,
171
172 #[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 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 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 pub fn with_protocols(mut self, protocols: Vec<Protocol>) -> Self {
207 self.share_state_across = protocols;
208 self
209 }
210
211 pub fn should_share_state(&self, protocol: &Protocol) -> bool {
213 self.enabled && self.share_state_across.contains(protocol)
214 }
215}
216
217#[derive(Debug, Clone, Serialize, Deserialize)]
219#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
220pub struct ContinuumRule {
221 pub pattern: String,
223 pub ratio: f64,
225 #[serde(skip_serializing_if = "Option::is_none")]
227 pub group: Option<String>,
228 #[serde(default = "default_true")]
230 pub enabled: bool,
231}
232
233fn default_true() -> bool {
234 true
235}
236
237impl ContinuumRule {
238 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 pub fn with_group(mut self, group: String) -> Self {
250 self.group = Some(group);
251 self
252 }
253
254 pub fn matches_path(&self, path: &str) -> bool {
256 if !self.enabled {
257 return false;
258 }
259
260 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}