mockforge_core/reality_continuum/
blender.rs1use crate::reality_continuum::MergeStrategy;
8use serde_json::Value;
9use std::collections::HashMap;
10
11#[derive(Debug, Clone)]
13pub struct ResponseBlender {
14 strategy: MergeStrategy,
16}
17
18impl ResponseBlender {
19 pub fn new(strategy: MergeStrategy) -> Self {
21 Self { strategy }
22 }
23
24 pub fn default() -> Self {
26 Self {
27 strategy: MergeStrategy::FieldLevel,
28 }
29 }
30
31 pub fn blend_responses(&self, mock: &Value, real: &Value, ratio: f64) -> Value {
41 let ratio = ratio.clamp(0.0, 1.0);
42
43 if ratio == 0.0 {
45 return mock.clone();
46 }
47
48 if ratio == 1.0 {
50 return real.clone();
51 }
52
53 match self.strategy {
55 MergeStrategy::FieldLevel => self.blend_field_level(mock, real, ratio),
56 MergeStrategy::Weighted => self.blend_weighted(mock, real, ratio),
57 MergeStrategy::BodyBlend => self.blend_body(mock, real, ratio),
58 }
59 }
60
61 fn blend_field_level(&self, mock: &Value, real: &Value, ratio: f64) -> Value {
65 match (mock, real) {
66 (Value::Object(mock_obj), Value::Object(real_obj)) => {
68 let mut result = serde_json::Map::new();
69
70 let mut all_keys = std::collections::HashSet::new();
72 for key in mock_obj.keys() {
73 all_keys.insert(key.clone());
74 }
75 for key in real_obj.keys() {
76 all_keys.insert(key.clone());
77 }
78
79 for key in all_keys {
81 let mock_val = mock_obj.get(&key);
82 let real_val = real_obj.get(&key);
83
84 match (mock_val, real_val) {
85 (Some(m), Some(r)) => {
86 result.insert(key, self.blend_field_level(m, r, ratio));
88 }
89 (Some(m), None) => {
90 if ratio < 0.5 {
92 result.insert(key, m.clone());
93 }
94 }
95 (None, Some(r)) => {
96 if ratio >= 0.5 {
98 result.insert(key, r.clone());
99 }
100 }
101 (None, None) => {
102 }
104 }
105 }
106
107 Value::Object(result)
108 }
109 (Value::Array(mock_arr), Value::Array(real_arr)) => {
111 let mut result = Vec::new();
112
113 let total_len = mock_arr.len().max(real_arr.len());
115 let mock_count = ((1.0 - ratio) * total_len as f64).round() as usize;
116 let real_count = (ratio * total_len as f64).round() as usize;
117
118 for (i, item) in mock_arr.iter().enumerate() {
120 if i < mock_count {
121 result.push(item.clone());
122 }
123 }
124
125 for (i, item) in real_arr.iter().enumerate() {
127 if i < real_count {
128 result.push(item.clone());
129 }
130 }
131
132 if mock_arr.len() != real_arr.len() {
134 let min_len = mock_arr.len().min(real_arr.len());
135 for i in min_len..total_len {
136 if i < mock_arr.len() && i < real_arr.len() {
137 result.push(self.blend_field_level(&mock_arr[i], &real_arr[i], ratio));
139 } else if i < mock_arr.len() {
140 result.push(mock_arr[i].clone());
141 } else if i < real_arr.len() {
142 result.push(real_arr[i].clone());
143 }
144 }
145 }
146
147 Value::Array(result)
148 }
149 (Value::Number(mock_num), Value::Number(real_num)) => {
151 if let (Some(mock_f64), Some(real_f64)) = (mock_num.as_f64(), real_num.as_f64()) {
152 let blended = mock_f64 * (1.0 - ratio) + real_f64 * ratio;
153 Value::Number(serde_json::Number::from_f64(blended).unwrap_or(mock_num.clone()))
154 } else {
155 if ratio < 0.5 {
157 Value::Number(mock_num.clone())
158 } else {
159 Value::Number(real_num.clone())
160 }
161 }
162 }
163 (Value::String(mock_str), Value::String(real_str)) => {
165 if ratio < 0.5 {
166 Value::String(mock_str.clone())
167 } else {
168 Value::String(real_str.clone())
169 }
170 }
171 (Value::Bool(mock_bool), Value::Bool(real_bool)) => {
173 if ratio < 0.5 {
174 Value::Bool(*mock_bool)
175 } else {
176 Value::Bool(*real_bool)
177 }
178 }
179 _ => {
181 if ratio >= 0.5 {
182 real.clone()
183 } else {
184 mock.clone()
185 }
186 }
187 }
188 }
189
190 fn blend_weighted(&self, mock: &Value, real: &Value, ratio: f64) -> Value {
195 if ratio >= 0.5 {
198 real.clone()
199 } else {
200 mock.clone()
201 }
202 }
203
204 fn blend_body(&self, mock: &Value, real: &Value, ratio: f64) -> Value {
208 match (mock, real) {
210 (Value::Object(mock_obj), Value::Object(real_obj)) => {
211 let mut result = serde_json::Map::new();
212
213 let mut all_keys = std::collections::HashSet::new();
215 for key in mock_obj.keys() {
216 all_keys.insert(key.clone());
217 }
218 for key in real_obj.keys() {
219 all_keys.insert(key.clone());
220 }
221
222 for key in all_keys {
224 let mock_val = mock_obj.get(&key);
225 let real_val = real_obj.get(&key);
226
227 match (mock_val, real_val) {
228 (Some(m), Some(r)) => {
229 result.insert(key, self.blend_body(m, r, ratio));
230 }
231 (Some(m), None) => {
232 result.insert(key, m.clone());
233 }
234 (None, Some(r)) => {
235 result.insert(key, r.clone());
236 }
237 (None, None) => {}
238 }
239 }
240
241 Value::Object(result)
242 }
243 (Value::Array(mock_arr), Value::Array(real_arr)) => {
244 let mut result = Vec::new();
246 let max_len = mock_arr.len().max(real_arr.len());
247
248 for i in 0..max_len {
249 if i < mock_arr.len() && i < real_arr.len() {
250 result.push(self.blend_body(&mock_arr[i], &real_arr[i], ratio));
252 } else if i < mock_arr.len() {
253 result.push(mock_arr[i].clone());
254 } else if i < real_arr.len() {
255 result.push(real_arr[i].clone());
256 }
257 }
258
259 Value::Array(result)
260 }
261 (Value::Number(mock_num), Value::Number(real_num)) => {
262 if let (Some(mock_f64), Some(real_f64)) = (mock_num.as_f64(), real_num.as_f64()) {
263 let blended = mock_f64 * (1.0 - ratio) + real_f64 * ratio;
264 Value::Number(serde_json::Number::from_f64(blended).unwrap_or(mock_num.clone()))
265 } else {
266 if ratio < 0.5 {
267 Value::Number(mock_num.clone())
268 } else {
269 Value::Number(real_num.clone())
270 }
271 }
272 }
273 _ => {
274 if ratio >= 0.5 {
275 real.clone()
276 } else {
277 mock.clone()
278 }
279 }
280 }
281 }
282
283 pub fn blend_status_code(&self, mock_status: u16, real_status: u16, ratio: f64) -> u16 {
288 if ratio >= 0.5 {
289 real_status
290 } else {
291 mock_status
292 }
293 }
294
295 pub fn blend_headers(
299 &self,
300 mock_headers: &HashMap<String, String>,
301 real_headers: &HashMap<String, String>,
302 ratio: f64,
303 ) -> HashMap<String, String> {
304 let mut result = HashMap::new();
305
306 let mut all_keys = std::collections::HashSet::new();
308 for key in mock_headers.keys() {
309 all_keys.insert(key.clone());
310 }
311 for key in real_headers.keys() {
312 all_keys.insert(key.clone());
313 }
314
315 for key in all_keys {
317 let mock_val = mock_headers.get(&key);
318 let real_val = real_headers.get(&key);
319
320 match (mock_val, real_val) {
321 (Some(m), Some(r)) => {
322 if ratio >= 0.5 {
324 result.insert(key, r.clone());
325 } else {
326 result.insert(key, m.clone());
327 }
328 }
329 (Some(m), None) => {
330 if ratio < 0.5 {
332 result.insert(key, m.clone());
333 }
334 }
335 (None, Some(r)) => {
336 if ratio >= 0.5 {
338 result.insert(key, r.clone());
339 }
340 }
341 (None, None) => {}
342 }
343 }
344
345 result
346 }
347}
348
349impl Default for ResponseBlender {
350 fn default() -> Self {
351 Self::new(MergeStrategy::FieldLevel)
352 }
353}
354
355#[cfg(test)]
356mod tests {
357 use super::*;
358 use serde_json::json;
359
360 #[test]
361 fn test_blend_objects() {
362 let blender = ResponseBlender::default();
363 let mock = json!({
364 "id": 1,
365 "name": "Mock User",
366 "email": "mock@example.com"
367 });
368 let real = json!({
369 "id": 2,
370 "name": "Real User",
371 "status": "active"
372 });
373
374 let blended = blender.blend_responses(&mock, &real, 0.5);
375 assert!(blended.is_object());
376 }
377
378 #[test]
379 fn test_blend_arrays() {
380 let blender = ResponseBlender::default();
381 let mock = json!([1, 2, 3]);
382 let real = json!([4, 5, 6]);
383
384 let blended = blender.blend_responses(&mock, &real, 0.5);
385 assert!(blended.is_array());
386 }
387
388 #[test]
389 fn test_blend_numbers() {
390 let blender = ResponseBlender::default();
391 let mock = json!(10.0);
392 let real = json!(20.0);
393
394 let blended = blender.blend_responses(&mock, &real, 0.5);
395 if let Value::Number(n) = blended {
396 if let Some(f) = n.as_f64() {
397 assert!((f - 15.0).abs() < 0.1); }
399 }
400 }
401
402 #[test]
403 fn test_blend_status_code() {
404 let blender = ResponseBlender::default();
405 assert_eq!(blender.blend_status_code(200, 404, 0.3), 200); assert_eq!(blender.blend_status_code(200, 404, 0.7), 404); }
408
409 #[test]
410 fn test_blend_headers() {
411 let blender = ResponseBlender::default();
412 let mut mock_headers = HashMap::new();
413 mock_headers.insert("X-Mock".to_string(), "true".to_string());
414 mock_headers.insert("Content-Type".to_string(), "application/json".to_string());
415
416 let mut real_headers = HashMap::new();
417 real_headers.insert("X-Real".to_string(), "true".to_string());
418 real_headers.insert("Content-Type".to_string(), "application/xml".to_string());
419
420 let blended = blender.blend_headers(&mock_headers, &real_headers, 0.7);
421 assert_eq!(blended.get("Content-Type"), Some(&"application/xml".to_string()));
422 assert_eq!(blended.get("X-Real"), Some(&"true".to_string()));
423 }
424
425 #[test]
426 fn test_blend_nested_objects() {
427 let blender = ResponseBlender::default();
428 let mock = json!({
429 "user": {
430 "id": 1,
431 "name": "Mock User",
432 "email": "mock@example.com"
433 },
434 "metadata": {
435 "source": "mock"
436 }
437 });
438 let real = json!({
439 "user": {
440 "id": 2,
441 "name": "Real User",
442 "status": "active"
443 },
444 "metadata": {
445 "source": "real",
446 "timestamp": "2025-01-01T00:00:00Z"
447 }
448 });
449
450 let blended = blender.blend_responses(&mock, &real, 0.5);
451 assert!(blended.is_object());
452 assert!(blended.get("user").is_some());
453 assert!(blended.get("metadata").is_some());
454 }
455
456 #[test]
457 fn test_blend_ratio_boundaries() {
458 let blender = ResponseBlender::default();
459 let mock = json!({"value": "mock"});
460 let real = json!({"value": "real"});
461
462 let result_0 = blender.blend_responses(&mock, &real, 0.0);
464 assert_eq!(result_0, mock);
465
466 let result_1 = blender.blend_responses(&mock, &real, 1.0);
468 assert_eq!(result_1, real);
469 }
470
471 #[test]
472 fn test_blend_mixed_types() {
473 let blender = ResponseBlender::default();
474 let mock = json!({
475 "string": "mock",
476 "number": 10,
477 "boolean": true,
478 "array": [1, 2, 3]
479 });
480 let real = json!({
481 "string": "real",
482 "number": 20,
483 "boolean": false,
484 "array": [4, 5, 6]
485 });
486
487 let blended = blender.blend_responses(&mock, &real, 0.5);
488 assert!(blended.is_object());
489 assert!(blended.get("string").is_some());
490 assert!(blended.get("number").is_some());
491 assert!(blended.get("boolean").is_some());
492 assert!(blended.get("array").is_some());
493 }
494}