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")]
14pub enum TransitionMode {
15 TimeBased,
17 Manual,
19 Scheduled,
21}
22
23impl Default for TransitionMode {
24 fn default() -> Self {
25 TransitionMode::Manual
26 }
27}
28
29#[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 FieldLevel,
36 Weighted,
38 BodyBlend,
40}
41
42impl Default for MergeStrategy {
43 fn default() -> Self {
44 MergeStrategy::FieldLevel
45 }
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
50#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
51pub struct ContinuumConfig {
52 #[serde(default = "default_false")]
54 pub enabled: bool,
55 #[serde(default = "default_blend_ratio")]
57 pub default_ratio: f64,
58 #[serde(default)]
60 pub transition_mode: TransitionMode,
61 #[serde(skip_serializing_if = "Option::is_none")]
63 pub time_schedule: Option<crate::reality_continuum::TimeSchedule>,
64 #[serde(default)]
66 pub merge_strategy: MergeStrategy,
67 #[serde(default)]
69 pub routes: Vec<ContinuumRule>,
70 #[serde(default)]
72 pub groups: HashMap<String, f64>,
73 #[serde(skip_serializing_if = "Option::is_none")]
75 pub field_mixing: Option<crate::reality_continuum::FieldRealityConfig>,
76 #[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 }
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 pub fn new() -> Self {
108 Self::default()
109 }
110
111 pub fn enable(mut self) -> Self {
113 self.enabled = true;
114 self
115 }
116
117 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 pub fn with_transition_mode(mut self, mode: TransitionMode) -> Self {
125 self.transition_mode = mode;
126 self
127 }
128
129 pub fn with_time_schedule(mut self, schedule: crate::reality_continuum::TimeSchedule) -> Self {
131 self.time_schedule = Some(schedule);
132 self
133 }
134
135 pub fn with_merge_strategy(mut self, strategy: MergeStrategy) -> Self {
137 self.merge_strategy = strategy;
138 self
139 }
140
141 pub fn add_route(mut self, rule: ContinuumRule) -> Self {
143 self.routes.push(rule);
144 self
145 }
146
147 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 pub fn with_cross_protocol_state(mut self, config: CrossProtocolStateConfig) -> Self {
155 self.cross_protocol_state = Some(config);
156 self
157 }
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize)]
165#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
166pub struct CrossProtocolStateConfig {
167 pub state_model: String,
172
173 #[serde(default)]
178 pub share_state_across: Vec<Protocol>,
179
180 #[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 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 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 pub fn with_protocols(mut self, protocols: Vec<Protocol>) -> Self {
215 self.share_state_across = protocols;
216 self
217 }
218
219 pub fn should_share_state(&self, protocol: &Protocol) -> bool {
221 self.enabled && self.share_state_across.contains(protocol)
222 }
223}
224
225#[derive(Debug, Clone, Serialize, Deserialize)]
227#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
228pub struct ContinuumRule {
229 pub pattern: String,
231 pub ratio: f64,
233 #[serde(skip_serializing_if = "Option::is_none")]
235 pub group: Option<String>,
236 #[serde(default = "default_true")]
238 pub enabled: bool,
239}
240
241fn default_true() -> bool {
242 true
243}
244
245impl ContinuumRule {
246 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 pub fn with_group(mut self, group: String) -> Self {
258 self.group = Some(group);
259 self
260 }
261
262 pub fn matches_path(&self, path: &str) -> bool {
264 if !self.enabled {
265 return false;
266 }
267
268 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}