use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FidelityScore {
pub overall: f64,
pub schema_similarity: f64,
pub sample_similarity: f64,
pub response_time_similarity: f64,
pub error_pattern_similarity: f64,
pub computed_at: DateTime<Utc>,
#[serde(default)]
pub metadata: HashMap<String, Value>,
}
pub struct SchemaComparator;
impl SchemaComparator {
pub fn compare(&self, mock_schema: &Value, real_schema: &Value) -> f64 {
let errors = crate::schema_diff::diff(mock_schema, real_schema);
if errors.is_empty() {
return 1.0;
}
let mock_fields = Self::count_fields(mock_schema);
let real_fields = Self::count_fields(real_schema);
let total_fields = mock_fields.max(real_fields);
if total_fields == 0 {
return 1.0;
}
let error_penalty = errors.len() as f64 / total_fields as f64;
let coverage_score = if mock_fields > 0 && real_fields > 0 {
let common_fields = total_fields - errors.len();
common_fields as f64 / total_fields as f64
} else {
0.0
};
(coverage_score * 0.7 + (1.0 - error_penalty.min(1.0)) * 0.3).clamp(0.0, 1.0)
}
fn count_fields(schema: &Value) -> usize {
match schema {
Value::Object(map) => map.len() + map.values().map(Self::count_fields).sum::<usize>(),
Value::Array(arr) => arr.iter().map(Self::count_fields).sum(),
_ => 1,
}
}
}
pub struct SampleComparator;
impl SampleComparator {
pub fn compare(&self, mock_samples: &[Value], real_samples: &[Value]) -> f64 {
if mock_samples.is_empty() || real_samples.is_empty() {
return 0.0;
}
let structure_score = self.compare_structures(mock_samples, real_samples);
let distribution_score = self.compare_distributions(mock_samples, real_samples);
(structure_score * 0.6 + distribution_score * 0.4).clamp(0.0, 1.0)
}
fn compare_structures(&self, mock_samples: &[Value], real_samples: &[Value]) -> f64 {
let mock_structure = Self::extract_structure(mock_samples.first().unwrap());
let real_structure = Self::extract_structure(real_samples.first().unwrap());
let mock_fields: std::collections::HashSet<String> =
mock_structure.keys().cloned().collect();
let real_fields: std::collections::HashSet<String> =
real_structure.keys().cloned().collect();
let intersection = mock_fields.intersection(&real_fields).count();
let union = mock_fields.union(&real_fields).count();
if union == 0 {
return 1.0;
}
intersection as f64 / union as f64
}
fn extract_structure(value: &Value) -> HashMap<String, String> {
let mut structure = HashMap::new();
Self::extract_structure_recursive(value, "", &mut structure);
structure
}
fn extract_structure_recursive(
value: &Value,
prefix: &str,
structure: &mut HashMap<String, String>,
) {
match value {
Value::Object(map) => {
for (key, val) in map {
let path = if prefix.is_empty() {
key.clone()
} else {
format!("{}.{}", prefix, key)
};
structure.insert(path.clone(), Self::type_of(val));
Self::extract_structure_recursive(val, &path, structure);
}
}
Value::Array(arr) => {
if let Some(first) = arr.first() {
Self::extract_structure_recursive(first, prefix, structure);
}
}
_ => {
if !prefix.is_empty() {
structure.insert(prefix.to_string(), Self::type_of(value));
}
}
}
}
fn type_of(value: &Value) -> String {
match value {
Value::Null => "null".to_string(),
Value::Bool(_) => "bool".to_string(),
Value::Number(n) => {
if n.is_i64() {
"integer".to_string()
} else {
"number".to_string()
}
}
Value::String(_) => "string".to_string(),
Value::Array(_) => "array".to_string(),
Value::Object(_) => "object".to_string(),
}
}
fn compare_distributions(&self, mock_samples: &[Value], real_samples: &[Value]) -> f64 {
let mock_types: std::collections::HashSet<String> =
mock_samples.iter().map(Self::type_of).collect();
let real_types: std::collections::HashSet<String> =
real_samples.iter().map(Self::type_of).collect();
let intersection = mock_types.intersection(&real_types).count();
let union = mock_types.union(&real_types).count();
if union == 0 {
return 1.0;
}
intersection as f64 / union as f64
}
}
pub struct FidelityCalculator {
schema_comparator: SchemaComparator,
sample_comparator: SampleComparator,
}
impl FidelityCalculator {
pub fn new() -> Self {
Self {
schema_comparator: SchemaComparator,
sample_comparator: SampleComparator,
}
}
#[allow(clippy::too_many_arguments)]
pub fn calculate(
&self,
mock_schema: &Value,
real_schema: &Value,
mock_samples: &[Value],
real_samples: &[Value],
mock_response_times: Option<&[u64]>,
real_response_times: Option<&[u64]>,
mock_error_patterns: Option<&HashMap<String, usize>>,
real_error_patterns: Option<&HashMap<String, usize>>,
) -> FidelityScore {
let schema_similarity = self.schema_comparator.compare(mock_schema, real_schema);
let sample_similarity = self.sample_comparator.compare(mock_samples, real_samples);
let response_time_similarity = self.compare_response_times(
mock_response_times.unwrap_or(&[]),
real_response_times.unwrap_or(&[]),
);
let error_pattern_similarity =
self.compare_error_patterns(mock_error_patterns, real_error_patterns);
let overall = (schema_similarity * 0.4
+ sample_similarity * 0.4
+ response_time_similarity * 0.1
+ error_pattern_similarity * 0.1)
.clamp(0.0, 1.0);
FidelityScore {
overall,
schema_similarity,
sample_similarity,
response_time_similarity,
error_pattern_similarity,
computed_at: Utc::now(),
metadata: HashMap::new(),
}
}
fn compare_response_times(&self, mock_times: &[u64], real_times: &[u64]) -> f64 {
if mock_times.is_empty() || real_times.is_empty() {
return 0.5; }
let mock_avg = mock_times.iter().sum::<u64>() as f64 / mock_times.len() as f64;
let real_avg = real_times.iter().sum::<u64>() as f64 / real_times.len() as f64;
if real_avg == 0.0 {
return if mock_avg == 0.0 { 1.0 } else { 0.0 };
}
let ratio = mock_avg / real_avg;
(1.0 - (ratio - 1.0).abs()).clamp(0.0, 1.0)
}
fn compare_error_patterns(
&self,
mock_patterns: Option<&HashMap<String, usize>>,
real_patterns: Option<&HashMap<String, usize>>,
) -> f64 {
match (mock_patterns, real_patterns) {
(Some(mock), Some(real)) => {
if mock.is_empty() && real.is_empty() {
return 1.0;
}
let mock_keys: std::collections::HashSet<&String> = mock.keys().collect();
let real_keys: std::collections::HashSet<&String> = real.keys().collect();
let intersection = mock_keys.intersection(&real_keys).count();
let union = mock_keys.union(&real_keys).count();
if union == 0 {
return 1.0;
}
intersection as f64 / union as f64
}
_ => 0.5, }
}
}
impl Default for FidelityCalculator {
fn default() -> Self {
Self::new()
}
}