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
359    #[test]
360    fn test_blend_objects() {
361        let blender = ResponseBlender::default();
362        let mock = json!({
363            "id": 1,
364            "name": "Mock User",
365            "email": "mock@example.com"
366        });
367        let real = json!({
368            "id": 2,
369            "name": "Real User",
370            "status": "active"
371        });
372
373        let blended = blender.blend_responses(&mock, &real, 0.5);
374        assert!(blended.is_object());
375    }
376
377    #[test]
378    fn test_blend_arrays() {
379        let blender = ResponseBlender::default();
380        let mock = json!([1, 2, 3]);
381        let real = json!([4, 5, 6]);
382
383        let blended = blender.blend_responses(&mock, &real, 0.5);
384        assert!(blended.is_array());
385    }
386
387    #[test]
388    fn test_blend_numbers() {
389        let blender = ResponseBlender::default();
390        let mock = json!(10.0);
391        let real = json!(20.0);
392
393        let blended = blender.blend_responses(&mock, &real, 0.5);
394        if let Value::Number(n) = blended {
395            if let Some(f) = n.as_f64() {
396                assert!((f - 15.0).abs() < 0.1); // Should be approximately 15.0
397            }
398        }
399    }
400
401    #[test]
402    fn test_blend_status_code() {
403        let blender = ResponseBlender::default();
404        assert_eq!(blender.blend_status_code(200, 404, 0.3), 200); // Prefer mock
405        assert_eq!(blender.blend_status_code(200, 404, 0.7), 404); // Prefer real
406    }
407
408    #[test]
409    fn test_blend_headers() {
410        let blender = ResponseBlender::default();
411        let mut mock_headers = HashMap::new();
412        mock_headers.insert("X-Mock".to_string(), "true".to_string());
413        mock_headers.insert("Content-Type".to_string(), "application/json".to_string());
414
415        let mut real_headers = HashMap::new();
416        real_headers.insert("X-Real".to_string(), "true".to_string());
417        real_headers.insert("Content-Type".to_string(), "application/xml".to_string());
418
419        let blended = blender.blend_headers(&mock_headers, &real_headers, 0.7);
420        assert_eq!(blended.get("Content-Type"), Some(&"application/xml".to_string()));
421        assert_eq!(blended.get("X-Real"), Some(&"true".to_string()));
422    }
423
424    #[test]
425    fn test_blend_nested_objects() {
426        let blender = ResponseBlender::default();
427        let mock = json!({
428            "user": {
429                "id": 1,
430                "name": "Mock User",
431                "email": "mock@example.com"
432            },
433            "metadata": {
434                "source": "mock"
435            }
436        });
437        let real = json!({
438            "user": {
439                "id": 2,
440                "name": "Real User",
441                "status": "active"
442            },
443            "metadata": {
444                "source": "real",
445                "timestamp": "2025-01-01T00:00:00Z"
446            }
447        });
448
449        let blended = blender.blend_responses(&mock, &real, 0.5);
450        assert!(blended.is_object());
451        assert!(blended.get("user").is_some());
452        assert!(blended.get("metadata").is_some());
453    }
454
455    #[test]
456    fn test_blend_ratio_boundaries() {
457        let blender = ResponseBlender::default();
458        let mock = json!({"value": "mock"});
459        let real = json!({"value": "real"});
460
461        // At 0.0, should return mock
462        let result_0 = blender.blend_responses(&mock, &real, 0.0);
463        assert_eq!(result_0, mock);
464
465        // At 1.0, should return real
466        let result_1 = blender.blend_responses(&mock, &real, 1.0);
467        assert_eq!(result_1, real);
468    }
469
470    #[test]
471    fn test_blend_mixed_types() {
472        let blender = ResponseBlender::default();
473        let mock = json!({
474            "string": "mock",
475            "number": 10,
476            "boolean": true,
477            "array": [1, 2, 3]
478        });
479        let real = json!({
480            "string": "real",
481            "number": 20,
482            "boolean": false,
483            "array": [4, 5, 6]
484        });
485
486        let blended = blender.blend_responses(&mock, &real, 0.5);
487        assert!(blended.is_object());
488        assert!(blended.get("string").is_some());
489        assert!(blended.get("number").is_some());
490        assert!(blended.get("boolean").is_some());
491        assert!(blended.get("array").is_some());
492    }
493}