mockforge_core/
overrides.rs

1//! Overrides engine with templating helpers.
2//!
3//! This module provides a comprehensive override system for modifying
4//! API responses based on operation IDs, tags, paths, and conditions.
5
6use crate::conditions::{evaluate_condition, ConditionContext};
7use crate::templating::expand_tokens as core_expand_tokens;
8use serde_json::Value;
9
10pub mod loader;
11pub mod matcher;
12pub mod models;
13pub mod patcher;
14
15// Re-export main types and functions for convenience
16pub use matcher::*;
17pub use models::{OverrideMode, OverrideRule, Overrides, PatchOp};
18pub use patcher::*;
19
20impl Overrides {
21    /// Get the loaded override rules
22    pub fn rules(&self) -> &[OverrideRule] {
23        &self.rules
24    }
25
26    pub fn apply(&self, operation_id: &str, tags: &[String], path: &str, body: &mut Value) {
27        self.apply_with_context(operation_id, tags, path, body, &ConditionContext::new())
28    }
29
30    /// Apply overrides with condition evaluation
31    pub fn apply_with_context(
32        &self,
33        operation_id: &str,
34        tags: &[String],
35        path: &str,
36        body: &mut Value,
37        context: &ConditionContext,
38    ) {
39        for r in &self.rules {
40            if !matcher::matches_target(r, operation_id, tags, path, &self.regex_cache) {
41                continue;
42            }
43
44            // Evaluate condition if present
45            if let Some(ref condition) = r.when {
46                match evaluate_condition(condition, context) {
47                    Ok(true) => {
48                        // Condition passed, continue with patch application
49                    }
50                    Ok(false) => {
51                        // Condition failed, skip this rule
52                        continue;
53                    }
54                    Err(e) => {
55                        // Log condition evaluation error but don't fail the entire override process
56                        tracing::warn!("Failed to evaluate condition '{}': {}", condition, e);
57                        continue;
58                    }
59                }
60            }
61
62            // Apply patches based on mode
63            match r.mode {
64                OverrideMode::Replace => {
65                    for op in &r.patch {
66                        let _ = patcher::apply_patch(body, op);
67                    }
68                }
69                OverrideMode::Merge => {
70                    for op in &r.patch {
71                        let _ = patcher::apply_merge_patch(body, op);
72                    }
73                }
74            }
75
76            // Apply post-templating expansion if enabled
77            if r.post_templating {
78                *body = core_expand_tokens(body);
79            }
80        }
81    }
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87    use serde_json::json;
88
89    #[test]
90    fn test_overrides_apply_basic() {
91        let overrides = Overrides {
92            rules: vec![OverrideRule {
93                targets: vec!["operation:test_op".to_string()],
94                mode: OverrideMode::Replace,
95                patch: vec![PatchOp::Replace {
96                    path: "/value".to_string(),
97                    value: json!("replaced"),
98                }],
99                when: None,
100                post_templating: false,
101            }],
102            regex_cache: Default::default(),
103        };
104
105        let mut body = json!({"value": "original"});
106        overrides.apply("test_op", &[], "/test", &mut body);
107
108        assert_eq!(body["value"], "replaced");
109    }
110
111    #[test]
112    fn test_overrides_apply_no_match() {
113        let overrides = Overrides {
114            rules: vec![OverrideRule {
115                targets: vec!["operation:other_op".to_string()],
116                mode: OverrideMode::Replace,
117                patch: vec![PatchOp::Replace {
118                    path: "/value".to_string(),
119                    value: json!("replaced"),
120                }],
121                when: None,
122                post_templating: false,
123            }],
124            regex_cache: Default::default(),
125        };
126
127        let mut body = json!({"value": "original"});
128        overrides.apply("test_op", &[], "/test", &mut body);
129
130        assert_eq!(body["value"], "original");
131    }
132
133    #[test]
134    fn test_overrides_apply_with_tag() {
135        let overrides = Overrides {
136            rules: vec![OverrideRule {
137                targets: vec!["tag:test_tag".to_string()],
138                mode: OverrideMode::Replace,
139                patch: vec![PatchOp::Replace {
140                    path: "/status".to_string(),
141                    value: json!("tagged"),
142                }],
143                when: None,
144                post_templating: false,
145            }],
146            regex_cache: Default::default(),
147        };
148
149        let mut body = json!({"status": "normal"});
150        overrides.apply("any_op", &["test_tag".to_string()], "/test", &mut body);
151
152        assert_eq!(body["status"], "tagged");
153    }
154
155    #[test]
156    fn test_overrides_apply_merge_mode() {
157        let overrides = Overrides {
158            rules: vec![OverrideRule {
159                targets: vec!["operation:test_op".to_string()],
160                mode: OverrideMode::Merge,
161                patch: vec![PatchOp::Add {
162                    path: "/extra".to_string(),
163                    value: json!("added"),
164                }],
165                when: None,
166                post_templating: false,
167            }],
168            regex_cache: Default::default(),
169        };
170
171        let mut body = json!({"value": "original"});
172        overrides.apply("test_op", &[], "/test", &mut body);
173
174        assert_eq!(body["value"], "original");
175        assert_eq!(body["extra"], "added");
176    }
177
178    #[test]
179    fn test_overrides_apply_with_context() {
180        // Test that the with_context method is callable
181        let overrides = Overrides {
182            rules: vec![OverrideRule {
183                targets: vec!["operation:test_op".to_string()],
184                mode: OverrideMode::Replace,
185                patch: vec![PatchOp::Replace {
186                    path: "/value".to_string(),
187                    value: json!("replaced"),
188                }],
189                when: None, // No condition for simplicity
190                post_templating: false,
191            }],
192            regex_cache: Default::default(),
193        };
194
195        let mut body = json!({"value": "original"});
196        let context = ConditionContext::new();
197        overrides.apply_with_context("test_op", &[], "/test", &mut body, &context);
198
199        assert_eq!(body["value"], "replaced");
200    }
201
202    #[test]
203    fn test_overrides_apply_multiple_patches() {
204        let overrides = Overrides {
205            rules: vec![OverrideRule {
206                targets: vec!["operation:test_op".to_string()],
207                mode: OverrideMode::Replace,
208                patch: vec![
209                    PatchOp::Add {
210                        path: "/field1".to_string(),
211                        value: json!("value1"),
212                    },
213                    PatchOp::Add {
214                        path: "/field2".to_string(),
215                        value: json!("value2"),
216                    },
217                ],
218                when: None,
219                post_templating: false,
220            }],
221            regex_cache: Default::default(),
222        };
223
224        let mut body = json!({"existing": "value"});
225        overrides.apply("test_op", &[], "/test", &mut body);
226
227        assert_eq!(body["field1"], "value1");
228        assert_eq!(body["field2"], "value2");
229    }
230
231    #[test]
232    fn test_overrides_apply_multiple_rules() {
233        let overrides = Overrides {
234            rules: vec![
235                OverrideRule {
236                    targets: vec!["operation:test_op".to_string()],
237                    mode: OverrideMode::Replace,
238                    patch: vec![PatchOp::Add {
239                        path: "/first".to_string(),
240                        value: json!("first_rule"),
241                    }],
242                    when: None,
243                    post_templating: false,
244                },
245                OverrideRule {
246                    targets: vec!["operation:test_op".to_string()],
247                    mode: OverrideMode::Replace,
248                    patch: vec![PatchOp::Add {
249                        path: "/second".to_string(),
250                        value: json!("second_rule"),
251                    }],
252                    when: None,
253                    post_templating: false,
254                },
255            ],
256            regex_cache: Default::default(),
257        };
258
259        let mut body = json!({"existing": "value"});
260        overrides.apply("test_op", &[], "/test", &mut body);
261
262        assert_eq!(body["first"], "first_rule");
263        assert_eq!(body["second"], "second_rule");
264    }
265
266    #[test]
267    fn test_overrides_replace_root_object() {
268        let overrides = Overrides {
269            rules: vec![OverrideRule {
270                targets: vec!["operation:testReplace".to_string()],
271                mode: OverrideMode::Replace,
272                patch: vec![PatchOp::Replace {
273                    path: "".to_string(),
274                    value: json!({"id": "1234", "kind": "replaced"}),
275                }],
276                when: None,
277                post_templating: false,
278            }],
279            regex_cache: Default::default(),
280        };
281
282        let mut body = json!({"existing": true});
283        overrides.apply("testReplace", &[], "/test-replace", &mut body);
284
285        assert_eq!(body["kind"], "replaced");
286        assert_eq!(body["id"], "1234");
287        assert!(body.get("existing").is_none());
288    }
289}