use anyhow::Result;
use serde_json::Value;
use std::collections::HashMap;
use crate::plugins::PluginManager;
use jaq_core::{Compiler, Ctx, Vars, data, load, unwrap_valr};
use jaq_json::Val as JaqVal;
use super::operators;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AssertionResult {
Pass,
Fail {
message: String,
expected: Option<String>,
actual: Option<String>,
},
Error(String),
}
impl AssertionResult {
pub fn fail(message: impl Into<String>) -> Self {
Self::Fail {
message: message.into(),
expected: None,
actual: None,
}
}
pub fn fail_with_diff(
message: impl Into<String>,
expected: impl Into<String>,
actual: impl Into<String>,
) -> Self {
Self::Fail {
message: message.into(),
expected: Some(expected.into()),
actual: Some(actual.into()),
}
}
}
pub struct AssertionEngine {
plugin_manager: PluginManager,
}
impl AssertionEngine {
pub fn new() -> Self {
Self {
plugin_manager: PluginManager::new(),
}
}
pub fn evaluate(
&self,
assertion: &str,
response: &Value,
headers: Option<&HashMap<String, String>>,
trailers: Option<&HashMap<String, String>>,
) -> Result<AssertionResult> {
let trimmed = assertion.trim();
match operators::evaluate_assertion(
&self.plugin_manager,
trimmed,
response,
headers,
trailers,
) {
Ok(AssertionResult::Error(msg)) if msg.starts_with("Unsupported assertion syntax") => {
self.evaluate_jaq(trimmed, response)
}
other => other,
}
}
pub fn query(&self, expr: &str, input: &Value) -> Result<Vec<Value>> {
let values = self.run_jaq(expr, input)?;
values
.into_iter()
.map(|val| {
serde_json::from_str(&val.to_string())
.map_err(|e| anyhow::anyhow!("JQ output conversion error: {}", e))
})
.collect()
}
fn evaluate_jaq(&self, expr: &str, response: &Value) -> Result<AssertionResult> {
let out = match self.run_jaq(expr, response) {
Ok(out) => out,
Err(e) => return Ok(AssertionResult::Error(format!("JQ Parse Error: {}", e))),
};
let mut passed = false;
let mut seen_false = false;
for val in out {
if matches!(val, JaqVal::Bool(true)) {
passed = true;
} else {
seen_false = true;
}
}
if seen_false {
return Ok(AssertionResult::fail(format!(
"JQ assertion evaluated to false: {}",
expr
)));
}
if passed {
Ok(AssertionResult::Pass)
} else {
Ok(AssertionResult::fail(format!(
"JQ assertion produced no output (falsey): {}",
expr
)))
}
}
fn run_jaq(&self, expr: &str, input: &Value) -> Result<Vec<JaqVal>> {
use jaq_core::defs as core_defs;
use jaq_core::funs as core_funs;
let arena = load::Arena::default();
let defs = core_defs().chain(jaq_std::defs()).chain(jaq_json::defs());
let funs = core_funs().chain(jaq_std::funs()).chain(jaq_json::funs());
let loader = load::Loader::new(defs);
let program = load::File {
code: expr,
path: (),
};
let modules = loader
.load(&arena, program)
.map_err(|errs| anyhow::anyhow!("Failed to parse JQ expression: {:?}", errs))?;
let filter = Compiler::default()
.with_funs(funs)
.compile(modules)
.map_err(|errs| anyhow::anyhow!("Failed to compile JQ expression: {:?}", errs))?;
let input_json = serde_json::to_string(input)
.map_err(|e| anyhow::anyhow!("Failed to serialize input for JQ: {}", e))?;
let input: JaqVal = jaq_json::read::parse_single(input_json.as_bytes())
.map_err(|e| anyhow::anyhow!("Failed to parse input for JQ: {}", e))?;
let ctx = Ctx::<data::JustLut<JaqVal>>::new(&filter.lut, Vars::new([]));
let out = filter.id.run((ctx, input)).map(unwrap_valr);
let mut values = Vec::new();
for item in out {
match item {
Ok(v) => values.push(v),
Err(e) => return Err(anyhow::anyhow!("JQ Runtime Error: {}", e)),
}
}
Ok(values)
}
pub fn has_failures(&self, results: &[AssertionResult]) -> bool {
results
.iter()
.any(|r| matches!(r, AssertionResult::Fail { .. } | AssertionResult::Error(_)))
}
pub fn get_failures<'a>(&self, results: &'a [AssertionResult]) -> Vec<&'a AssertionResult> {
results
.iter()
.filter(|r| matches!(r, AssertionResult::Fail { .. } | AssertionResult::Error(_)))
.collect()
}
pub fn evaluate_all(
&self,
assertions: &[String],
response: &serde_json::Value,
headers: Option<&HashMap<String, String>>,
trailers: Option<&HashMap<String, String>>,
) -> Vec<AssertionResult> {
assertions
.iter()
.map(|assertion| {
self.evaluate(assertion, response, headers, trailers)
.unwrap_or_else(|e| AssertionResult::Error(format!("Internal error: {}", e)))
})
.collect()
}
}
impl Default for AssertionEngine {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn create_test_response() -> Value {
json!({
"id": 123,
"name": "test",
"email": "test@example.com",
"active": true,
"tags": ["a", "b", "c"],
"nested": {
"value": 42
}
})
}
#[test]
fn test_assertion_engine_new() {
let engine = AssertionEngine::new();
assert!(engine.plugin_manager.get("uuid").is_some());
assert!(engine.plugin_manager.get("email").is_some());
}
#[test]
fn test_assertion_engine_default() {
let engine = AssertionEngine::default();
assert!(engine.plugin_manager.get("uuid").is_some());
}
#[test]
fn test_assertion_result_fail() {
let result = AssertionResult::fail("test message");
if let AssertionResult::Fail { message, .. } = result {
assert_eq!(message, "test message");
} else {
panic!("Expected Fail result");
}
}
#[test]
fn test_assertion_result_fail_with_diff() {
let result = AssertionResult::fail_with_diff("mismatch", "expected", "actual");
if let AssertionResult::Fail {
message,
expected,
actual,
} = result
{
assert_eq!(message, "mismatch");
assert_eq!(expected, Some("expected".to_string()));
assert_eq!(actual, Some("actual".to_string()));
} else {
panic!("Expected Fail result");
}
}
#[test]
fn test_assertion_result_debug() {
let result = AssertionResult::Pass;
let debug_str = format!("{:?}", result);
assert!(debug_str.contains("Pass"));
}
#[test]
fn test_evaluate_plugin_function() {
let engine = AssertionEngine::new();
let response = create_test_response();
let result = engine
.evaluate("@email(.email)", &response, None, None)
.unwrap();
if let AssertionResult::Pass = result {
} else {
panic!("Expected Pass for valid email");
}
}
#[test]
fn test_evaluate_plugin_function_invalid() {
let engine = AssertionEngine::new();
let response = json!({"email": "not-an-email"});
let result = engine
.evaluate("@email(.email)", &response, None, None)
.unwrap();
if let AssertionResult::Fail { .. } = result {
} else {
panic!("Expected Fail for invalid email");
}
}
#[test]
fn test_evaluate_empty_plugin() {
let engine = AssertionEngine::new();
let response = json!({"tags": []});
let result = engine
.evaluate("@empty(.tags)", &response, None, None)
.unwrap();
if let AssertionResult::Pass = result {
} else {
panic!("Expected Pass for empty value");
}
}
#[test]
fn test_evaluate_equality_operator() {
let engine = AssertionEngine::new();
let response = create_test_response();
let result = engine
.evaluate(".id == 123", &response, None, None)
.unwrap();
if let AssertionResult::Pass = result {
} else {
panic!("Expected Pass for equality check");
}
}
#[test]
fn test_evaluate_equality_operator_fail() {
let engine = AssertionEngine::new();
let response = create_test_response();
let result = engine
.evaluate(".id == 456", &response, None, None)
.unwrap();
if let AssertionResult::Fail { .. } = result {
} else {
panic!("Expected Fail for equality check");
}
}
#[test]
fn test_evaluate_inequality_operator() {
let engine = AssertionEngine::new();
let response = create_test_response();
let result = engine
.evaluate(".id != 456", &response, None, None)
.unwrap();
if let AssertionResult::Pass = result {
} else {
panic!("Expected Pass for inequality check");
}
}
#[test]
fn test_evaluate_contains_operator() {
let engine = AssertionEngine::new();
let response = create_test_response();
let result = engine
.evaluate(".name contains \"test\"", &response, None, None)
.unwrap();
if let AssertionResult::Pass = result {
} else {
panic!("Expected Pass for contains check");
}
}
#[test]
fn test_evaluate_contains_operator_array() {
let engine = AssertionEngine::new();
let response = create_test_response();
let result = engine
.evaluate(".tags contains \"a\"", &response, None, None)
.unwrap();
if let AssertionResult::Pass = result {
} else {
panic!("Expected Pass for array contains check");
}
}
#[test]
fn test_evaluate_starts_with_operator() {
let engine = AssertionEngine::new();
let response = create_test_response();
let result = engine
.evaluate(".name startsWith \"te\"", &response, None, None)
.unwrap();
if let AssertionResult::Pass = result {
} else {
panic!("Expected Pass for startsWith check");
}
}
#[test]
fn test_evaluate_ends_with_operator() {
let engine = AssertionEngine::new();
let response = create_test_response();
let result = engine
.evaluate(".name endsWith \"st\"", &response, None, None)
.unwrap();
if let AssertionResult::Pass = result {
} else {
panic!("Expected Pass for endsWith check");
}
}
#[test]
fn test_evaluate_numeric_greater_than() {
let engine = AssertionEngine::new();
let response = create_test_response();
let result = engine.evaluate(".id > 100", &response, None, None).unwrap();
if let AssertionResult::Pass = result {
} else {
panic!("Expected Pass for greater than check");
}
}
#[test]
fn test_evaluate_numeric_less_than() {
let engine = AssertionEngine::new();
let response = create_test_response();
let result = engine.evaluate(".id < 200", &response, None, None).unwrap();
if let AssertionResult::Pass = result {
} else {
panic!("Expected Pass for less than check");
}
}
#[test]
fn test_evaluate_numeric_gte() {
let engine = AssertionEngine::new();
let response = create_test_response();
let result = engine
.evaluate(".id >= 123", &response, None, None)
.unwrap();
if let AssertionResult::Pass = result {
} else {
panic!("Expected Pass for gte check");
}
}
#[test]
fn test_evaluate_numeric_lte() {
let engine = AssertionEngine::new();
let response = create_test_response();
let result = engine
.evaluate(".id <= 123", &response, None, None)
.unwrap();
if let AssertionResult::Pass = result {
} else {
panic!("Expected Pass for lte check");
}
}
#[test]
fn test_evaluate_matches_regex() {
let engine = AssertionEngine::new();
let response = create_test_response();
let result = engine
.evaluate(".name matches \"^te.*t$\"", &response, None, None)
.unwrap();
if let AssertionResult::Pass = result {
} else {
panic!("Expected Pass for regex match");
}
}
#[test]
fn test_evaluate_matches_regex_fail() {
let engine = AssertionEngine::new();
let response = create_test_response();
let result = engine
.evaluate(".name matches \"^xyz\"", &response, None, None)
.unwrap();
if let AssertionResult::Fail { .. } = result {
} else {
panic!("Expected Fail for regex match");
}
}
#[test]
fn test_evaluate_header_plugin() {
let engine = AssertionEngine::new();
let response = create_test_response();
let mut headers = HashMap::new();
headers.insert("content-type".to_string(), "application/json".to_string());
let result = engine
.evaluate("@header(\"content-type\")", &response, Some(&headers), None)
.unwrap();
if let AssertionResult::Pass = result {
} else {
panic!("Expected Pass for header check");
}
}
#[test]
fn test_evaluate_trailer_plugin() {
let engine = AssertionEngine::new();
let response = create_test_response();
let mut trailers = HashMap::new();
trailers.insert("x-custom".to_string(), "value".to_string());
let result = engine
.evaluate("@trailer(\"x-custom\")", &response, None, Some(&trailers))
.unwrap();
if let AssertionResult::Pass = result {
} else {
panic!("Expected Pass for trailer check");
}
}
#[test]
fn test_evaluate_nested_path() {
let engine = AssertionEngine::new();
let response = create_test_response();
let result = engine
.evaluate(".nested.value == 42", &response, None, None)
.unwrap();
if let AssertionResult::Pass = result {
} else {
panic!("Expected Pass for nested path check");
}
}
#[test]
fn test_evaluate_boolean_path() {
let engine = AssertionEngine::new();
let response = create_test_response();
let result = engine
.evaluate(".active == true", &response, None, None)
.unwrap();
if let AssertionResult::Pass = result {
} else {
panic!("Expected Pass for boolean check");
}
}
#[test]
fn test_evaluate_array_index() {
let engine = AssertionEngine::new();
let response = create_test_response();
let result = engine
.evaluate(".tags[0] == \"a\"", &response, None, None)
.unwrap();
if let AssertionResult::Pass = result {
} else {
panic!("Expected Pass for array index check");
}
}
#[test]
fn test_evaluate_unsupported_syntax() {
let engine = AssertionEngine::new();
let response = create_test_response();
let result = engine.evaluate("some_unknown_function()", &response, None, None);
assert!(result.is_ok());
}
#[test]
fn test_evaluate_all() {
let engine = AssertionEngine::new();
let response = create_test_response();
let assertions = vec![".id == 123".to_string(), ".name == \"test\"".to_string()];
let results = engine.evaluate_all(&assertions, &response, None, None);
assert_eq!(results.len(), 2);
assert!(results.iter().all(|r| matches!(r, AssertionResult::Pass)));
}
#[test]
fn test_evaluate_all_with_failure() {
let engine = AssertionEngine::new();
let response = create_test_response();
let assertions = vec![".id == 123".to_string(), ".id == 999".to_string()];
let results = engine.evaluate_all(&assertions, &response, None, None);
assert_eq!(results.len(), 2);
assert!(matches!(&results[0], AssertionResult::Pass));
assert!(matches!(&results[1], AssertionResult::Fail { .. }));
}
#[test]
fn test_query_jq_simple() {
let engine = AssertionEngine::new();
let response = create_test_response();
let results = engine.query(".id", &response).unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0], json!(123));
}
#[test]
fn test_query_jq_nested() {
let engine = AssertionEngine::new();
let response = create_test_response();
let results = engine.query(".nested.value", &response).unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0], json!(42));
}
#[test]
fn test_query_jq_array() {
let engine = AssertionEngine::new();
let response = create_test_response();
let results = engine.query(".tags[]", &response).unwrap();
assert_eq!(results.len(), 3);
assert_eq!(results[0], json!("a"));
assert_eq!(results[1], json!("b"));
assert_eq!(results[2], json!("c"));
}
#[test]
fn test_query_jq_filter() {
let engine = AssertionEngine::new();
let response = json!([1, 2, 3, 4, 5]);
let results = engine.query(".[] | select(. > 3)", &response).unwrap();
assert_eq!(results.len(), 2);
assert_eq!(results[0], json!(4));
assert_eq!(results[1], json!(5));
}
#[test]
fn test_query_jq_length() {
let engine = AssertionEngine::new();
let response = create_test_response();
let results = engine.query(".tags | length", &response).unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0], json!(3));
}
#[test]
fn test_query_invalid_expression() {
let engine = AssertionEngine::new();
let response = create_test_response();
let results = engine.query("invalid[[[", &response);
assert!(results.is_err());
}
}