use jsonpath::Selector;
use roxmltree::{Document, Node};
use serde_json::Value;
use std::collections::HashMap;
use thiserror::Error;
use tracing::debug;
#[derive(Debug, Error)]
pub enum ConditionError {
#[error("Invalid JSONPath expression: {0}")]
InvalidJsonPath(String),
#[error("Invalid XPath expression: {0}")]
InvalidXPath(String),
#[error("Invalid XML: {0}")]
InvalidXml(String),
#[error("Unsupported condition type: {0}")]
UnsupportedCondition(String),
#[error("Condition evaluation failed: {0}")]
EvaluationFailed(String),
}
#[derive(Debug, Clone)]
pub struct ConditionContext {
pub request_body: Option<Value>,
pub response_body: Option<Value>,
pub request_xml: Option<String>,
pub response_xml: Option<String>,
pub headers: HashMap<String, String>,
pub query_params: HashMap<String, String>,
pub path: String,
pub method: String,
pub operation_id: Option<String>,
pub tags: Vec<String>,
}
impl Default for ConditionContext {
fn default() -> Self {
Self::new()
}
}
impl ConditionContext {
pub fn new() -> Self {
Self {
request_body: None,
response_body: None,
request_xml: None,
response_xml: None,
headers: HashMap::new(),
query_params: HashMap::new(),
path: String::new(),
method: String::new(),
operation_id: None,
tags: Vec::new(),
}
}
pub fn with_request_body(mut self, body: Value) -> Self {
self.request_body = Some(body);
self
}
pub fn with_response_body(mut self, body: Value) -> Self {
self.response_body = Some(body);
self
}
pub fn with_request_xml(mut self, xml: String) -> Self {
self.request_xml = Some(xml);
self
}
pub fn with_response_xml(mut self, xml: String) -> Self {
self.response_xml = Some(xml);
self
}
pub fn with_headers(mut self, headers: HashMap<String, String>) -> Self {
self.headers = headers;
self
}
pub fn with_query_params(mut self, params: HashMap<String, String>) -> Self {
self.query_params = params;
self
}
pub fn with_path(mut self, path: String) -> Self {
self.path = path;
self
}
pub fn with_method(mut self, method: String) -> Self {
self.method = method;
self
}
pub fn with_operation_id(mut self, operation_id: String) -> Self {
self.operation_id = Some(operation_id);
self
}
pub fn with_tags(mut self, tags: Vec<String>) -> Self {
self.tags = tags;
self
}
}
pub fn evaluate_condition(
condition: &str,
context: &ConditionContext,
) -> Result<bool, ConditionError> {
let condition = condition.trim();
if condition.is_empty() {
return Ok(true); }
if let Some(and_conditions) = condition.strip_prefix("AND(") {
if let Some(inner) = and_conditions.strip_suffix(")") {
return evaluate_and_condition(inner, context);
}
}
if let Some(or_conditions) = condition.strip_prefix("OR(") {
if let Some(inner) = or_conditions.strip_suffix(")") {
return evaluate_or_condition(inner, context);
}
}
if let Some(not_condition) = condition.strip_prefix("NOT(") {
if let Some(inner) = not_condition.strip_suffix(")") {
return evaluate_not_condition(inner, context);
}
}
if condition.starts_with("$.") || condition.starts_with("$[") {
return evaluate_jsonpath(condition, context);
}
if condition.starts_with("/") || condition.starts_with("//") {
return evaluate_xpath(condition, context);
}
evaluate_simple_condition(condition, context)
}
fn evaluate_and_condition(
conditions: &str,
context: &ConditionContext,
) -> Result<bool, ConditionError> {
let parts: Vec<&str> = conditions.split(',').map(|s| s.trim()).collect();
for part in parts {
if !evaluate_condition(part, context)? {
return Ok(false);
}
}
Ok(true)
}
fn evaluate_or_condition(
conditions: &str,
context: &ConditionContext,
) -> Result<bool, ConditionError> {
let parts: Vec<&str> = conditions.split(',').map(|s| s.trim()).collect();
for part in parts {
if evaluate_condition(part, context)? {
return Ok(true);
}
}
Ok(false)
}
fn evaluate_not_condition(
condition: &str,
context: &ConditionContext,
) -> Result<bool, ConditionError> {
Ok(!evaluate_condition(condition, context)?)
}
fn evaluate_jsonpath(query: &str, context: &ConditionContext) -> Result<bool, ConditionError> {
let (jsonpath_expr, comparison_op, expected_value) =
if let Some((path, value)) = query.split_once("==") {
let path = path.trim();
let value = value.trim().trim_matches('\'').trim_matches('"');
(path, Some("=="), Some(value))
} else if let Some((path, value)) = query.split_once("!=") {
let path = path.trim();
let value = value.trim().trim_matches('\'').trim_matches('"');
(path, Some("!="), Some(value))
} else {
(query, None, None)
};
let (_is_request, json_value) = if jsonpath_expr.starts_with("$.request.") {
let _query = jsonpath_expr.replace("$.request.", "$.");
(true, &context.request_body)
} else if jsonpath_expr.starts_with("$.response.") {
let _query = jsonpath_expr.replace("$.response.", "$.");
(false, &context.response_body)
} else {
if context.response_body.is_some() {
(false, &context.response_body)
} else {
(true, &context.request_body)
}
};
let Some(json_value) = json_value else {
return Ok(false); };
match Selector::new(jsonpath_expr) {
Ok(selector) => {
let results: Vec<_> = selector.find(json_value).collect();
if let (Some(op), Some(expected)) = (comparison_op, expected_value) {
if results.is_empty() {
return Ok(false);
}
let actual_value = match &results[0] {
Value::String(s) => s.as_str(),
Value::Number(n) => {
return Ok(match op {
"==" => n.to_string() == expected,
"!=" => n.to_string() != expected,
_ => false,
})
}
Value::Bool(b) => {
return Ok(match op {
"==" => b.to_string() == expected,
"!=" => b.to_string() != expected,
_ => false,
})
}
Value::Null => {
return Ok(match op {
"==" => expected == "null",
"!=" => expected != "null",
_ => false,
})
}
_ => return Ok(false),
};
return Ok(match op {
"==" => actual_value == expected,
"!=" => actual_value != expected,
_ => false,
});
}
Ok(!results.is_empty())
}
Err(_) => Err(ConditionError::InvalidJsonPath(jsonpath_expr.to_string())),
}
}
fn evaluate_xpath(query: &str, context: &ConditionContext) -> Result<bool, ConditionError> {
let (_is_request, xml_content) = if query.starts_with("/request/") {
let _query = query.replace("/request/", "/");
(true, &context.request_xml)
} else if query.starts_with("/response/") {
let _query = query.replace("/response/", "/");
(false, &context.response_xml)
} else {
(false, &context.response_xml)
};
let Some(xml_content) = xml_content else {
debug!("No XML content available for query: {}", query);
return Ok(false); };
debug!("Evaluating XPath '{}' against XML content: {}", query, xml_content);
match Document::parse(xml_content) {
Ok(doc) => {
let root = doc.root_element();
debug!("XML root element: {}", root.tag_name().name());
let matches = evaluate_xpath_simple(&root, query);
debug!("XPath result: {}", matches);
Ok(matches)
}
Err(e) => {
debug!("Failed to parse XML: {}", e);
Err(ConditionError::InvalidXml(xml_content.clone()))
}
}
}
fn evaluate_xpath_simple(node: &Node, xpath: &str) -> bool {
if let Some(element_name) = xpath.strip_prefix("//") {
debug!(
"Checking descendant-or-self for element '{}' on node '{}'",
element_name,
node.tag_name().name()
);
if node.tag_name().name() == element_name {
debug!("Found match: {} == {}", node.tag_name().name(), element_name);
return true;
}
for child in node.children() {
if child.is_element() {
debug!("Checking child element: {}", child.tag_name().name());
if evaluate_xpath_simple(&child, &format!("//{}", element_name)) {
return true;
}
}
}
return false; }
let xpath = xpath.trim_start_matches('/');
if xpath.is_empty() {
return true;
}
if let Some((element_part, attr_part)) = xpath.split_once('[') {
if let Some(attr_query) = attr_part.strip_suffix(']') {
if let Some((attr_name, attr_value)) = attr_query.split_once("='") {
if let Some(expected_value) = attr_value.strip_suffix('\'') {
if let Some(attr_val) = attr_name.strip_prefix('@') {
if node.tag_name().name() == element_part {
if let Some(attr) = node.attribute(attr_val) {
return attr == expected_value;
}
}
}
}
}
}
return false;
}
if let Some((element_name, rest)) = xpath.split_once('/') {
if node.tag_name().name() == element_name {
if rest.is_empty() {
return true;
}
for child in node.children() {
if child.is_element() && evaluate_xpath_simple(&child, rest) {
return true;
}
}
}
} else if node.tag_name().name() == xpath {
return true;
}
if let Some(text_query) = xpath.strip_suffix("/text()") {
if node.tag_name().name() == text_query {
return node.text().is_some_and(|t| !t.trim().is_empty());
}
}
false
}
fn evaluate_simple_condition(
condition: &str,
context: &ConditionContext,
) -> Result<bool, ConditionError> {
if let Some(header_condition) = condition.strip_prefix("header[") {
if let Some((header_name, rest)) = header_condition.split_once("]") {
let header_name_lower = header_name.to_lowercase();
let rest_trimmed = rest.trim();
if let Some(expected_value) = rest_trimmed.strip_prefix("!=") {
let expected_value = expected_value.trim().trim_matches('\'').trim_matches('"');
if let Some(actual_value) = context.headers.get(&header_name_lower) {
return Ok(actual_value != expected_value);
}
return Ok(!expected_value.is_empty());
}
if let Some(expected_value) = rest_trimmed.strip_prefix("=") {
let expected_value = expected_value.trim().trim_matches('\'').trim_matches('"');
if let Some(actual_value) = context.headers.get(&header_name_lower) {
return Ok(actual_value == expected_value);
}
return Ok(false);
}
}
}
if let Some(query_condition) = condition.strip_prefix("query[") {
if let Some((param_name, rest)) = query_condition.split_once("]") {
let rest_trimmed = rest.trim();
if let Some(expected_value) = rest_trimmed.strip_prefix("==") {
let expected_value = expected_value.trim().trim_matches('\'').trim_matches('"');
if let Some(actual_value) = context.query_params.get(param_name) {
return Ok(actual_value == expected_value);
}
return Ok(false);
}
if let Some(expected_value) = rest_trimmed.strip_prefix("=") {
let expected_value = expected_value.trim();
if let Some(actual_value) = context.query_params.get(param_name) {
return Ok(actual_value == expected_value);
}
return Ok(false);
}
}
}
if let Some(method_condition) = condition.strip_prefix("method=") {
return Ok(context.method == method_condition);
}
if let Some(path_condition) = condition.strip_prefix("path=") {
return Ok(context.path == path_condition);
}
if let Some(tag_condition) = condition.strip_prefix("has_tag[") {
if let Some(tag) = tag_condition.strip_suffix("]") {
return Ok(context.tags.contains(&tag.to_string()));
}
}
if let Some(op_condition) = condition.strip_prefix("operation=") {
if let Some(operation_id) = &context.operation_id {
return Ok(operation_id == op_condition);
}
return Ok(false);
}
Err(ConditionError::UnsupportedCondition(condition.to_string()))
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_jsonpath_condition() {
let context = ConditionContext::new().with_response_body(json!({
"user": {
"name": "John",
"role": "admin"
},
"items": [1, 2, 3]
}));
assert!(evaluate_condition("$.user", &context).unwrap());
assert!(evaluate_condition("$.user.role", &context).unwrap());
assert!(evaluate_condition("$.items[0]", &context).unwrap());
assert!(!evaluate_condition("$.nonexistent", &context).unwrap());
}
#[test]
fn test_simple_conditions() {
let mut headers = HashMap::new();
headers.insert("authorization".to_string(), "Bearer token123".to_string());
let mut query_params = HashMap::new();
query_params.insert("limit".to_string(), "10".to_string());
let context = ConditionContext::new()
.with_headers(headers)
.with_query_params(query_params)
.with_method("POST".to_string())
.with_path("/api/users".to_string());
assert!(evaluate_condition("header[authorization]=Bearer token123", &context).unwrap());
assert!(!evaluate_condition("header[authorization]=Bearer wrong", &context).unwrap());
assert!(evaluate_condition("query[limit]=10", &context).unwrap());
assert!(!evaluate_condition("query[limit]=20", &context).unwrap());
assert!(evaluate_condition("method=POST", &context).unwrap());
assert!(!evaluate_condition("method=GET", &context).unwrap());
assert!(evaluate_condition("path=/api/users", &context).unwrap());
assert!(!evaluate_condition("path=/api/posts", &context).unwrap());
}
#[test]
fn test_logical_conditions() {
let context = ConditionContext::new()
.with_method("POST".to_string())
.with_path("/api/users".to_string());
assert!(evaluate_condition("AND(method=POST,path=/api/users)", &context).unwrap());
assert!(!evaluate_condition("AND(method=GET,path=/api/users)", &context).unwrap());
assert!(evaluate_condition("OR(method=POST,path=/api/posts)", &context).unwrap());
assert!(!evaluate_condition("OR(method=GET,path=/api/posts)", &context).unwrap());
assert!(!evaluate_condition("NOT(method=POST)", &context).unwrap());
assert!(evaluate_condition("NOT(method=GET)", &context).unwrap());
}
#[test]
fn test_xpath_condition() {
let xml_content = r#"
<user id="123">
<name>John Doe</name>
<role>admin</role>
<preferences>
<theme>dark</theme>
<notifications>true</notifications>
</preferences>
</user>
"#;
let context = ConditionContext::new().with_response_xml(xml_content.to_string());
assert!(evaluate_condition("/user", &context).unwrap());
assert!(evaluate_condition("/user/name", &context).unwrap());
assert!(evaluate_condition("/user[@id='123']", &context).unwrap());
assert!(!evaluate_condition("/user[@id='456']", &context).unwrap());
assert!(evaluate_condition("/user/name/text()", &context).unwrap());
assert!(evaluate_condition("//theme", &context).unwrap());
assert!(!evaluate_condition("/nonexistent", &context).unwrap());
}
}