use jmespath::{Rcvar, Runtime, Variable};
use serde::Deserialize;
use serde_json::Value;
use std::rc::Rc;
#[derive(Debug, Deserialize)]
struct TestCase {
expression: String,
#[serde(default)]
result: Option<Value>,
#[serde(default)]
error: Option<String>,
#[serde(default)]
bench: Option<String>,
}
#[derive(Debug, Deserialize)]
struct TestSuite {
given: Value,
cases: Vec<TestCase>,
}
fn values_equal(a: &Value, b: &Value) -> bool {
match (a, b) {
(Value::Number(na), Value::Number(nb)) => {
let fa = na.as_f64().unwrap_or(f64::NAN);
let fb = nb.as_f64().unwrap_or(f64::NAN);
(fa - fb).abs() < 1e-10
}
(Value::Array(aa), Value::Array(ab)) => {
aa.len() == ab.len() && aa.iter().zip(ab.iter()).all(|(x, y)| values_equal(x, y))
}
(Value::Object(oa), Value::Object(ob)) => {
oa.len() == ob.len()
&& oa
.iter()
.all(|(k, v)| ob.get(k).is_some_and(|v2| values_equal(v, v2)))
}
_ => a == b,
}
}
fn create_runtime() -> Runtime {
let mut runtime = Runtime::new();
runtime.register_builtin_functions();
jmespath_extensions::register_all(&mut runtime);
runtime
}
fn run_test_case(
runtime: &Runtime,
given: &Value,
case: &TestCase,
suite_name: &str,
case_idx: usize,
) {
if case.bench.is_some() {
return;
}
let given_var = serde_json::from_value::<Variable>(given.clone())
.expect("Failed to convert given data to Variable");
let given_rc: Rcvar = Rc::new(given_var);
let compile_result = runtime.compile(&case.expression);
match (&case.error, &case.result) {
(Some(error_type), None) => {
match error_type.as_str() {
"syntax" => {
assert!(
compile_result.is_err(),
"[{}:{}] Expected syntax error for '{}', but it parsed successfully",
suite_name,
case_idx,
case.expression
);
}
"invalid-type" | "invalid-value" | "invalid-arity" | "unknown-function" => {
match compile_result {
Err(e) => {
let _ = e;
}
Ok(expr) => {
let search_result = expr.search(given_rc);
assert!(
search_result.is_err(),
"[{}:{}] Expected {} error for '{}', but got result: {:?}",
suite_name,
case_idx,
error_type,
case.expression,
search_result
);
}
}
}
other => {
panic!(
"[{}:{}] Unknown error type: {}",
suite_name, case_idx, other
);
}
}
}
(None, Some(expected)) => {
let expr = compile_result.unwrap_or_else(|e| {
panic!(
"[{}:{}] Failed to compile '{}': {}",
suite_name, case_idx, case.expression, e
)
});
let result = expr.search(given_rc).unwrap_or_else(|e| {
panic!(
"[{}:{}] Failed to evaluate '{}': {}",
suite_name, case_idx, case.expression, e
)
});
let result_json: Value =
serde_json::to_value(&*result).expect("Failed to convert result to JSON");
assert!(
values_equal(&result_json, expected),
"[{}:{}] Expression '{}' returned {:?}, expected {:?}",
suite_name,
case_idx,
case.expression,
result_json,
expected
);
}
(Some(_), Some(_)) => {
panic!(
"[{}:{}] Test case has both error and result",
suite_name, case_idx
);
}
(None, None) => {
}
}
}
fn run_suite(json_content: &str, suite_name: &str) {
let runtime = create_runtime();
let suites: Vec<TestSuite> = serde_json::from_str(json_content)
.unwrap_or_else(|e| panic!("Failed to parse {}: {}", suite_name, e));
for (suite_idx, suite) in suites.iter().enumerate() {
for (case_idx, case) in suite.cases.iter().enumerate() {
run_test_case(
&runtime,
&suite.given,
case,
&format!("{}[{}]", suite_name, suite_idx),
case_idx,
);
}
}
}
const BASIC_JSON: &str = include_str!("compliance/basic.json");
const BOOLEAN_JSON: &str = include_str!("compliance/boolean.json");
const CURRENT_JSON: &str = include_str!("compliance/current.json");
const ESCAPE_JSON: &str = include_str!("compliance/escape.json");
const FILTERS_JSON: &str = include_str!("compliance/filters.json");
const FUNCTIONS_JSON: &str = include_str!("compliance/functions.json");
const IDENTIFIERS_JSON: &str = include_str!("compliance/identifiers.json");
const INDICES_JSON: &str = include_str!("compliance/indices.json");
const LITERAL_JSON: &str = include_str!("compliance/literal.json");
const MULTISELECT_JSON: &str = include_str!("compliance/multiselect.json");
const PIPE_JSON: &str = include_str!("compliance/pipe.json");
const SLICE_JSON: &str = include_str!("compliance/slice.json");
const SYNTAX_JSON: &str = include_str!("compliance/syntax.json");
const UNICODE_JSON: &str = include_str!("compliance/unicode.json");
const WILDCARD_JSON: &str = include_str!("compliance/wildcard.json");
#[test]
fn compliance_basic() {
run_suite(BASIC_JSON, "basic");
}
#[test]
fn compliance_boolean() {
run_suite(BOOLEAN_JSON, "boolean");
}
#[test]
fn compliance_current() {
run_suite(CURRENT_JSON, "current");
}
#[test]
fn compliance_escape() {
run_suite(ESCAPE_JSON, "escape");
}
#[test]
fn compliance_filters() {
run_suite(FILTERS_JSON, "filters");
}
#[test]
fn compliance_functions() {
run_suite(FUNCTIONS_JSON, "functions");
}
#[test]
fn compliance_identifiers() {
run_suite(IDENTIFIERS_JSON, "identifiers");
}
#[test]
fn compliance_indices() {
run_suite(INDICES_JSON, "indices");
}
#[test]
fn compliance_literal() {
run_suite(LITERAL_JSON, "literal");
}
#[test]
fn compliance_multiselect() {
run_suite(MULTISELECT_JSON, "multiselect");
}
#[test]
fn compliance_pipe() {
run_suite(PIPE_JSON, "pipe");
}
#[test]
fn compliance_slice() {
run_suite(SLICE_JSON, "slice");
}
#[test]
fn compliance_syntax() {
run_suite(SYNTAX_JSON, "syntax");
}
#[test]
fn compliance_unicode() {
run_suite(UNICODE_JSON, "unicode");
}
#[test]
fn compliance_wildcard() {
run_suite(WILDCARD_JSON, "wildcard");
}
#[test]
fn standard_functions_not_shadowed() {
let runtime = create_runtime();
let test_cases = [
(r#"abs(`-5`)"#, "5"),
(r#"avg(`[1, 2, 3, 4, 5]`)"#, "3.0"),
(r#"ceil(`1.5`)"#, "2"),
(r#"contains(`["a", "b"]`, `"a"`)"#, "true"),
(r#"ends_with(`"hello"`, `"lo"`)"#, "true"),
(r#"floor(`1.9`)"#, "1"),
(r#"join(`","`, `["a", "b"]`)"#, r#""a,b""#),
(r#"keys(`{"a": 1, "b": 2}`)"#, r#"["a","b"]"#),
(r#"length(`[1, 2, 3]`)"#, "3"),
(r#"max(`[1, 2, 3]`)"#, "3"),
(r#"merge(`{"a": 1}`, `{"b": 2}`)"#, r#"{"a":1,"b":2}"#),
(r#"min(`[1, 2, 3]`)"#, "1"),
(r#"not_null(`null`, `1`, `2`)"#, "1"),
(r#"reverse(`[1, 2, 3]`)"#, "[3,2,1]"),
(r#"sort(`[3, 1, 2]`)"#, "[1,2,3]"),
(r#"starts_with(`"hello"`, `"he"`)"#, "true"),
(r#"sum(`[1, 2, 3]`)"#, "6"),
(r#"to_array(`"hello"`)"#, r#"["hello"]"#),
(r#"to_number(`"42"`)"#, "42"),
(r#"to_string(`42`)"#, r#""42""#),
(r#"type(`"hello"`)"#, r#""string""#),
(r#"values(`{"a": 1, "b": 2}`)"#, "[1,2]"),
];
for (expr_str, expected_str) in test_cases {
let expr = runtime
.compile(expr_str)
.unwrap_or_else(|e| panic!("Failed to compile '{}': {}", expr_str, e));
let result = expr
.search(Rc::new(Variable::Null))
.unwrap_or_else(|e| panic!("Failed to evaluate '{}': {}", expr_str, e));
let result_json: Value = serde_json::to_value(&*result).unwrap();
let expected_json: Value = serde_json::from_str(expected_str).unwrap();
assert!(
values_equal(&result_json, &expected_json),
"Standard function test failed for '{}': got {:?}, expected {:?}",
expr_str,
result_json,
expected_json
);
}
}