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        let ratio = ratio.clamp(0.0, 1.0);
42
43        // If ratio is 0.0, return mock entirely
44        if ratio == 0.0 {
45            return mock.clone();
46        }
47
48        // If ratio is 1.0, return real entirely
49        if ratio == 1.0 {
50            return real.clone();
51        }
52
53        // Apply the selected merge strategy
54        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    /// Field-level intelligent merge
62    ///
63    /// Deep merges objects, combines arrays, and uses weighted selection for primitives.
64    fn blend_field_level(&self, mock: &Value, real: &Value, ratio: f64) -> Value {
65        match (mock, real) {
66            // Both are objects - deep merge
67            (Value::Object(mock_obj), Value::Object(real_obj)) => {
68                let mut result = serde_json::Map::new();
69
70                // Collect all keys from both objects
71                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                // Merge each key
80                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                            // Both exist - recursively blend
87                            result.insert(key, self.blend_field_level(m, r, ratio));
88                        }
89                        (Some(m), None) => {
90                            // Only in mock - include with reduced weight
91                            if ratio < 0.5 {
92                                result.insert(key, m.clone());
93                            }
94                        }
95                        (None, Some(r)) => {
96                            // Only in real - include with increased weight
97                            if ratio >= 0.5 {
98                                result.insert(key, r.clone());
99                            }
100                        }
101                        (None, None) => {
102                            // Neither (shouldn't happen)
103                        }
104                    }
105                }
106
107                Value::Object(result)
108            }
109            // Both are arrays - combine based on ratio
110            (Value::Array(mock_arr), Value::Array(real_arr)) => {
111                let mut result = Vec::new();
112
113                // Calculate how many items from each array
114                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                // Add items from mock array
119                for (i, item) in mock_arr.iter().enumerate() {
120                    if i < mock_count {
121                        result.push(item.clone());
122                    }
123                }
124
125                // Add items from real array
126                for (i, item) in real_arr.iter().enumerate() {
127                    if i < real_count {
128                        result.push(item.clone());
129                    }
130                }
131
132                // If arrays have different lengths, blend remaining items
133                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                            // Blend corresponding items
138                            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            // Both are numbers - weighted average
150            (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                    // Fallback to weighted selection
156                    if ratio < 0.5 {
157                        Value::Number(mock_num.clone())
158                    } else {
159                        Value::Number(real_num.clone())
160                    }
161                }
162            }
163            // Both are strings - weighted selection
164            (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            // Both are booleans - weighted selection
172            (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            // Type mismatch - prefer real if ratio > 0.5, otherwise mock
180            _ => {
181                if ratio >= 0.5 {
182                    real.clone()
183                } else {
184                    mock.clone()
185                }
186            }
187        }
188    }
189
190    /// Weighted selection strategy
191    ///
192    /// Randomly selects between mock and real based on ratio (for testing/demo purposes).
193    /// In practice, this would use the ratio as a probability threshold.
194    fn blend_weighted(&self, mock: &Value, real: &Value, ratio: f64) -> Value {
195        // For deterministic behavior, use threshold-based selection
196        // In a real implementation, you might want to use actual randomness
197        if ratio >= 0.5 {
198            real.clone()
199        } else {
200            mock.clone()
201        }
202    }
203
204    /// Body blending strategy
205    ///
206    /// Merges arrays, averages numeric fields, and deep merges objects.
207    fn blend_body(&self, mock: &Value, real: &Value, ratio: f64) -> Value {
208        // Similar to field-level but with different array handling
209        match (mock, real) {
210            (Value::Object(mock_obj), Value::Object(real_obj)) => {
211                let mut result = serde_json::Map::new();
212
213                // Collect all keys
214                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                // Merge each key
223                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                // Combine arrays, interleaving based on ratio
245                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                        // Blend corresponding items
251                        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    /// Blend HTTP status codes
284    ///
285    /// Returns the status code to use based on blend ratio.
286    /// Prefers real status code if ratio > 0.5, otherwise uses mock.
287    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    /// Blend HTTP headers
296    ///
297    /// Merges headers from both responses, preferring real headers when ratio > 0.5.
298    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        // Collect all header keys
307        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        // Merge headers
316        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                    // Both exist - prefer real if ratio > 0.5
323                    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                    // Only in mock - include if ratio < 0.5
331                    if ratio < 0.5 {
332                        result.insert(key, m.clone());
333                    }
334                }
335                (None, Some(r)) => {
336                    // Only in real - include if ratio >= 0.5
337                    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); // Should be approximately 15.0
398            }
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); // Prefer mock
406        assert_eq!(blender.blend_status_code(200, 404, 0.7), 404); // Prefer real
407    }
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        // At 0.0, should return mock
463        let result_0 = blender.blend_responses(&mock, &real, 0.0);
464        assert_eq!(result_0, mock);
465
466        // At 1.0, should return real
467        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}