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) => map.len() + map.values().map(Self::count_fields).sum::<usize>(),
77 Value::Array(arr) => arr.iter().map(Self::count_fields).sum(),
78 _ => 1,
79 }
80 }
81}
82
83pub struct SampleComparator;
85
86impl SampleComparator {
87 pub fn compare(&self, mock_samples: &[Value], real_samples: &[Value]) -> f64 {
96 if mock_samples.is_empty() || real_samples.is_empty() {
97 return 0.0;
98 }
99
100 let structure_score = self.compare_structures(mock_samples, real_samples);
102
103 let distribution_score = self.compare_distributions(mock_samples, real_samples);
105
106 (structure_score * 0.6 + distribution_score * 0.4).max(0.0).min(1.0)
108 }
109
110 fn compare_structures(&self, mock_samples: &[Value], real_samples: &[Value]) -> f64 {
112 let mock_structure = Self::extract_structure(mock_samples.first().unwrap());
114 let real_structure = Self::extract_structure(real_samples.first().unwrap());
115
116 let mock_fields: std::collections::HashSet<String> =
118 mock_structure.keys().cloned().collect();
119 let real_fields: std::collections::HashSet<String> =
120 real_structure.keys().cloned().collect();
121
122 let intersection = mock_fields.intersection(&real_fields).count();
123 let union = mock_fields.union(&real_fields).count();
124
125 if union == 0 {
126 return 1.0;
127 }
128
129 intersection as f64 / union as f64
130 }
131
132 fn extract_structure(value: &Value) -> HashMap<String, String> {
134 let mut structure = HashMap::new();
135 Self::extract_structure_recursive(value, "", &mut structure);
136 structure
137 }
138
139 fn extract_structure_recursive(
141 value: &Value,
142 prefix: &str,
143 structure: &mut HashMap<String, String>,
144 ) {
145 match value {
146 Value::Object(map) => {
147 for (key, val) in map {
148 let path = if prefix.is_empty() {
149 key.clone()
150 } else {
151 format!("{}.{}", prefix, key)
152 };
153 structure.insert(path.clone(), Self::type_of(val));
154 Self::extract_structure_recursive(val, &path, structure);
155 }
156 }
157 Value::Array(arr) => {
158 if let Some(first) = arr.first() {
159 Self::extract_structure_recursive(first, prefix, structure);
160 }
161 }
162 _ => {
163 if !prefix.is_empty() {
164 structure.insert(prefix.to_string(), Self::type_of(value));
165 }
166 }
167 }
168 }
169
170 fn type_of(value: &Value) -> String {
172 match value {
173 Value::Null => "null".to_string(),
174 Value::Bool(_) => "bool".to_string(),
175 Value::Number(n) => {
176 if n.is_i64() {
177 "integer".to_string()
178 } else {
179 "number".to_string()
180 }
181 }
182 Value::String(_) => "string".to_string(),
183 Value::Array(_) => "array".to_string(),
184 Value::Object(_) => "object".to_string(),
185 }
186 }
187
188 fn compare_distributions(&self, mock_samples: &[Value], real_samples: &[Value]) -> f64 {
190 let mock_types: std::collections::HashSet<String> =
194 mock_samples.iter().map(Self::type_of).collect();
195 let real_types: std::collections::HashSet<String> =
196 real_samples.iter().map(Self::type_of).collect();
197
198 let intersection = mock_types.intersection(&real_types).count();
199 let union = mock_types.union(&real_types).count();
200
201 if union == 0 {
202 return 1.0;
203 }
204
205 intersection as f64 / union as f64
206 }
207}
208
209pub struct FidelityCalculator {
211 schema_comparator: SchemaComparator,
212 sample_comparator: SampleComparator,
213}
214
215impl FidelityCalculator {
216 pub fn new() -> Self {
218 Self {
219 schema_comparator: SchemaComparator,
220 sample_comparator: SampleComparator,
221 }
222 }
223
224 pub fn calculate(
239 &self,
240 mock_schema: &Value,
241 real_schema: &Value,
242 mock_samples: &[Value],
243 real_samples: &[Value],
244 mock_response_times: Option<&[u64]>,
245 real_response_times: Option<&[u64]>,
246 mock_error_patterns: Option<&HashMap<String, usize>>,
247 real_error_patterns: Option<&HashMap<String, usize>>,
248 ) -> FidelityScore {
249 let schema_similarity = self.schema_comparator.compare(mock_schema, real_schema);
251
252 let sample_similarity = self.sample_comparator.compare(mock_samples, real_samples);
254
255 let response_time_similarity = self.compare_response_times(
257 mock_response_times.unwrap_or(&[]),
258 real_response_times.unwrap_or(&[]),
259 );
260
261 let error_pattern_similarity =
263 self.compare_error_patterns(mock_error_patterns, real_error_patterns);
264
265 let overall = (schema_similarity * 0.4
267 + sample_similarity * 0.4
268 + response_time_similarity * 0.1
269 + error_pattern_similarity * 0.1)
270 .max(0.0)
271 .min(1.0);
272
273 FidelityScore {
274 overall,
275 schema_similarity,
276 sample_similarity,
277 response_time_similarity,
278 error_pattern_similarity,
279 computed_at: Utc::now(),
280 metadata: HashMap::new(),
281 }
282 }
283
284 fn compare_response_times(&self, mock_times: &[u64], real_times: &[u64]) -> f64 {
286 if mock_times.is_empty() || real_times.is_empty() {
287 return 0.5; }
289
290 let mock_avg = mock_times.iter().sum::<u64>() as f64 / mock_times.len() as f64;
291 let real_avg = real_times.iter().sum::<u64>() as f64 / real_times.len() as f64;
292
293 if real_avg == 0.0 {
294 return if mock_avg == 0.0 { 1.0 } else { 0.0 };
295 }
296
297 let ratio = mock_avg / real_avg;
299 (1.0 - (ratio - 1.0).abs()).max(0.0).min(1.0)
301 }
302
303 fn compare_error_patterns(
305 &self,
306 mock_patterns: Option<&HashMap<String, usize>>,
307 real_patterns: Option<&HashMap<String, usize>>,
308 ) -> f64 {
309 match (mock_patterns, real_patterns) {
310 (Some(mock), Some(real)) => {
311 if mock.is_empty() && real.is_empty() {
312 return 1.0;
313 }
314
315 let mock_keys: std::collections::HashSet<&String> = mock.keys().collect();
316 let real_keys: std::collections::HashSet<&String> = real.keys().collect();
317
318 let intersection = mock_keys.intersection(&real_keys).count();
319 let union = mock_keys.union(&real_keys).count();
320
321 if union == 0 {
322 return 1.0;
323 }
324
325 intersection as f64 / union as f64
326 }
327 _ => 0.5, }
329 }
330}
331
332impl Default for FidelityCalculator {
333 fn default() -> Self {
334 Self::new()
335 }
336}