1use jsonpath::Selector;
7use roxmltree::{Document, Node};
8use serde_json::Value;
9use std::collections::HashMap;
10use thiserror::Error;
11use tracing::debug;
12
13#[derive(Debug, Error)]
15pub enum ConditionError {
16 #[error("Invalid JSONPath expression: {0}")]
18 InvalidJsonPath(String),
19
20 #[error("Invalid XPath expression: {0}")]
22 InvalidXPath(String),
23
24 #[error("Invalid XML: {0}")]
26 InvalidXml(String),
27
28 #[error("Unsupported condition type: {0}")]
30 UnsupportedCondition(String),
31
32 #[error("Condition evaluation failed: {0}")]
34 EvaluationFailed(String),
35}
36
37#[derive(Debug, Clone)]
39pub struct ConditionContext {
40 pub request_body: Option<Value>,
42 pub response_body: Option<Value>,
44 pub request_xml: Option<String>,
46 pub response_xml: Option<String>,
48 pub headers: HashMap<String, String>,
50 pub query_params: HashMap<String, String>,
52 pub path: String,
54 pub method: String,
56 pub operation_id: Option<String>,
58 pub tags: Vec<String>,
60}
61
62impl Default for ConditionContext {
63 fn default() -> Self {
64 Self::new()
65 }
66}
67
68impl ConditionContext {
69 pub fn new() -> Self {
71 Self {
72 request_body: None,
73 response_body: None,
74 request_xml: None,
75 response_xml: None,
76 headers: HashMap::new(),
77 query_params: HashMap::new(),
78 path: String::new(),
79 method: String::new(),
80 operation_id: None,
81 tags: Vec::new(),
82 }
83 }
84
85 pub fn with_request_body(mut self, body: Value) -> Self {
87 self.request_body = Some(body);
88 self
89 }
90
91 pub fn with_response_body(mut self, body: Value) -> Self {
93 self.response_body = Some(body);
94 self
95 }
96
97 pub fn with_request_xml(mut self, xml: String) -> Self {
99 self.request_xml = Some(xml);
100 self
101 }
102
103 pub fn with_response_xml(mut self, xml: String) -> Self {
105 self.response_xml = Some(xml);
106 self
107 }
108
109 pub fn with_headers(mut self, headers: HashMap<String, String>) -> Self {
111 self.headers = headers;
112 self
113 }
114
115 pub fn with_query_params(mut self, params: HashMap<String, String>) -> Self {
117 self.query_params = params;
118 self
119 }
120
121 pub fn with_path(mut self, path: String) -> Self {
123 self.path = path;
124 self
125 }
126
127 pub fn with_method(mut self, method: String) -> Self {
129 self.method = method;
130 self
131 }
132
133 pub fn with_operation_id(mut self, operation_id: String) -> Self {
135 self.operation_id = Some(operation_id);
136 self
137 }
138
139 pub fn with_tags(mut self, tags: Vec<String>) -> Self {
141 self.tags = tags;
142 self
143 }
144}
145
146pub fn evaluate_condition(
148 condition: &str,
149 context: &ConditionContext,
150) -> Result<bool, ConditionError> {
151 let condition = condition.trim();
152
153 if condition.is_empty() {
154 return Ok(true); }
156
157 if let Some(and_conditions) = condition.strip_prefix("AND(") {
159 if let Some(inner) = and_conditions.strip_suffix(")") {
160 return evaluate_and_condition(inner, context);
161 }
162 }
163
164 if let Some(or_conditions) = condition.strip_prefix("OR(") {
165 if let Some(inner) = or_conditions.strip_suffix(")") {
166 return evaluate_or_condition(inner, context);
167 }
168 }
169
170 if let Some(not_condition) = condition.strip_prefix("NOT(") {
171 if let Some(inner) = not_condition.strip_suffix(")") {
172 return evaluate_not_condition(inner, context);
173 }
174 }
175
176 if condition.starts_with("$.") || condition.starts_with("$[") {
178 return evaluate_jsonpath(condition, context);
179 }
180
181 if condition.starts_with("/") || condition.starts_with("//") {
183 return evaluate_xpath(condition, context);
184 }
185
186 evaluate_simple_condition(condition, context)
188}
189
190fn evaluate_and_condition(
192 conditions: &str,
193 context: &ConditionContext,
194) -> Result<bool, ConditionError> {
195 let parts: Vec<&str> = conditions.split(',').map(|s| s.trim()).collect();
196
197 for part in parts {
198 if !evaluate_condition(part, context)? {
199 return Ok(false);
200 }
201 }
202
203 Ok(true)
204}
205
206fn evaluate_or_condition(
208 conditions: &str,
209 context: &ConditionContext,
210) -> Result<bool, ConditionError> {
211 let parts: Vec<&str> = conditions.split(',').map(|s| s.trim()).collect();
212
213 for part in parts {
214 if evaluate_condition(part, context)? {
215 return Ok(true);
216 }
217 }
218
219 Ok(false)
220}
221
222fn evaluate_not_condition(
224 condition: &str,
225 context: &ConditionContext,
226) -> Result<bool, ConditionError> {
227 Ok(!evaluate_condition(condition, context)?)
228}
229
230fn evaluate_jsonpath(query: &str, context: &ConditionContext) -> Result<bool, ConditionError> {
232 let (jsonpath_expr, comparison_op, expected_value) =
235 if let Some((path, value)) = query.split_once("==") {
236 let path = path.trim();
237 let value = value.trim().trim_matches('\'').trim_matches('"');
238 (path, Some("=="), Some(value))
239 } else if let Some((path, value)) = query.split_once("!=") {
240 let path = path.trim();
241 let value = value.trim().trim_matches('\'').trim_matches('"');
242 (path, Some("!="), Some(value))
243 } else {
244 (query, None, None)
245 };
246
247 let (_is_request, json_value) = if jsonpath_expr.starts_with("$.request.") {
249 let _query = jsonpath_expr.replace("$.request.", "$.");
250 (true, &context.request_body)
251 } else if jsonpath_expr.starts_with("$.response.") {
252 let _query = jsonpath_expr.replace("$.response.", "$.");
253 (false, &context.response_body)
254 } else {
255 if context.response_body.is_some() {
257 (false, &context.response_body)
258 } else {
259 (true, &context.request_body)
260 }
261 };
262
263 let Some(json_value) = json_value else {
264 return Ok(false); };
266
267 match Selector::new(jsonpath_expr) {
268 Ok(selector) => {
269 let results: Vec<_> = selector.find(json_value).collect();
270
271 if let (Some(op), Some(expected)) = (comparison_op, expected_value) {
273 if results.is_empty() {
274 return Ok(false);
275 }
276
277 let actual_value = match &results[0] {
279 Value::String(s) => s.as_str(),
280 Value::Number(n) => {
281 return Ok(match op {
282 "==" => n.to_string() == expected,
283 "!=" => n.to_string() != expected,
284 _ => false,
285 })
286 }
287 Value::Bool(b) => {
288 return Ok(match op {
289 "==" => b.to_string() == expected,
290 "!=" => b.to_string() != expected,
291 _ => false,
292 })
293 }
294 Value::Null => {
295 return Ok(match op {
296 "==" => expected == "null",
297 "!=" => expected != "null",
298 _ => false,
299 })
300 }
301 _ => return Ok(false),
302 };
303
304 return Ok(match op {
305 "==" => actual_value == expected,
306 "!=" => actual_value != expected,
307 _ => false,
308 });
309 }
310
311 Ok(!results.is_empty())
313 }
314 Err(_) => Err(ConditionError::InvalidJsonPath(jsonpath_expr.to_string())),
315 }
316}
317
318fn evaluate_xpath(query: &str, context: &ConditionContext) -> Result<bool, ConditionError> {
320 let (_is_request, xml_content) = if query.starts_with("/request/") {
322 let _query = query.replace("/request/", "/");
323 (true, &context.request_xml)
324 } else if query.starts_with("/response/") {
325 let _query = query.replace("/response/", "/");
326 (false, &context.response_xml)
327 } else {
328 (false, &context.response_xml)
330 };
331
332 let Some(xml_content) = xml_content else {
333 debug!("No XML content available for query: {}", query);
334 return Ok(false); };
336
337 debug!("Evaluating XPath '{}' against XML content: {}", query, xml_content);
338
339 match Document::parse(xml_content) {
340 Ok(doc) => {
341 let root = doc.root_element();
343 debug!("XML root element: {}", root.tag_name().name());
344 let matches = evaluate_xpath_simple(&root, query);
345 debug!("XPath result: {}", matches);
346 Ok(matches)
347 }
348 Err(e) => {
349 debug!("Failed to parse XML: {}", e);
350 Err(ConditionError::InvalidXml(xml_content.clone()))
351 }
352 }
353}
354
355fn evaluate_xpath_simple(node: &Node, xpath: &str) -> bool {
357 if let Some(element_name) = xpath.strip_prefix("//") {
362 debug!(
363 "Checking descendant-or-self for element '{}' on node '{}'",
364 element_name,
365 node.tag_name().name()
366 );
367 if node.tag_name().name() == element_name {
368 debug!("Found match: {} == {}", node.tag_name().name(), element_name);
369 return true;
370 }
371 for child in node.children() {
373 if child.is_element() {
374 debug!("Checking child element: {}", child.tag_name().name());
375 if evaluate_xpath_simple(&child, &format!("//{}", element_name)) {
376 return true;
377 }
378 }
379 }
380 return false; }
382
383 let xpath = xpath.trim_start_matches('/');
384
385 if xpath.is_empty() {
386 return true;
387 }
388
389 if let Some((element_part, attr_part)) = xpath.split_once('[') {
391 if let Some(attr_query) = attr_part.strip_suffix(']') {
392 if let Some((attr_name, attr_value)) = attr_query.split_once("='") {
393 if let Some(expected_value) = attr_value.strip_suffix('\'') {
394 if let Some(attr_val) = attr_name.strip_prefix('@') {
395 if node.tag_name().name() == element_part {
396 if let Some(attr) = node.attribute(attr_val) {
397 return attr == expected_value;
398 }
399 }
400 }
401 }
402 }
403 }
404 return false;
405 }
406
407 if let Some((element_name, rest)) = xpath.split_once('/') {
409 if node.tag_name().name() == element_name {
410 if rest.is_empty() {
411 return true;
412 }
413 for child in node.children() {
415 if child.is_element() && evaluate_xpath_simple(&child, rest) {
416 return true;
417 }
418 }
419 }
420 } else if node.tag_name().name() == xpath {
421 return true;
422 }
423
424 if let Some(text_query) = xpath.strip_suffix("/text()") {
426 if node.tag_name().name() == text_query {
427 return node.text().is_some_and(|t| !t.trim().is_empty());
428 }
429 }
430
431 false
432}
433
434fn evaluate_simple_condition(
436 condition: &str,
437 context: &ConditionContext,
438) -> Result<bool, ConditionError> {
439 if let Some(header_condition) = condition.strip_prefix("header[") {
441 if let Some((header_name, rest)) = header_condition.split_once("]") {
442 let header_name_lower = header_name.to_lowercase();
444 let rest_trimmed = rest.trim();
445 if let Some(expected_value) = rest_trimmed.strip_prefix("!=") {
447 let expected_value = expected_value.trim().trim_matches('\'').trim_matches('"');
448 if let Some(actual_value) = context.headers.get(&header_name_lower) {
449 return Ok(actual_value != expected_value);
451 }
452 return Ok(!expected_value.is_empty());
455 }
456 if let Some(expected_value) = rest_trimmed.strip_prefix("=") {
458 let expected_value = expected_value.trim().trim_matches('\'').trim_matches('"');
459 if let Some(actual_value) = context.headers.get(&header_name_lower) {
460 return Ok(actual_value == expected_value);
461 }
462 return Ok(false);
463 }
464 }
465 }
466
467 if let Some(query_condition) = condition.strip_prefix("query[") {
469 if let Some((param_name, rest)) = query_condition.split_once("]") {
470 let rest_trimmed = rest.trim();
471 if let Some(expected_value) = rest_trimmed.strip_prefix("==") {
473 let expected_value = expected_value.trim().trim_matches('\'').trim_matches('"');
474 if let Some(actual_value) = context.query_params.get(param_name) {
475 return Ok(actual_value == expected_value);
476 }
477 return Ok(false);
478 }
479 if let Some(expected_value) = rest_trimmed.strip_prefix("=") {
481 let expected_value = expected_value.trim();
482 if let Some(actual_value) = context.query_params.get(param_name) {
483 return Ok(actual_value == expected_value);
484 }
485 return Ok(false);
486 }
487 }
488 }
489
490 if let Some(method_condition) = condition.strip_prefix("method=") {
492 return Ok(context.method == method_condition);
493 }
494
495 if let Some(path_condition) = condition.strip_prefix("path=") {
497 return Ok(context.path == path_condition);
498 }
499
500 if let Some(tag_condition) = condition.strip_prefix("has_tag[") {
502 if let Some(tag) = tag_condition.strip_suffix("]") {
503 return Ok(context.tags.contains(&tag.to_string()));
504 }
505 }
506
507 if let Some(op_condition) = condition.strip_prefix("operation=") {
509 if let Some(operation_id) = &context.operation_id {
510 return Ok(operation_id == op_condition);
511 }
512 return Ok(false);
513 }
514
515 Err(ConditionError::UnsupportedCondition(condition.to_string()))
516}
517
518#[cfg(test)]
519mod tests {
520 use super::*;
521 use serde_json::json;
522
523 #[test]
524 fn test_jsonpath_condition() {
525 let context = ConditionContext::new().with_response_body(json!({
526 "user": {
527 "name": "John",
528 "role": "admin"
529 },
530 "items": [1, 2, 3]
531 }));
532
533 assert!(evaluate_condition("$.user", &context).unwrap());
535
536 assert!(evaluate_condition("$.user.role", &context).unwrap());
538
539 assert!(evaluate_condition("$.items[0]", &context).unwrap());
541
542 assert!(!evaluate_condition("$.nonexistent", &context).unwrap());
544 }
545
546 #[test]
547 fn test_simple_conditions() {
548 let mut headers = HashMap::new();
549 headers.insert("authorization".to_string(), "Bearer token123".to_string());
550
551 let mut query_params = HashMap::new();
552 query_params.insert("limit".to_string(), "10".to_string());
553
554 let context = ConditionContext::new()
555 .with_headers(headers)
556 .with_query_params(query_params)
557 .with_method("POST".to_string())
558 .with_path("/api/users".to_string());
559
560 assert!(evaluate_condition("header[authorization]=Bearer token123", &context).unwrap());
562 assert!(!evaluate_condition("header[authorization]=Bearer wrong", &context).unwrap());
563
564 assert!(evaluate_condition("query[limit]=10", &context).unwrap());
566 assert!(!evaluate_condition("query[limit]=20", &context).unwrap());
567
568 assert!(evaluate_condition("method=POST", &context).unwrap());
570 assert!(!evaluate_condition("method=GET", &context).unwrap());
571
572 assert!(evaluate_condition("path=/api/users", &context).unwrap());
574 assert!(!evaluate_condition("path=/api/posts", &context).unwrap());
575 }
576
577 #[test]
578 fn test_logical_conditions() {
579 let context = ConditionContext::new()
580 .with_method("POST".to_string())
581 .with_path("/api/users".to_string());
582
583 assert!(evaluate_condition("AND(method=POST,path=/api/users)", &context).unwrap());
585 assert!(!evaluate_condition("AND(method=GET,path=/api/users)", &context).unwrap());
586
587 assert!(evaluate_condition("OR(method=POST,path=/api/posts)", &context).unwrap());
589 assert!(!evaluate_condition("OR(method=GET,path=/api/posts)", &context).unwrap());
590
591 assert!(!evaluate_condition("NOT(method=POST)", &context).unwrap());
593 assert!(evaluate_condition("NOT(method=GET)", &context).unwrap());
594 }
595
596 #[test]
597 fn test_xpath_condition() {
598 let xml_content = r#"
599 <user id="123">
600 <name>John Doe</name>
601 <role>admin</role>
602 <preferences>
603 <theme>dark</theme>
604 <notifications>true</notifications>
605 </preferences>
606 </user>
607 "#;
608
609 let context = ConditionContext::new().with_response_xml(xml_content.to_string());
610
611 assert!(evaluate_condition("/user", &context).unwrap());
613
614 assert!(evaluate_condition("/user/name", &context).unwrap());
616
617 assert!(evaluate_condition("/user[@id='123']", &context).unwrap());
619 assert!(!evaluate_condition("/user[@id='456']", &context).unwrap());
620
621 assert!(evaluate_condition("/user/name/text()", &context).unwrap());
623
624 assert!(evaluate_condition("//theme", &context).unwrap());
626
627 assert!(!evaluate_condition("/nonexistent", &context).unwrap());
629 }
630}