mockforge_core/reality_continuum/
blender.rs

1//! Response blending logic for Reality Continuum
2//!
3//! Implements intelligent merging of mock and real responses based on blend ratio.
4//! Supports deep merging of JSON objects, combining arrays, and weighted selection
5//! for primitive values.
6
7use crate::reality_continuum::MergeStrategy;
8use serde_json::Value;
9use std::collections::HashMap;
10
11/// Response blender for combining mock and real responses
12#[derive(Debug, Clone)]
13pub struct ResponseBlender {
14    /// Merge strategy to use
15    strategy: MergeStrategy,
16}
17
18impl ResponseBlender {
19    /// Create a new response blender with the specified strategy
20    pub fn new(strategy: MergeStrategy) -> Self {
21        Self { strategy }
22    }
23
24    /// Create a new response blender with default field-level strategy
25    pub fn default() -> Self {
26        Self {
27            strategy: MergeStrategy::FieldLevel,
28        }
29    }
30
31    /// Blend two JSON responses based on the blend ratio
32    ///
33    /// # Arguments
34    /// * `mock` - Mock response value
35    /// * `real` - Real response value
36    /// * `ratio` - Blend ratio (0.0 = 100% mock, 1.0 = 100% real)
37    ///
38    /// # Returns
39    /// Blended response value
40    pub fn blend_responses(&self, mock: &Value, real: &Value, ratio: f64) -> Value {
41        self.blend_responses_with_config(mock, real, ratio, None)
42    }
43
44    /// Blend two JSON responses with field-level configuration
45    ///
46    /// # Arguments
47    /// * `mock` - Mock response value
48    /// * `real` - Real response value
49    /// * `global_ratio` - Global blend ratio (0.0 = 100% mock, 1.0 = 100% real)
50    /// * `field_config` - Optional field-level reality configuration
51    ///
52    /// # Returns
53    /// Blended response value
54    pub fn blend_responses_with_config(
55        &self,
56        mock: &Value,
57        real: &Value,
58        global_ratio: f64,
59        field_config: Option<&crate::reality_continuum::FieldRealityConfig>,
60    ) -> Value {
61        let global_ratio = global_ratio.clamp(0.0, 1.0);
62
63        // If no field config, use global ratio
64        if field_config.is_none() {
65            // If ratio is 0.0, return mock entirely
66            if global_ratio == 0.0 {
67                return mock.clone();
68            }
69
70            // If ratio is 1.0, return real entirely
71            if global_ratio == 1.0 {
72                return real.clone();
73            }
74
75            // Apply the selected merge strategy
76            match self.strategy {
77                MergeStrategy::FieldLevel => self.blend_field_level(mock, real, global_ratio),
78                MergeStrategy::Weighted => self.blend_weighted(mock, real, global_ratio),
79                MergeStrategy::BodyBlend => self.blend_body(mock, real, global_ratio),
80            }
81        } else {
82            // Use field-level blending
83            self.blend_with_field_config(mock, real, global_ratio, field_config.unwrap())
84        }
85    }
86
87    /// Blend responses with field-level configuration
88    fn blend_with_field_config(
89        &self,
90        mock: &Value,
91        real: &Value,
92        global_ratio: f64,
93        field_config: &crate::reality_continuum::FieldRealityConfig,
94    ) -> Value {
95        match (mock, real) {
96            (Value::Object(mock_obj), Value::Object(real_obj)) => {
97                let mut result = serde_json::Map::new();
98
99                // Collect all keys from both objects
100                let mut all_keys = std::collections::HashSet::new();
101                for key in mock_obj.keys() {
102                    all_keys.insert(key.clone());
103                }
104                for key in real_obj.keys() {
105                    all_keys.insert(key.clone());
106                }
107
108                // Blend each key with field-specific ratio
109                for key in all_keys {
110                    let json_path = key.clone();
111                    let mock_val = mock_obj.get(&key);
112                    let real_val = real_obj.get(&key);
113
114                    // Get field-specific blend ratio
115                    let field_ratio =
116                        field_config.get_blend_ratio_for_path(&json_path).unwrap_or(global_ratio);
117
118                    match (mock_val, real_val) {
119                        (Some(m), Some(r)) => {
120                            // Both exist - recursively blend with field ratio
121                            result.insert(
122                                key,
123                                self.blend_with_field_config(m, r, field_ratio, field_config),
124                            );
125                        }
126                        (Some(m), None) => {
127                            // Only in mock - include if field ratio < 0.5
128                            if field_ratio < 0.5 {
129                                result.insert(key, m.clone());
130                            }
131                        }
132                        (None, Some(r)) => {
133                            // Only in real - include if field ratio >= 0.5
134                            if field_ratio >= 0.5 {
135                                result.insert(key, r.clone());
136                            }
137                        }
138                        (None, None) => {
139                            // Neither (shouldn't happen)
140                        }
141                    }
142                }
143
144                Value::Object(result)
145            }
146            (Value::Array(mock_arr), Value::Array(real_arr)) => {
147                // For arrays, use global ratio (field-level doesn't apply well to arrays)
148                match self.strategy {
149                    MergeStrategy::FieldLevel => self.blend_field_level(mock, real, global_ratio),
150                    MergeStrategy::Weighted => self.blend_weighted(mock, real, global_ratio),
151                    MergeStrategy::BodyBlend => self.blend_body(mock, real, global_ratio),
152                }
153            }
154            _ => {
155                // For primitives, use global ratio
156                if global_ratio < 0.5 {
157                    mock.clone()
158                } else {
159                    real.clone()
160                }
161            }
162        }
163    }
164
165    /// Field-level intelligent merge
166    ///
167    /// Deep merges objects, combines arrays, and uses weighted selection for primitives.
168    fn blend_field_level(&self, mock: &Value, real: &Value, ratio: f64) -> Value {
169        match (mock, real) {
170            // Both are objects - deep merge
171            (Value::Object(mock_obj), Value::Object(real_obj)) => {
172                let mut result = serde_json::Map::new();
173
174                // Collect all keys from both objects
175                let mut all_keys = std::collections::HashSet::new();
176                for key in mock_obj.keys() {
177                    all_keys.insert(key.clone());
178                }
179                for key in real_obj.keys() {
180                    all_keys.insert(key.clone());
181                }
182
183                // Merge each key
184                for key in all_keys {
185                    let mock_val = mock_obj.get(&key);
186                    let real_val = real_obj.get(&key);
187
188                    match (mock_val, real_val) {
189                        (Some(m), Some(r)) => {
190                            // Both exist - recursively blend
191                            result.insert(key, self.blend_field_level(m, r, ratio));
192                        }
193                        (Some(m), None) => {
194                            // Only in mock - include with reduced weight
195                            if ratio < 0.5 {
196                                result.insert(key, m.clone());
197                            }
198                        }
199                        (None, Some(r)) => {
200                            // Only in real - include with increased weight
201                            if ratio >= 0.5 {
202                                result.insert(key, r.clone());
203                            }
204                        }
205                        (None, None) => {
206                            // Neither (shouldn't happen)
207                        }
208                    }
209                }
210
211                Value::Object(result)
212            }
213            // Both are arrays - combine based on ratio
214            (Value::Array(mock_arr), Value::Array(real_arr)) => {
215                let mut result = Vec::new();
216
217                // Calculate how many items from each array
218                let total_len = mock_arr.len().max(real_arr.len());
219                let mock_count = ((1.0 - ratio) * total_len as f64).round() as usize;
220                let real_count = (ratio * total_len as f64).round() as usize;
221
222                // Add items from mock array
223                for (i, item) in mock_arr.iter().enumerate() {
224                    if i < mock_count {
225                        result.push(item.clone());
226                    }
227                }
228
229                // Add items from real array
230                for (i, item) in real_arr.iter().enumerate() {
231                    if i < real_count {
232                        result.push(item.clone());
233                    }
234                }
235
236                // If arrays have different lengths, blend remaining items
237                if mock_arr.len() != real_arr.len() {
238                    let min_len = mock_arr.len().min(real_arr.len());
239                    for i in min_len..total_len {
240                        if i < mock_arr.len() && i < real_arr.len() {
241                            // Blend corresponding items
242                            result.push(self.blend_field_level(&mock_arr[i], &real_arr[i], ratio));
243                        } else if i < mock_arr.len() {
244                            result.push(mock_arr[i].clone());
245                        } else if i < real_arr.len() {
246                            result.push(real_arr[i].clone());
247                        }
248                    }
249                }
250
251                Value::Array(result)
252            }
253            // Both are numbers - weighted average
254            (Value::Number(mock_num), Value::Number(real_num)) => {
255                if let (Some(mock_f64), Some(real_f64)) = (mock_num.as_f64(), real_num.as_f64()) {
256                    let blended = mock_f64 * (1.0 - ratio) + real_f64 * ratio;
257                    Value::Number(serde_json::Number::from_f64(blended).unwrap_or(mock_num.clone()))
258                } else {
259                    // Fallback to weighted selection
260                    if ratio < 0.5 {
261                        Value::Number(mock_num.clone())
262                    } else {
263                        Value::Number(real_num.clone())
264                    }
265                }
266            }
267            // Both are strings - weighted selection
268            (Value::String(mock_str), Value::String(real_str)) => {
269                if ratio < 0.5 {
270                    Value::String(mock_str.clone())
271                } else {
272                    Value::String(real_str.clone())
273                }
274            }
275            // Both are booleans - weighted selection
276            (Value::Bool(mock_bool), Value::Bool(real_bool)) => {
277                if ratio < 0.5 {
278                    Value::Bool(*mock_bool)
279                } else {
280                    Value::Bool(*real_bool)
281                }
282            }
283            // Type mismatch - prefer real if ratio > 0.5, otherwise mock
284            _ => {
285                if ratio >= 0.5 {
286                    real.clone()
287                } else {
288                    mock.clone()
289                }
290            }
291        }
292    }
293
294    /// Weighted selection strategy
295    ///
296    /// Randomly selects between mock and real based on ratio (for testing/demo purposes).
297    /// In practice, this would use the ratio as a probability threshold.
298    fn blend_weighted(&self, mock: &Value, real: &Value, ratio: f64) -> Value {
299        // For deterministic behavior, use threshold-based selection
300        // In a real implementation, you might want to use actual randomness
301        if ratio >= 0.5 {
302            real.clone()
303        } else {
304            mock.clone()
305        }
306    }
307
308    /// Body blending strategy
309    ///
310    /// Merges arrays, averages numeric fields, and deep merges objects.
311    fn blend_body(&self, mock: &Value, real: &Value, ratio: f64) -> Value {
312        // Similar to field-level but with different array handling
313        match (mock, real) {
314            (Value::Object(mock_obj), Value::Object(real_obj)) => {
315                let mut result = serde_json::Map::new();
316
317                // Collect all keys
318                let mut all_keys = std::collections::HashSet::new();
319                for key in mock_obj.keys() {
320                    all_keys.insert(key.clone());
321                }
322                for key in real_obj.keys() {
323                    all_keys.insert(key.clone());
324                }
325
326                // Merge each key
327                for key in all_keys {
328                    let mock_val = mock_obj.get(&key);
329                    let real_val = real_obj.get(&key);
330
331                    match (mock_val, real_val) {
332                        (Some(m), Some(r)) => {
333                            result.insert(key, self.blend_body(m, r, ratio));
334                        }
335                        (Some(m), None) => {
336                            result.insert(key, m.clone());
337                        }
338                        (None, Some(r)) => {
339                            result.insert(key, r.clone());
340                        }
341                        (None, None) => {}
342                    }
343                }
344
345                Value::Object(result)
346            }
347            (Value::Array(mock_arr), Value::Array(real_arr)) => {
348                // Combine arrays, interleaving based on ratio
349                let mut result = Vec::new();
350                let max_len = mock_arr.len().max(real_arr.len());
351
352                for i in 0..max_len {
353                    if i < mock_arr.len() && i < real_arr.len() {
354                        // Blend corresponding items
355                        result.push(self.blend_body(&mock_arr[i], &real_arr[i], ratio));
356                    } else if i < mock_arr.len() {
357                        result.push(mock_arr[i].clone());
358                    } else if i < real_arr.len() {
359                        result.push(real_arr[i].clone());
360                    }
361                }
362
363                Value::Array(result)
364            }
365            (Value::Number(mock_num), Value::Number(real_num)) => {
366                if let (Some(mock_f64), Some(real_f64)) = (mock_num.as_f64(), real_num.as_f64()) {
367                    let blended = mock_f64 * (1.0 - ratio) + real_f64 * ratio;
368                    Value::Number(serde_json::Number::from_f64(blended).unwrap_or(mock_num.clone()))
369                } else {
370                    if ratio < 0.5 {
371                        Value::Number(mock_num.clone())
372                    } else {
373                        Value::Number(real_num.clone())
374                    }
375                }
376            }
377            _ => {
378                if ratio >= 0.5 {
379                    real.clone()
380                } else {
381                    mock.clone()
382                }
383            }
384        }
385    }
386
387    /// Blend HTTP status codes
388    ///
389    /// Returns the status code to use based on blend ratio.
390    /// Prefers real status code if ratio > 0.5, otherwise uses mock.
391    pub fn blend_status_code(&self, mock_status: u16, real_status: u16, ratio: f64) -> u16 {
392        if ratio >= 0.5 {
393            real_status
394        } else {
395            mock_status
396        }
397    }
398
399    /// Blend HTTP headers
400    ///
401    /// Merges headers from both responses, preferring real headers when ratio > 0.5.
402    pub fn blend_headers(
403        &self,
404        mock_headers: &HashMap<String, String>,
405        real_headers: &HashMap<String, String>,
406        ratio: f64,
407    ) -> HashMap<String, String> {
408        let mut result = HashMap::new();
409
410        // Collect all header keys
411        let mut all_keys = std::collections::HashSet::new();
412        for key in mock_headers.keys() {
413            all_keys.insert(key.clone());
414        }
415        for key in real_headers.keys() {
416            all_keys.insert(key.clone());
417        }
418
419        // Merge headers
420        for key in all_keys {
421            let mock_val = mock_headers.get(&key);
422            let real_val = real_headers.get(&key);
423
424            match (mock_val, real_val) {
425                (Some(m), Some(r)) => {
426                    // Both exist - prefer real if ratio > 0.5
427                    if ratio >= 0.5 {
428                        result.insert(key, r.clone());
429                    } else {
430                        result.insert(key, m.clone());
431                    }
432                }
433                (Some(m), None) => {
434                    // Only in mock - include if ratio < 0.5
435                    if ratio < 0.5 {
436                        result.insert(key, m.clone());
437                    }
438                }
439                (None, Some(r)) => {
440                    // Only in real - include if ratio >= 0.5
441                    if ratio >= 0.5 {
442                        result.insert(key, r.clone());
443                    }
444                }
445                (None, None) => {}
446            }
447        }
448
449        result
450    }
451}
452
453impl Default for ResponseBlender {
454    fn default() -> Self {
455        Self::new(MergeStrategy::FieldLevel)
456    }
457}
458
459#[cfg(test)]
460mod tests {
461    use super::*;
462    use serde_json::json;
463
464    #[test]
465    fn test_blend_objects() {
466        let blender = ResponseBlender::default();
467        let mock = json!({
468            "id": 1,
469            "name": "Mock User",
470            "email": "mock@example.com"
471        });
472        let real = json!({
473            "id": 2,
474            "name": "Real User",
475            "status": "active"
476        });
477
478        let blended = blender.blend_responses(&mock, &real, 0.5);
479        assert!(blended.is_object());
480    }
481
482    #[test]
483    fn test_blend_arrays() {
484        let blender = ResponseBlender::default();
485        let mock = json!([1, 2, 3]);
486        let real = json!([4, 5, 6]);
487
488        let blended = blender.blend_responses(&mock, &real, 0.5);
489        assert!(blended.is_array());
490    }
491
492    #[test]
493    fn test_blend_numbers() {
494        let blender = ResponseBlender::default();
495        let mock = json!(10.0);
496        let real = json!(20.0);
497
498        let blended = blender.blend_responses(&mock, &real, 0.5);
499        if let Value::Number(n) = blended {
500            if let Some(f) = n.as_f64() {
501                assert!((f - 15.0).abs() < 0.1); // Should be approximately 15.0
502            }
503        }
504    }
505
506    #[test]
507    fn test_blend_status_code() {
508        let blender = ResponseBlender::default();
509        assert_eq!(blender.blend_status_code(200, 404, 0.3), 200); // Prefer mock
510        assert_eq!(blender.blend_status_code(200, 404, 0.7), 404); // Prefer real
511    }
512
513    #[test]
514    fn test_blend_headers() {
515        let blender = ResponseBlender::default();
516        let mut mock_headers = HashMap::new();
517        mock_headers.insert("X-Mock".to_string(), "true".to_string());
518        mock_headers.insert("Content-Type".to_string(), "application/json".to_string());
519
520        let mut real_headers = HashMap::new();
521        real_headers.insert("X-Real".to_string(), "true".to_string());
522        real_headers.insert("Content-Type".to_string(), "application/xml".to_string());
523
524        let blended = blender.blend_headers(&mock_headers, &real_headers, 0.7);
525        assert_eq!(blended.get("Content-Type"), Some(&"application/xml".to_string()));
526        assert_eq!(blended.get("X-Real"), Some(&"true".to_string()));
527    }
528
529    #[test]
530    fn test_blend_nested_objects() {
531        let blender = ResponseBlender::default();
532        let mock = json!({
533            "user": {
534                "id": 1,
535                "name": "Mock User",
536                "email": "mock@example.com"
537            },
538            "metadata": {
539                "source": "mock"
540            }
541        });
542        let real = json!({
543            "user": {
544                "id": 2,
545                "name": "Real User",
546                "status": "active"
547            },
548            "metadata": {
549                "source": "real",
550                "timestamp": "2025-01-01T00:00:00Z"
551            }
552        });
553
554        let blended = blender.blend_responses(&mock, &real, 0.5);
555        assert!(blended.is_object());
556        assert!(blended.get("user").is_some());
557        assert!(blended.get("metadata").is_some());
558    }
559
560    #[test]
561    fn test_blend_ratio_boundaries() {
562        let blender = ResponseBlender::default();
563        let mock = json!({"value": "mock"});
564        let real = json!({"value": "real"});
565
566        // At 0.0, should return mock
567        let result_0 = blender.blend_responses(&mock, &real, 0.0);
568        assert_eq!(result_0, mock);
569
570        // At 1.0, should return real
571        let result_1 = blender.blend_responses(&mock, &real, 1.0);
572        assert_eq!(result_1, real);
573    }
574
575    #[test]
576    fn test_blend_mixed_types() {
577        let blender = ResponseBlender::default();
578        let mock = json!({
579            "string": "mock",
580            "number": 10,
581            "boolean": true,
582            "array": [1, 2, 3]
583        });
584        let real = json!({
585            "string": "real",
586            "number": 20,
587            "boolean": false,
588            "array": [4, 5, 6]
589        });
590
591        let blended = blender.blend_responses(&mock, &real, 0.5);
592        assert!(blended.is_object());
593        assert!(blended.get("string").is_some());
594        assert!(blended.get("number").is_some());
595        assert!(blended.get("boolean").is_some());
596        assert!(blended.get("array").is_some());
597    }
598}