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}")]
17 InvalidJsonPath(String),
18
19 #[error("Invalid XPath expression: {0}")]
21 InvalidXPath(String),
22
23 #[error("Invalid XML: {0}")]
25 InvalidXml(String),
26
27 #[error("Unsupported condition type: {0}")]
29 UnsupportedCondition(String),
30
31 #[error("Condition evaluation failed: {0}")]
33 EvaluationFailed(String),
34}
35
36#[derive(Debug, Clone)]
38pub struct ConditionContext {
39 pub request_body: Option<Value>,
41 pub response_body: Option<Value>,
43 pub request_xml: Option<String>,
45 pub response_xml: Option<String>,
47 pub headers: HashMap<String, String>,
49 pub query_params: HashMap<String, String>,
51 pub path: String,
53 pub method: String,
55 pub operation_id: Option<String>,
57 pub tags: Vec<String>,
59}
60
61impl Default for ConditionContext {
62 fn default() -> Self {
63 Self::new()
64 }
65}
66
67impl ConditionContext {
68 pub fn new() -> Self {
70 Self {
71 request_body: None,
72 response_body: None,
73 request_xml: None,
74 response_xml: None,
75 headers: HashMap::new(),
76 query_params: HashMap::new(),
77 path: String::new(),
78 method: String::new(),
79 operation_id: None,
80 tags: Vec::new(),
81 }
82 }
83
84 pub fn with_request_body(mut self, body: Value) -> Self {
86 self.request_body = Some(body);
87 self
88 }
89
90 pub fn with_response_body(mut self, body: Value) -> Self {
92 self.response_body = Some(body);
93 self
94 }
95
96 pub fn with_request_xml(mut self, xml: String) -> Self {
98 self.request_xml = Some(xml);
99 self
100 }
101
102 pub fn with_response_xml(mut self, xml: String) -> Self {
104 self.response_xml = Some(xml);
105 self
106 }
107
108 pub fn with_headers(mut self, headers: HashMap<String, String>) -> Self {
110 self.headers = headers;
111 self
112 }
113
114 pub fn with_query_params(mut self, params: HashMap<String, String>) -> Self {
116 self.query_params = params;
117 self
118 }
119
120 pub fn with_path(mut self, path: String) -> Self {
122 self.path = path;
123 self
124 }
125
126 pub fn with_method(mut self, method: String) -> Self {
128 self.method = method;
129 self
130 }
131
132 pub fn with_operation_id(mut self, operation_id: String) -> Self {
134 self.operation_id = Some(operation_id);
135 self
136 }
137
138 pub fn with_tags(mut self, tags: Vec<String>) -> Self {
140 self.tags = tags;
141 self
142 }
143}
144
145pub fn evaluate_condition(
147 condition: &str,
148 context: &ConditionContext,
149) -> Result<bool, ConditionError> {
150 let condition = condition.trim();
151
152 if condition.is_empty() {
153 return Ok(true); }
155
156 if let Some(and_conditions) = condition.strip_prefix("AND(") {
158 if let Some(inner) = and_conditions.strip_suffix(")") {
159 return evaluate_and_condition(inner, context);
160 }
161 }
162
163 if let Some(or_conditions) = condition.strip_prefix("OR(") {
164 if let Some(inner) = or_conditions.strip_suffix(")") {
165 return evaluate_or_condition(inner, context);
166 }
167 }
168
169 if let Some(not_condition) = condition.strip_prefix("NOT(") {
170 if let Some(inner) = not_condition.strip_suffix(")") {
171 return evaluate_not_condition(inner, context);
172 }
173 }
174
175 if condition.starts_with("$.") || condition.starts_with("$[") {
177 return evaluate_jsonpath(condition, context);
178 }
179
180 if condition.starts_with("/") || condition.starts_with("//") {
182 return evaluate_xpath(condition, context);
183 }
184
185 evaluate_simple_condition(condition, context)
187}
188
189fn evaluate_and_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(false);
199 }
200 }
201
202 Ok(true)
203}
204
205fn evaluate_or_condition(
207 conditions: &str,
208 context: &ConditionContext,
209) -> Result<bool, ConditionError> {
210 let parts: Vec<&str> = conditions.split(',').map(|s| s.trim()).collect();
211
212 for part in parts {
213 if evaluate_condition(part, context)? {
214 return Ok(true);
215 }
216 }
217
218 Ok(false)
219}
220
221fn evaluate_not_condition(
223 condition: &str,
224 context: &ConditionContext,
225) -> Result<bool, ConditionError> {
226 Ok(!evaluate_condition(condition, context)?)
227}
228
229fn evaluate_jsonpath(query: &str, context: &ConditionContext) -> Result<bool, ConditionError> {
231 let (_is_request, json_value) = if query.starts_with("$.request.") {
233 let _query = query.replace("$.request.", "$.");
234 (true, &context.request_body)
235 } else if query.starts_with("$.response.") {
236 let _query = query.replace("$.response.", "$.");
237 (false, &context.response_body)
238 } else {
239 if context.response_body.is_some() {
241 (false, &context.response_body)
242 } else {
243 (true, &context.request_body)
244 }
245 };
246
247 let Some(json_value) = json_value else {
248 return Ok(false); };
250
251 match Selector::new(query) {
252 Ok(selector) => {
253 let results: Vec<_> = selector.find(json_value).collect();
254 Ok(!results.is_empty())
255 }
256 Err(_) => Err(ConditionError::InvalidJsonPath(query.to_string())),
257 }
258}
259
260fn evaluate_xpath(query: &str, context: &ConditionContext) -> Result<bool, ConditionError> {
262 let (_is_request, xml_content) = if query.starts_with("/request/") {
264 let _query = query.replace("/request/", "/");
265 (true, &context.request_xml)
266 } else if query.starts_with("/response/") {
267 let _query = query.replace("/response/", "/");
268 (false, &context.response_xml)
269 } else {
270 (false, &context.response_xml)
272 };
273
274 let Some(xml_content) = xml_content else {
275 println!("Debug - No XML content available for query: {}", query);
276 return Ok(false); };
278
279 println!("Debug - Evaluating XPath '{}' against XML content: {}", query, xml_content);
280
281 match Document::parse(xml_content) {
282 Ok(doc) => {
283 let root = doc.root_element();
285 println!("Debug - XML root element: {}", root.tag_name().name());
286 let matches = evaluate_xpath_simple(&root, query);
287 println!("Debug - XPath result: {}", matches);
288 Ok(matches)
289 }
290 Err(e) => {
291 println!("Debug - Failed to parse XML: {}", e);
292 Err(ConditionError::InvalidXml(xml_content.clone()))
293 }
294 }
295}
296
297fn evaluate_xpath_simple(node: &Node, xpath: &str) -> bool {
299 if let Some(element_name) = xpath.strip_prefix("//") {
304 println!(
305 "Debug - Checking descendant-or-self for element '{}' on node '{}'",
306 element_name,
307 node.tag_name().name()
308 );
309 if node.tag_name().name() == element_name {
310 println!("Debug - Found match: {} == {}", node.tag_name().name(), element_name);
311 return true;
312 }
313 for child in node.children() {
315 if child.is_element() {
316 println!("Debug - Checking child element: {}", child.tag_name().name());
317 if evaluate_xpath_simple(&child, &format!("//{}", element_name)) {
318 return true;
319 }
320 }
321 }
322 return false; }
324
325 let xpath = xpath.trim_start_matches('/');
326
327 if xpath.is_empty() {
328 return true;
329 }
330
331 if let Some((element_part, attr_part)) = xpath.split_once('[') {
333 if let Some(attr_query) = attr_part.strip_suffix(']') {
334 if let Some((attr_name, attr_value)) = attr_query.split_once("='") {
335 if let Some(expected_value) = attr_value.strip_suffix('\'') {
336 if let Some(attr_val) = attr_name.strip_prefix('@') {
337 if node.tag_name().name() == element_part {
338 if let Some(attr) = node.attribute(attr_val) {
339 return attr == expected_value;
340 }
341 }
342 }
343 }
344 }
345 }
346 return false;
347 }
348
349 if let Some((element_name, rest)) = xpath.split_once('/') {
351 if node.tag_name().name() == element_name {
352 if rest.is_empty() {
353 return true;
354 }
355 for child in node.children() {
357 if child.is_element() && evaluate_xpath_simple(&child, rest) {
358 return true;
359 }
360 }
361 }
362 } else if node.tag_name().name() == xpath {
363 return true;
364 }
365
366 if let Some(text_query) = xpath.strip_suffix("/text()") {
368 if node.tag_name().name() == text_query {
369 return node.text().is_some_and(|t| !t.trim().is_empty());
370 }
371 }
372
373 false
374}
375
376fn evaluate_simple_condition(
378 condition: &str,
379 context: &ConditionContext,
380) -> Result<bool, ConditionError> {
381 if let Some(header_condition) = condition.strip_prefix("header[") {
383 if let Some((header_name, rest)) = header_condition.split_once("]") {
384 let header_name_lower = header_name.to_lowercase();
386 let rest_trimmed = rest.trim();
387 if let Some(expected_value) = rest_trimmed.strip_prefix("!=") {
389 let expected_value = expected_value.trim().trim_matches('\'').trim_matches('"');
390 if let Some(actual_value) = context.headers.get(&header_name_lower) {
391 return Ok(actual_value != expected_value);
393 }
394 return Ok(expected_value.is_empty());
397 }
398 if let Some(expected_value) = rest_trimmed.strip_prefix("=") {
400 let expected_value = expected_value.trim().trim_matches('\'').trim_matches('"');
401 if let Some(actual_value) = context.headers.get(&header_name_lower) {
402 return Ok(actual_value == expected_value);
403 }
404 return Ok(false);
405 }
406 }
407 }
408
409 if let Some(query_condition) = condition.strip_prefix("query[") {
411 if let Some((param_name, rest)) = query_condition.split_once("]") {
412 let rest_trimmed = rest.trim();
413 if let Some(expected_value) = rest_trimmed.strip_prefix("==") {
415 let expected_value = expected_value.trim().trim_matches('\'').trim_matches('"');
416 if let Some(actual_value) = context.query_params.get(param_name) {
417 return Ok(actual_value == expected_value);
418 }
419 return Ok(false);
420 }
421 if let Some(expected_value) = rest_trimmed.strip_prefix("=") {
423 let expected_value = expected_value.trim();
424 if let Some(actual_value) = context.query_params.get(param_name) {
425 return Ok(actual_value == expected_value);
426 }
427 return Ok(false);
428 }
429 }
430 }
431
432 if let Some(method_condition) = condition.strip_prefix("method=") {
434 return Ok(context.method == method_condition);
435 }
436
437 if let Some(path_condition) = condition.strip_prefix("path=") {
439 return Ok(context.path == path_condition);
440 }
441
442 if let Some(tag_condition) = condition.strip_prefix("has_tag[") {
444 if let Some(tag) = tag_condition.strip_suffix("]") {
445 return Ok(context.tags.contains(&tag.to_string()));
446 }
447 }
448
449 if let Some(op_condition) = condition.strip_prefix("operation=") {
451 if let Some(operation_id) = &context.operation_id {
452 return Ok(operation_id == op_condition);
453 }
454 return Ok(false);
455 }
456
457 Err(ConditionError::UnsupportedCondition(condition.to_string()))
458}
459
460#[cfg(test)]
461mod tests {
462 use super::*;
463 use serde_json::json;
464
465 #[test]
466 fn test_jsonpath_condition() {
467 let context = ConditionContext::new().with_response_body(json!({
468 "user": {
469 "name": "John",
470 "role": "admin"
471 },
472 "items": [1, 2, 3]
473 }));
474
475 assert!(evaluate_condition("$.user", &context).unwrap());
477
478 assert!(evaluate_condition("$.user.role", &context).unwrap());
480
481 assert!(evaluate_condition("$.items[0]", &context).unwrap());
483
484 assert!(!evaluate_condition("$.nonexistent", &context).unwrap());
486 }
487
488 #[test]
489 fn test_simple_conditions() {
490 let mut headers = HashMap::new();
491 headers.insert("authorization".to_string(), "Bearer token123".to_string());
492
493 let mut query_params = HashMap::new();
494 query_params.insert("limit".to_string(), "10".to_string());
495
496 let context = ConditionContext::new()
497 .with_headers(headers)
498 .with_query_params(query_params)
499 .with_method("POST".to_string())
500 .with_path("/api/users".to_string());
501
502 assert!(evaluate_condition("header[authorization]=Bearer token123", &context).unwrap());
504 assert!(!evaluate_condition("header[authorization]=Bearer wrong", &context).unwrap());
505
506 assert!(evaluate_condition("query[limit]=10", &context).unwrap());
508 assert!(!evaluate_condition("query[limit]=20", &context).unwrap());
509
510 assert!(evaluate_condition("method=POST", &context).unwrap());
512 assert!(!evaluate_condition("method=GET", &context).unwrap());
513
514 assert!(evaluate_condition("path=/api/users", &context).unwrap());
516 assert!(!evaluate_condition("path=/api/posts", &context).unwrap());
517 }
518
519 #[test]
520 fn test_logical_conditions() {
521 let context = ConditionContext::new()
522 .with_method("POST".to_string())
523 .with_path("/api/users".to_string());
524
525 assert!(evaluate_condition("AND(method=POST,path=/api/users)", &context).unwrap());
527 assert!(!evaluate_condition("AND(method=GET,path=/api/users)", &context).unwrap());
528
529 assert!(evaluate_condition("OR(method=POST,path=/api/posts)", &context).unwrap());
531 assert!(!evaluate_condition("OR(method=GET,path=/api/posts)", &context).unwrap());
532
533 assert!(!evaluate_condition("NOT(method=POST)", &context).unwrap());
535 assert!(evaluate_condition("NOT(method=GET)", &context).unwrap());
536 }
537
538 #[test]
539 fn test_xpath_condition() {
540 let xml_content = r#"
541 <user id="123">
542 <name>John Doe</name>
543 <role>admin</role>
544 <preferences>
545 <theme>dark</theme>
546 <notifications>true</notifications>
547 </preferences>
548 </user>
549 "#;
550
551 let context = ConditionContext::new().with_response_xml(xml_content.to_string());
552
553 assert!(evaluate_condition("/user", &context).unwrap());
555
556 assert!(evaluate_condition("/user/name", &context).unwrap());
558
559 assert!(evaluate_condition("/user[@id='123']", &context).unwrap());
561 assert!(!evaluate_condition("/user[@id='456']", &context).unwrap());
562
563 assert!(evaluate_condition("/user/name/text()", &context).unwrap());
565
566 assert!(evaluate_condition("//theme", &context).unwrap());
568
569 assert!(!evaluate_condition("/nonexistent", &context).unwrap());
571 }
572}