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 if ratio < 0.5 {
370                    Value::Number(mock_num.clone())
371                } else {
372                    Value::Number(real_num.clone())
373                }
374            }
375            _ => {
376                if ratio >= 0.5 {
377                    real.clone()
378                } else {
379                    mock.clone()
380                }
381            }
382        }
383    }
384
385    /// Blend HTTP status codes
386    ///
387    /// Returns the status code to use based on blend ratio.
388    /// Prefers real status code if ratio > 0.5, otherwise uses mock.
389    pub fn blend_status_code(&self, mock_status: u16, real_status: u16, ratio: f64) -> u16 {
390        if ratio >= 0.5 {
391            real_status
392        } else {
393            mock_status
394        }
395    }
396
397    /// Blend HTTP headers
398    ///
399    /// Merges headers from both responses, preferring real headers when ratio > 0.5.
400    pub fn blend_headers(
401        &self,
402        mock_headers: &HashMap<String, String>,
403        real_headers: &HashMap<String, String>,
404        ratio: f64,
405    ) -> HashMap<String, String> {
406        let mut result = HashMap::new();
407
408        // Collect all header keys
409        let mut all_keys = std::collections::HashSet::new();
410        for key in mock_headers.keys() {
411            all_keys.insert(key.clone());
412        }
413        for key in real_headers.keys() {
414            all_keys.insert(key.clone());
415        }
416
417        // Merge headers
418        for key in all_keys {
419            let mock_val = mock_headers.get(&key);
420            let real_val = real_headers.get(&key);
421
422            match (mock_val, real_val) {
423                (Some(m), Some(r)) => {
424                    // Both exist - prefer real if ratio > 0.5
425                    if ratio >= 0.5 {
426                        result.insert(key, r.clone());
427                    } else {
428                        result.insert(key, m.clone());
429                    }
430                }
431                (Some(m), None) => {
432                    // Only in mock - include if ratio < 0.5
433                    if ratio < 0.5 {
434                        result.insert(key, m.clone());
435                    }
436                }
437                (None, Some(r)) => {
438                    // Only in real - include if ratio >= 0.5
439                    if ratio >= 0.5 {
440                        result.insert(key, r.clone());
441                    }
442                }
443                (None, None) => {}
444            }
445        }
446
447        result
448    }
449}
450
451impl Default for ResponseBlender {
452    fn default() -> Self {
453        Self::new(MergeStrategy::FieldLevel)
454    }
455}
456
457#[cfg(test)]
458mod tests {
459    use super::*;
460    use serde_json::json;
461
462    #[test]
463    fn test_blend_objects() {
464        let blender = ResponseBlender::default();
465        let mock = json!({
466            "id": 1,
467            "name": "Mock User",
468            "email": "mock@example.com"
469        });
470        let real = json!({
471            "id": 2,
472            "name": "Real User",
473            "status": "active"
474        });
475
476        let blended = blender.blend_responses(&mock, &real, 0.5);
477        assert!(blended.is_object());
478    }
479
480    #[test]
481    fn test_blend_arrays() {
482        let blender = ResponseBlender::default();
483        let mock = json!([1, 2, 3]);
484        let real = json!([4, 5, 6]);
485
486        let blended = blender.blend_responses(&mock, &real, 0.5);
487        assert!(blended.is_array());
488    }
489
490    #[test]
491    fn test_blend_numbers() {
492        let blender = ResponseBlender::default();
493        let mock = json!(10.0);
494        let real = json!(20.0);
495
496        let blended = blender.blend_responses(&mock, &real, 0.5);
497        if let Value::Number(n) = blended {
498            if let Some(f) = n.as_f64() {
499                assert!((f - 15.0).abs() < 0.1); // Should be approximately 15.0
500            }
501        }
502    }
503
504    #[test]
505    fn test_blend_status_code() {
506        let blender = ResponseBlender::default();
507        assert_eq!(blender.blend_status_code(200, 404, 0.3), 200); // Prefer mock
508        assert_eq!(blender.blend_status_code(200, 404, 0.7), 404); // Prefer real
509    }
510
511    #[test]
512    fn test_blend_headers() {
513        let blender = ResponseBlender::default();
514        let mut mock_headers = HashMap::new();
515        mock_headers.insert("X-Mock".to_string(), "true".to_string());
516        mock_headers.insert("Content-Type".to_string(), "application/json".to_string());
517
518        let mut real_headers = HashMap::new();
519        real_headers.insert("X-Real".to_string(), "true".to_string());
520        real_headers.insert("Content-Type".to_string(), "application/xml".to_string());
521
522        let blended = blender.blend_headers(&mock_headers, &real_headers, 0.7);
523        assert_eq!(blended.get("Content-Type"), Some(&"application/xml".to_string()));
524        assert_eq!(blended.get("X-Real"), Some(&"true".to_string()));
525    }
526
527    #[test]
528    fn test_blend_nested_objects() {
529        let blender = ResponseBlender::default();
530        let mock = json!({
531            "user": {
532                "id": 1,
533                "name": "Mock User",
534                "email": "mock@example.com"
535            },
536            "metadata": {
537                "source": "mock"
538            }
539        });
540        let real = json!({
541            "user": {
542                "id": 2,
543                "name": "Real User",
544                "status": "active"
545            },
546            "metadata": {
547                "source": "real",
548                "timestamp": "2025-01-01T00:00:00Z"
549            }
550        });
551
552        let blended = blender.blend_responses(&mock, &real, 0.5);
553        assert!(blended.is_object());
554        assert!(blended.get("user").is_some());
555        assert!(blended.get("metadata").is_some());
556    }
557
558    #[test]
559    fn test_blend_ratio_boundaries() {
560        let blender = ResponseBlender::default();
561        let mock = json!({"value": "mock"});
562        let real = json!({"value": "real"});
563
564        // At 0.0, should return mock
565        let result_0 = blender.blend_responses(&mock, &real, 0.0);
566        assert_eq!(result_0, mock);
567
568        // At 1.0, should return real
569        let result_1 = blender.blend_responses(&mock, &real, 1.0);
570        assert_eq!(result_1, real);
571    }
572
573    #[test]
574    fn test_blend_mixed_types() {
575        let blender = ResponseBlender::default();
576        let mock = json!({
577            "string": "mock",
578            "number": 10,
579            "boolean": true,
580            "array": [1, 2, 3]
581        });
582        let real = json!({
583            "string": "real",
584            "number": 20,
585            "boolean": false,
586            "array": [4, 5, 6]
587        });
588
589        let blended = blender.blend_responses(&mock, &real, 0.5);
590        assert!(blended.is_object());
591        assert!(blended.get("string").is_some());
592        assert!(blended.get("number").is_some());
593        assert!(blended.get("boolean").is_some());
594        assert!(blended.get("array").is_some());
595    }
596}