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