use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "lowercase")]
pub enum RealitySource {
Real,
Mock,
Recorded,
Synthetic,
}
impl RealitySource {
pub fn to_blend_ratio(&self) -> f64 {
match self {
RealitySource::Real => 1.0,
RealitySource::Mock => 0.0,
RealitySource::Recorded => 0.5, RealitySource::Synthetic => 0.0, }
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct FieldPattern {
pub path: String,
pub source: RealitySource,
#[serde(default)]
pub priority: i32,
}
impl FieldPattern {
pub fn matches(&self, json_path: &str) -> bool {
if self.path == "*" {
return true;
}
if self.path.ends_with(".*") {
let prefix = &self.path[..self.path.len() - 2];
return json_path.starts_with(prefix) && json_path.len() > prefix.len();
}
if self.path.starts_with("*.") {
let suffix = &self.path[2..];
return json_path.ends_with(suffix) && json_path.len() > suffix.len();
}
self.path == json_path
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct EntityRealityRule {
pub entity_type: String,
pub source: RealitySource,
#[serde(default)]
pub field_overrides: HashMap<String, RealitySource>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct FieldRealityConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default)]
pub field_patterns: Vec<FieldPattern>,
#[serde(default)]
pub entity_rules: HashMap<String, EntityRealityRule>,
#[serde(default)]
pub default_source: Option<RealitySource>,
}
impl FieldRealityConfig {
pub fn new() -> Self {
Self::default()
}
pub fn enable(mut self) -> Self {
self.enabled = true;
self
}
pub fn add_field_pattern(mut self, pattern: FieldPattern) -> Self {
self.field_patterns.push(pattern);
self.field_patterns.sort_by(|a, b| b.priority.cmp(&a.priority));
self
}
pub fn add_entity_rule(mut self, entity_type: String, rule: EntityRealityRule) -> Self {
self.entity_rules.insert(entity_type, rule);
self
}
pub fn get_source_for_path(&self, json_path: &str) -> Option<RealitySource> {
if !self.enabled {
return None;
}
for pattern in &self.field_patterns {
if pattern.matches(json_path) {
return Some(pattern.source);
}
}
if let Some(dot_pos) = json_path.find('.') {
let entity_type = &json_path[..dot_pos];
if let Some(rule) = self.entity_rules.get(entity_type) {
let field = &json_path[dot_pos + 1..];
if let Some(override_source) = rule.field_overrides.get(field) {
return Some(*override_source);
}
return Some(rule.source);
}
} else {
if let Some(rule) = self.entity_rules.get(json_path) {
return Some(rule.source);
}
}
self.default_source
}
pub fn get_blend_ratio_for_path(&self, json_path: &str) -> Option<f64> {
self.get_source_for_path(json_path).map(|source| source.to_blend_ratio())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_field_pattern_exact_match() {
let pattern = FieldPattern {
path: "id".to_string(),
source: RealitySource::Recorded,
priority: 0,
};
assert!(pattern.matches("id"));
assert!(!pattern.matches("email"));
}
#[test]
fn test_field_pattern_wildcard_suffix() {
let pattern = FieldPattern {
path: "*.currency".to_string(),
source: RealitySource::Real,
priority: 0,
};
assert!(pattern.matches("user.currency"));
assert!(pattern.matches("order.currency"));
assert!(!pattern.matches("currency"));
}
#[test]
fn test_field_pattern_wildcard_prefix() {
let pattern = FieldPattern {
path: "user.*".to_string(),
source: RealitySource::Synthetic,
priority: 0,
};
assert!(pattern.matches("user.id"));
assert!(pattern.matches("user.email"));
assert!(!pattern.matches("order.id"));
}
#[test]
fn test_field_reality_config_path_matching() {
let mut config = FieldRealityConfig::new().enable();
config = config.add_field_pattern(FieldPattern {
path: "id".to_string(),
source: RealitySource::Recorded,
priority: 10,
});
config = config.add_field_pattern(FieldPattern {
path: "*.pii".to_string(),
source: RealitySource::Synthetic,
priority: 5,
});
assert_eq!(config.get_source_for_path("id"), Some(RealitySource::Recorded));
assert_eq!(config.get_source_for_path("user.pii"), Some(RealitySource::Synthetic));
}
#[test]
fn test_entity_rule() {
let mut config = FieldRealityConfig::new().enable();
let mut rule = EntityRealityRule {
entity_type: "currency".to_string(),
source: RealitySource::Real,
field_overrides: HashMap::new(),
};
rule.field_overrides.insert("rate".to_string(), RealitySource::Real);
config = config.add_entity_rule("currency".to_string(), rule);
assert_eq!(config.get_source_for_path("currency"), Some(RealitySource::Real));
assert_eq!(config.get_source_for_path("currency.rate"), Some(RealitySource::Real));
}
}