1use jsonpath::Selector;
7use roxmltree::{Document, Node};
8use serde_json::Value;
9use std::collections::HashMap;
10use thiserror::Error;
11
12#[derive(Debug, Error)]
14pub enum ConditionError {
15 #[error("Invalid JSONPath expression: {0}")]
16 InvalidJsonPath(String),
17
18 #[error("Invalid XPath expression: {0}")]
19 InvalidXPath(String),
20
21 #[error("Invalid XML: {0}")]
22 InvalidXml(String),
23
24 #[error("Unsupported condition type: {0}")]
25 UnsupportedCondition(String),
26
27 #[error("Condition evaluation failed: {0}")]
28 EvaluationFailed(String),
29}
30
31#[derive(Debug, Clone)]
33pub struct ConditionContext {
34 pub request_body: Option<Value>,
36 pub response_body: Option<Value>,
38 pub request_xml: Option<String>,
40 pub response_xml: Option<String>,
42 pub headers: HashMap<String, String>,
44 pub query_params: HashMap<String, String>,
46 pub path: String,
48 pub method: String,
50 pub operation_id: Option<String>,
52 pub tags: Vec<String>,
54}
55
56impl Default for ConditionContext {
57 fn default() -> Self {
58 Self::new()
59 }
60}
61
62impl ConditionContext {
63 pub fn new() -> Self {
64 Self {
65 request_body: None,
66 response_body: None,
67 request_xml: None,
68 response_xml: None,
69 headers: HashMap::new(),
70 query_params: HashMap::new(),
71 path: String::new(),
72 method: String::new(),
73 operation_id: None,
74 tags: Vec::new(),
75 }
76 }
77
78 pub fn with_request_body(mut self, body: Value) -> Self {
79 self.request_body = Some(body);
80 self
81 }
82
83 pub fn with_response_body(mut self, body: Value) -> Self {
84 self.response_body = Some(body);
85 self
86 }
87
88 pub fn with_request_xml(mut self, xml: String) -> Self {
89 self.request_xml = Some(xml);
90 self
91 }
92
93 pub fn with_response_xml(mut self, xml: String) -> Self {
94 self.response_xml = Some(xml);
95 self
96 }
97
98 pub fn with_headers(mut self, headers: HashMap<String, String>) -> Self {
99 self.headers = headers;
100 self
101 }
102
103 pub fn with_query_params(mut self, params: HashMap<String, String>) -> Self {
104 self.query_params = params;
105 self
106 }
107
108 pub fn with_path(mut self, path: String) -> Self {
109 self.path = path;
110 self
111 }
112
113 pub fn with_method(mut self, method: String) -> Self {
114 self.method = method;
115 self
116 }
117
118 pub fn with_operation_id(mut self, operation_id: String) -> Self {
119 self.operation_id = Some(operation_id);
120 self
121 }
122
123 pub fn with_tags(mut self, tags: Vec<String>) -> Self {
124 self.tags = tags;
125 self
126 }
127}
128
129pub fn evaluate_condition(
131 condition: &str,
132 context: &ConditionContext,
133) -> Result<bool, ConditionError> {
134 let condition = condition.trim();
135
136 if condition.is_empty() {
137 return Ok(true); }
139
140 if let Some(and_conditions) = condition.strip_prefix("AND(") {
142 if let Some(inner) = and_conditions.strip_suffix(")") {
143 return evaluate_and_condition(inner, context);
144 }
145 }
146
147 if let Some(or_conditions) = condition.strip_prefix("OR(") {
148 if let Some(inner) = or_conditions.strip_suffix(")") {
149 return evaluate_or_condition(inner, context);
150 }
151 }
152
153 if let Some(not_condition) = condition.strip_prefix("NOT(") {
154 if let Some(inner) = not_condition.strip_suffix(")") {
155 return evaluate_not_condition(inner, context);
156 }
157 }
158
159 if condition.starts_with("$.") || condition.starts_with("$[") {
161 return evaluate_jsonpath(condition, context);
162 }
163
164 if condition.starts_with("/") || condition.starts_with("//") {
166 return evaluate_xpath(condition, context);
167 }
168
169 evaluate_simple_condition(condition, context)
171}
172
173fn evaluate_and_condition(
175 conditions: &str,
176 context: &ConditionContext,
177) -> Result<bool, ConditionError> {
178 let parts: Vec<&str> = conditions.split(',').map(|s| s.trim()).collect();
179
180 for part in parts {
181 if !evaluate_condition(part, context)? {
182 return Ok(false);
183 }
184 }
185
186 Ok(true)
187}
188
189fn evaluate_or_condition(
191 conditions: &str,
192 context: &ConditionContext,
193) -> Result<bool, ConditionError> {
194 let parts: Vec<&str> = conditions.split(',').map(|s| s.trim()).collect();
195
196 for part in parts {
197 if evaluate_condition(part, context)? {
198 return Ok(true);
199 }
200 }
201
202 Ok(false)
203}
204
205fn evaluate_not_condition(
207 condition: &str,
208 context: &ConditionContext,
209) -> Result<bool, ConditionError> {
210 Ok(!evaluate_condition(condition, context)?)
211}
212
213fn evaluate_jsonpath(query: &str, context: &ConditionContext) -> Result<bool, ConditionError> {
215 let (_is_request, json_value) = if query.starts_with("$.request.") {
217 let _query = query.replace("$.request.", "$.");
218 (true, &context.request_body)
219 } else if query.starts_with("$.response.") {
220 let _query = query.replace("$.response.", "$.");
221 (false, &context.response_body)
222 } else {
223 (false, &context.response_body)
225 };
226
227 let Some(json_value) = json_value else {
228 return Ok(false); };
230
231 match Selector::new(query) {
232 Ok(selector) => {
233 let results: Vec<_> = selector.find(json_value).collect();
234 Ok(!results.is_empty())
235 }
236 Err(_) => Err(ConditionError::InvalidJsonPath(query.to_string())),
237 }
238}
239
240fn evaluate_xpath(query: &str, context: &ConditionContext) -> Result<bool, ConditionError> {
242 let (_is_request, xml_content) = if query.starts_with("/request/") {
244 let _query = query.replace("/request/", "/");
245 (true, &context.request_xml)
246 } else if query.starts_with("/response/") {
247 let _query = query.replace("/response/", "/");
248 (false, &context.response_xml)
249 } else {
250 (false, &context.response_xml)
252 };
253
254 let Some(xml_content) = xml_content else {
255 println!("Debug - No XML content available for query: {}", query);
256 return Ok(false); };
258
259 println!("Debug - Evaluating XPath '{}' against XML content: {}", query, xml_content);
260
261 match Document::parse(xml_content) {
262 Ok(doc) => {
263 let root = doc.root_element();
265 println!("Debug - XML root element: {}", root.tag_name().name());
266 let matches = evaluate_xpath_simple(&root, query);
267 println!("Debug - XPath result: {}", matches);
268 Ok(matches)
269 }
270 Err(e) => {
271 println!("Debug - Failed to parse XML: {}", e);
272 Err(ConditionError::InvalidXml(xml_content.clone()))
273 }
274 }
275}
276
277fn evaluate_xpath_simple(node: &Node, xpath: &str) -> bool {
279 if let Some(element_name) = xpath.strip_prefix("//") {
284 println!(
285 "Debug - Checking descendant-or-self for element '{}' on node '{}'",
286 element_name,
287 node.tag_name().name()
288 );
289 if node.tag_name().name() == element_name {
290 println!("Debug - Found match: {} == {}", node.tag_name().name(), element_name);
291 return true;
292 }
293 for child in node.children() {
295 if child.is_element() {
296 println!("Debug - Checking child element: {}", child.tag_name().name());
297 if evaluate_xpath_simple(&child, &format!("//{}", element_name)) {
298 return true;
299 }
300 }
301 }
302 return false; }
304
305 let xpath = xpath.trim_start_matches('/');
306
307 if xpath.is_empty() {
308 return true;
309 }
310
311 if let Some((element_part, attr_part)) = xpath.split_once('[') {
313 if let Some(attr_query) = attr_part.strip_suffix(']') {
314 if let Some((attr_name, attr_value)) = attr_query.split_once("='") {
315 if let Some(expected_value) = attr_value.strip_suffix('\'') {
316 if let Some(attr_val) = attr_name.strip_prefix('@') {
317 if node.tag_name().name() == element_part {
318 if let Some(attr) = node.attribute(attr_val) {
319 return attr == expected_value;
320 }
321 }
322 }
323 }
324 }
325 }
326 return false;
327 }
328
329 if let Some((element_name, rest)) = xpath.split_once('/') {
331 if node.tag_name().name() == element_name {
332 if rest.is_empty() {
333 return true;
334 }
335 for child in node.children() {
337 if child.is_element() && evaluate_xpath_simple(&child, rest) {
338 return true;
339 }
340 }
341 }
342 } else if node.tag_name().name() == xpath {
343 return true;
344 }
345
346 if let Some(text_query) = xpath.strip_suffix("/text()") {
348 if node.tag_name().name() == text_query {
349 return node.text().is_some_and(|t| !t.trim().is_empty());
350 }
351 }
352
353 false
354}
355
356fn evaluate_simple_condition(
358 condition: &str,
359 context: &ConditionContext,
360) -> Result<bool, ConditionError> {
361 if let Some(header_condition) = condition.strip_prefix("header[") {
363 if let Some((header_name, expected_value)) = header_condition.split_once("]=") {
364 let expected_value = expected_value.trim();
365 if let Some(actual_value) = context.headers.get(header_name) {
366 return Ok(actual_value == expected_value);
367 }
368 return Ok(false);
369 }
370 }
371
372 if let Some(query_condition) = condition.strip_prefix("query[") {
374 if let Some((param_name, expected_value)) = query_condition.split_once("]=") {
375 let expected_value = expected_value.trim();
376 if let Some(actual_value) = context.query_params.get(param_name) {
377 return Ok(actual_value == expected_value);
378 }
379 return Ok(false);
380 }
381 }
382
383 if let Some(method_condition) = condition.strip_prefix("method=") {
385 return Ok(context.method == method_condition);
386 }
387
388 if let Some(path_condition) = condition.strip_prefix("path=") {
390 return Ok(context.path == path_condition);
391 }
392
393 if let Some(tag_condition) = condition.strip_prefix("has_tag[") {
395 if let Some(tag) = tag_condition.strip_suffix("]") {
396 return Ok(context.tags.contains(&tag.to_string()));
397 }
398 }
399
400 if let Some(op_condition) = condition.strip_prefix("operation=") {
402 if let Some(operation_id) = &context.operation_id {
403 return Ok(operation_id == op_condition);
404 }
405 return Ok(false);
406 }
407
408 Err(ConditionError::UnsupportedCondition(condition.to_string()))
409}
410
411#[cfg(test)]
412mod tests {
413 use super::*;
414 use serde_json::json;
415
416 #[test]
417 fn test_jsonpath_condition() {
418 let context = ConditionContext::new().with_response_body(json!({
419 "user": {
420 "name": "John",
421 "role": "admin"
422 },
423 "items": [1, 2, 3]
424 }));
425
426 assert!(evaluate_condition("$.user", &context).unwrap());
428
429 assert!(evaluate_condition("$.user.role", &context).unwrap());
431
432 assert!(evaluate_condition("$.items[0]", &context).unwrap());
434
435 assert!(!evaluate_condition("$.nonexistent", &context).unwrap());
437 }
438
439 #[test]
440 fn test_simple_conditions() {
441 let mut headers = HashMap::new();
442 headers.insert("authorization".to_string(), "Bearer token123".to_string());
443
444 let mut query_params = HashMap::new();
445 query_params.insert("limit".to_string(), "10".to_string());
446
447 let context = ConditionContext::new()
448 .with_headers(headers)
449 .with_query_params(query_params)
450 .with_method("POST".to_string())
451 .with_path("/api/users".to_string());
452
453 assert!(evaluate_condition("header[authorization]=Bearer token123", &context).unwrap());
455 assert!(!evaluate_condition("header[authorization]=Bearer wrong", &context).unwrap());
456
457 assert!(evaluate_condition("query[limit]=10", &context).unwrap());
459 assert!(!evaluate_condition("query[limit]=20", &context).unwrap());
460
461 assert!(evaluate_condition("method=POST", &context).unwrap());
463 assert!(!evaluate_condition("method=GET", &context).unwrap());
464
465 assert!(evaluate_condition("path=/api/users", &context).unwrap());
467 assert!(!evaluate_condition("path=/api/posts", &context).unwrap());
468 }
469
470 #[test]
471 fn test_logical_conditions() {
472 let context = ConditionContext::new()
473 .with_method("POST".to_string())
474 .with_path("/api/users".to_string());
475
476 assert!(evaluate_condition("AND(method=POST,path=/api/users)", &context).unwrap());
478 assert!(!evaluate_condition("AND(method=GET,path=/api/users)", &context).unwrap());
479
480 assert!(evaluate_condition("OR(method=POST,path=/api/posts)", &context).unwrap());
482 assert!(!evaluate_condition("OR(method=GET,path=/api/posts)", &context).unwrap());
483
484 assert!(!evaluate_condition("NOT(method=POST)", &context).unwrap());
486 assert!(evaluate_condition("NOT(method=GET)", &context).unwrap());
487 }
488
489 #[test]
490 fn test_xpath_condition() {
491 let xml_content = r#"
492 <user id="123">
493 <name>John Doe</name>
494 <role>admin</role>
495 <preferences>
496 <theme>dark</theme>
497 <notifications>true</notifications>
498 </preferences>
499 </user>
500 "#;
501
502 let context = ConditionContext::new().with_response_xml(xml_content.to_string());
503
504 assert!(evaluate_condition("/user", &context).unwrap());
506
507 assert!(evaluate_condition("/user/name", &context).unwrap());
509
510 assert!(evaluate_condition("/user[@id='123']", &context).unwrap());
512 assert!(!evaluate_condition("/user[@id='456']", &context).unwrap());
513
514 assert!(evaluate_condition("/user/name/text()", &context).unwrap());
516
517 assert!(evaluate_condition("//theme", &context).unwrap());
519
520 assert!(!evaluate_condition("/nonexistent", &context).unwrap());
522 }
523}