mockforge_core/
fidelity.rs1use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9use std::collections::HashMap;
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct FidelityScore {
14 pub overall: f64,
16 pub schema_similarity: f64,
18 pub sample_similarity: f64,
20 pub response_time_similarity: f64,
22 pub error_pattern_similarity: f64,
24 pub computed_at: DateTime<Utc>,
26 #[serde(default)]
28 pub metadata: HashMap<String, Value>,
29}
30
31pub struct SchemaComparator;
33
34impl SchemaComparator {
35 pub fn compare(&self, mock_schema: &Value, real_schema: &Value) -> f64 {
44 let errors = crate::schema_diff::diff(mock_schema, real_schema);
46
47 if errors.is_empty() {
48 return 1.0;
49 }
50
51 let mock_fields = Self::count_fields(mock_schema);
53 let real_fields = Self::count_fields(real_schema);
54 let total_fields = mock_fields.max(real_fields);
55
56 if total_fields == 0 {
57 return 1.0;
58 }
59
60 let error_penalty = errors.len() as f64 / total_fields as f64;
62 let coverage_score = if mock_fields > 0 && real_fields > 0 {
63 let common_fields = total_fields - errors.len();
64 common_fields as f64 / total_fields as f64
65 } else {
66 0.0
67 };
68
69 (coverage_score * 0.7 + (1.0 - error_penalty.min(1.0)) * 0.3).max(0.0).min(1.0)
71 }
72
73 fn count_fields(schema: &Value) -> usize {
75 match schema {
76 Value::Object(map) => {
77 map.len() + map.values().map(|v| Self::count_fields(v)).sum::<usize>()
78 }
79 Value::Array(arr) => arr.iter().map(|v| Self::count_fields(v)).sum(),
80 _ => 1,
81 }
82 }
83}
84
85pub struct SampleComparator;
87
88impl SampleComparator {
89 pub fn compare(&self, mock_samples: &[Value], real_samples: &[Value]) -> f64 {
98 if mock_samples.is_empty() || real_samples.is_empty() {
99 return 0.0;
100 }
101
102 let structure_score = self.compare_structures(mock_samples, real_samples);
104
105 let distribution_score = self.compare_distributions(mock_samples, real_samples);
107
108 (structure_score * 0.6 + distribution_score * 0.4).max(0.0).min(1.0)
110 }
111
112 fn compare_structures(&self, mock_samples: &[Value], real_samples: &[Value]) -> f64 {
114 let mock_structure = Self::extract_structure(mock_samples.first().unwrap());
116 let real_structure = Self::extract_structure(real_samples.first().unwrap());
117
118 let mock_fields: std::collections::HashSet<String> =
120 mock_structure.keys().cloned().collect();
121 let real_fields: std::collections::HashSet<String> =
122 real_structure.keys().cloned().collect();
123
124 let intersection = mock_fields.intersection(&real_fields).count();
125 let union = mock_fields.union(&real_fields).count();
126
127 if union == 0 {
128 return 1.0;
129 }
130
131 intersection as f64 / union as f64
132 }
133
134 fn extract_structure(value: &Value) -> HashMap<String, String> {
136 let mut structure = HashMap::new();
137 Self::extract_structure_recursive(value, "", &mut structure);
138 structure
139 }
140
141 fn extract_structure_recursive(
143 value: &Value,
144 prefix: &str,
145 structure: &mut HashMap<String, String>,
146 ) {
147 match value {
148 Value::Object(map) => {
149 for (key, val) in map {
150 let path = if prefix.is_empty() {
151 key.clone()
152 } else {
153 format!("{}.{}", prefix, key)
154 };
155 structure.insert(path.clone(), Self::type_of(val));
156 Self::extract_structure_recursive(val, &path, structure);
157 }
158 }
159 Value::Array(arr) => {
160 if let Some(first) = arr.first() {
161 Self::extract_structure_recursive(first, prefix, structure);
162 }
163 }
164 _ => {
165 if !prefix.is_empty() {
166 structure.insert(prefix.to_string(), Self::type_of(value));
167 }
168 }
169 }
170 }
171
172 fn type_of(value: &Value) -> String {
174 match value {
175 Value::Null => "null".to_string(),
176 Value::Bool(_) => "bool".to_string(),
177 Value::Number(n) => {
178 if n.is_i64() {
179 "integer".to_string()
180 } else {
181 "number".to_string()
182 }
183 }
184 Value::String(_) => "string".to_string(),
185 Value::Array(_) => "array".to_string(),
186 Value::Object(_) => "object".to_string(),
187 }
188 }
189
190 fn compare_distributions(&self, mock_samples: &[Value], real_samples: &[Value]) -> f64 {
192 let mock_types: std::collections::HashSet<String> =
196 mock_samples.iter().map(|v| Self::type_of(v)).collect();
197 let real_types: std::collections::HashSet<String> =
198 real_samples.iter().map(|v| Self::type_of(v)).collect();
199
200 let intersection = mock_types.intersection(&real_types).count();
201 let union = mock_types.union(&real_types).count();
202
203 if union == 0 {
204 return 1.0;
205 }
206
207 intersection as f64 / union as f64
208 }
209}
210
211pub struct FidelityCalculator {
213 schema_comparator: SchemaComparator,
214 sample_comparator: SampleComparator,
215}
216
217impl FidelityCalculator {
218 pub fn new() -> Self {
220 Self {
221 schema_comparator: SchemaComparator,
222 sample_comparator: SampleComparator,
223 }
224 }
225
226 pub fn calculate(
241 &self,
242 mock_schema: &Value,
243 real_schema: &Value,
244 mock_samples: &[Value],
245 real_samples: &[Value],
246 mock_response_times: Option<&[u64]>,
247 real_response_times: Option<&[u64]>,
248 mock_error_patterns: Option<&HashMap<String, usize>>,
249 real_error_patterns: Option<&HashMap<String, usize>>,
250 ) -> FidelityScore {
251 let schema_similarity = self.schema_comparator.compare(mock_schema, real_schema);
253
254 let sample_similarity = self.sample_comparator.compare(mock_samples, real_samples);
256
257 let response_time_similarity = self.compare_response_times(
259 mock_response_times.unwrap_or(&[]),
260 real_response_times.unwrap_or(&[]),
261 );
262
263 let error_pattern_similarity =
265 self.compare_error_patterns(mock_error_patterns, real_error_patterns);
266
267 let overall = (schema_similarity * 0.4
269 + sample_similarity * 0.4
270 + response_time_similarity * 0.1
271 + error_pattern_similarity * 0.1)
272 .max(0.0)
273 .min(1.0);
274
275 FidelityScore {
276 overall,
277 schema_similarity,
278 sample_similarity,
279 response_time_similarity,
280 error_pattern_similarity,
281 computed_at: Utc::now(),
282 metadata: HashMap::new(),
283 }
284 }
285
286 fn compare_response_times(&self, mock_times: &[u64], real_times: &[u64]) -> f64 {
288 if mock_times.is_empty() || real_times.is_empty() {
289 return 0.5; }
291
292 let mock_avg = mock_times.iter().sum::<u64>() as f64 / mock_times.len() as f64;
293 let real_avg = real_times.iter().sum::<u64>() as f64 / real_times.len() as f64;
294
295 if real_avg == 0.0 {
296 return if mock_avg == 0.0 { 1.0 } else { 0.0 };
297 }
298
299 let ratio = mock_avg / real_avg;
301 (1.0 - (ratio - 1.0).abs()).max(0.0).min(1.0)
303 }
304
305 fn compare_error_patterns(
307 &self,
308 mock_patterns: Option<&HashMap<String, usize>>,
309 real_patterns: Option<&HashMap<String, usize>>,
310 ) -> f64 {
311 match (mock_patterns, real_patterns) {
312 (Some(mock), Some(real)) => {
313 if mock.is_empty() && real.is_empty() {
314 return 1.0;
315 }
316
317 let mock_keys: std::collections::HashSet<&String> = mock.keys().collect();
318 let real_keys: std::collections::HashSet<&String> = real.keys().collect();
319
320 let intersection = mock_keys.intersection(&real_keys).count();
321 let union = mock_keys.union(&real_keys).count();
322
323 if union == 0 {
324 return 1.0;
325 }
326
327 intersection as f64 / union as f64
328 }
329 _ => 0.5, }
331 }
332}
333
334impl Default for FidelityCalculator {
335 fn default() -> Self {
336 Self::new()
337 }
338}