mockforge_foundation/reality_continuum/
field_mixer.rs1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
11#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
12#[serde(rename_all = "lowercase")]
13pub enum RealitySource {
14 Real,
16 Mock,
18 Recorded,
20 Synthetic,
22}
23
24impl RealitySource {
25 pub fn to_blend_ratio(&self) -> f64 {
27 match self {
28 RealitySource::Real => 1.0,
29 RealitySource::Mock => 0.0,
30 RealitySource::Recorded => 0.5, RealitySource::Synthetic => 0.0, }
33 }
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
38#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
39pub struct FieldPattern {
40 pub path: String,
42 pub source: RealitySource,
44 #[serde(default)]
46 pub priority: i32,
47}
48
49impl FieldPattern {
50 pub fn matches(&self, json_path: &str) -> bool {
58 if self.path == "*" {
59 return true;
60 }
61
62 if self.path.ends_with(".*") {
64 let prefix = &self.path[..self.path.len() - 2];
65 return json_path.starts_with(prefix) && json_path.len() > prefix.len();
66 }
67
68 if self.path.starts_with("*.") {
69 let suffix = &self.path[2..];
70 return json_path.ends_with(suffix) && json_path.len() > suffix.len();
71 }
72
73 self.path == json_path
75 }
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
80#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
81pub struct EntityRealityRule {
82 pub entity_type: String,
84 pub source: RealitySource,
86 #[serde(default)]
88 pub field_overrides: HashMap<String, RealitySource>,
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize, Default)]
96#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
97pub struct FieldRealityConfig {
98 #[serde(default)]
100 pub enabled: bool,
101 #[serde(default)]
103 pub field_patterns: Vec<FieldPattern>,
104 #[serde(default)]
106 pub entity_rules: HashMap<String, EntityRealityRule>,
107 #[serde(default)]
109 pub default_source: Option<RealitySource>,
110}
111
112impl FieldRealityConfig {
113 pub fn new() -> Self {
115 Self::default()
116 }
117
118 pub fn enable(mut self) -> Self {
120 self.enabled = true;
121 self
122 }
123
124 pub fn add_field_pattern(mut self, pattern: FieldPattern) -> Self {
126 self.field_patterns.push(pattern);
128 self.field_patterns.sort_by_key(|p| std::cmp::Reverse(p.priority));
129 self
130 }
131
132 pub fn add_entity_rule(mut self, entity_type: String, rule: EntityRealityRule) -> Self {
134 self.entity_rules.insert(entity_type, rule);
135 self
136 }
137
138 pub fn get_source_for_path(&self, json_path: &str) -> Option<RealitySource> {
147 if !self.enabled {
148 return None;
149 }
150
151 for pattern in &self.field_patterns {
153 if pattern.matches(json_path) {
154 return Some(pattern.source);
155 }
156 }
157
158 if let Some(dot_pos) = json_path.find('.') {
161 let entity_type = &json_path[..dot_pos];
162 if let Some(rule) = self.entity_rules.get(entity_type) {
163 let field = &json_path[dot_pos + 1..];
165 if let Some(override_source) = rule.field_overrides.get(field) {
166 return Some(*override_source);
167 }
168 return Some(rule.source);
169 }
170 } else {
171 if let Some(rule) = self.entity_rules.get(json_path) {
173 return Some(rule.source);
174 }
175 }
176
177 self.default_source
179 }
180
181 pub fn get_blend_ratio_for_path(&self, json_path: &str) -> Option<f64> {
186 self.get_source_for_path(json_path).map(|source| source.to_blend_ratio())
187 }
188}
189
190#[cfg(test)]
191mod tests {
192 use super::*;
193
194 #[test]
195 fn test_field_pattern_exact_match() {
196 let pattern = FieldPattern {
197 path: "id".to_string(),
198 source: RealitySource::Recorded,
199 priority: 0,
200 };
201 assert!(pattern.matches("id"));
202 assert!(!pattern.matches("email"));
203 }
204
205 #[test]
206 fn test_field_pattern_wildcard_suffix() {
207 let pattern = FieldPattern {
208 path: "*.currency".to_string(),
209 source: RealitySource::Real,
210 priority: 0,
211 };
212 assert!(pattern.matches("user.currency"));
213 assert!(pattern.matches("order.currency"));
214 assert!(!pattern.matches("currency"));
215 }
216
217 #[test]
218 fn test_field_pattern_wildcard_prefix() {
219 let pattern = FieldPattern {
220 path: "user.*".to_string(),
221 source: RealitySource::Synthetic,
222 priority: 0,
223 };
224 assert!(pattern.matches("user.id"));
225 assert!(pattern.matches("user.email"));
226 assert!(!pattern.matches("order.id"));
227 }
228
229 #[test]
230 fn test_field_reality_config_path_matching() {
231 let mut config = FieldRealityConfig::new().enable();
232 config = config.add_field_pattern(FieldPattern {
233 path: "id".to_string(),
234 source: RealitySource::Recorded,
235 priority: 10,
236 });
237 config = config.add_field_pattern(FieldPattern {
238 path: "*.pii".to_string(),
239 source: RealitySource::Synthetic,
240 priority: 5,
241 });
242
243 assert_eq!(config.get_source_for_path("id"), Some(RealitySource::Recorded));
244 assert_eq!(config.get_source_for_path("user.pii"), Some(RealitySource::Synthetic));
245 }
246
247 #[test]
248 fn test_entity_rule() {
249 let mut config = FieldRealityConfig::new().enable();
250 let mut rule = EntityRealityRule {
251 entity_type: "currency".to_string(),
252 source: RealitySource::Real,
253 field_overrides: HashMap::new(),
254 };
255 rule.field_overrides.insert("rate".to_string(), RealitySource::Real);
256 config = config.add_entity_rule("currency".to_string(), rule);
257
258 assert_eq!(config.get_source_for_path("currency"), Some(RealitySource::Real));
259 assert_eq!(config.get_source_for_path("currency.rate"), Some(RealitySource::Real));
260 }
261}