mockforge_core/reality_continuum/
config.rs1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
11#[serde(rename_all = "snake_case")]
12pub enum TransitionMode {
13 TimeBased,
15 Manual,
17 Scheduled,
19}
20
21impl Default for TransitionMode {
22 fn default() -> Self {
23 TransitionMode::Manual
24 }
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
29#[serde(rename_all = "snake_case")]
30pub enum MergeStrategy {
31 FieldLevel,
33 Weighted,
35 BodyBlend,
37}
38
39impl Default for MergeStrategy {
40 fn default() -> Self {
41 MergeStrategy::FieldLevel
42 }
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct ContinuumConfig {
48 #[serde(default = "default_false")]
50 pub enabled: bool,
51 #[serde(default = "default_blend_ratio")]
53 pub default_ratio: f64,
54 #[serde(default)]
56 pub transition_mode: TransitionMode,
57 #[serde(skip_serializing_if = "Option::is_none")]
59 pub time_schedule: Option<crate::reality_continuum::TimeSchedule>,
60 #[serde(default)]
62 pub merge_strategy: MergeStrategy,
63 #[serde(default)]
65 pub routes: Vec<ContinuumRule>,
66 #[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 }
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 pub fn new() -> Self {
96 Self::default()
97 }
98
99 pub fn enable(mut self) -> Self {
101 self.enabled = true;
102 self
103 }
104
105 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 pub fn with_transition_mode(mut self, mode: TransitionMode) -> Self {
113 self.transition_mode = mode;
114 self
115 }
116
117 pub fn with_time_schedule(mut self, schedule: crate::reality_continuum::TimeSchedule) -> Self {
119 self.time_schedule = Some(schedule);
120 self
121 }
122
123 pub fn with_merge_strategy(mut self, strategy: MergeStrategy) -> Self {
125 self.merge_strategy = strategy;
126 self
127 }
128
129 pub fn add_route(mut self, rule: ContinuumRule) -> Self {
131 self.routes.push(rule);
132 self
133 }
134
135 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#[derive(Debug, Clone, Serialize, Deserialize)]
144pub struct ContinuumRule {
145 pub pattern: String,
147 pub ratio: f64,
149 #[serde(skip_serializing_if = "Option::is_none")]
151 pub group: Option<String>,
152 #[serde(default = "default_true")]
154 pub enabled: bool,
155}
156
157fn default_true() -> bool {
158 true
159}
160
161impl ContinuumRule {
162 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 pub fn with_group(mut self, group: String) -> Self {
174 self.group = Some(group);
175 self
176 }
177
178 pub fn matches_path(&self, path: &str) -> bool {
180 if !self.enabled {
181 return false;
182 }
183
184 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}