use crate::error::{Result, TqlError};
use crate::parser::Value as AstValue;
use once_cell::sync::Lazy;
use regex::Regex;
use serde_json::Value as JsonValue;
use std::collections::HashMap;
use std::net::IpAddr;
use std::str::FromStr;
use std::sync::Mutex;
static REGEX_CACHE: Lazy<Mutex<HashMap<String, Regex>>> = Lazy::new(|| Mutex::new(HashMap::new()));
const MAX_REGEX_CACHE_SIZE: usize = 1024;
pub const MAX_REGEX_PATTERN_LENGTH: usize = 1000;
pub fn negated_matches_absent(operator: &str) -> bool {
matches!(
operator,
"not_in"
| "not_in_cs"
| "not_contains"
| "not_contains_cs"
| "not_startswith"
| "not_startswith_cs"
| "not_endswith"
| "not_endswith_cs"
| "not_regexp"
| "not_regex"
| "not_matches"
| "not_between"
| "not_cidr"
| "ne"
| "!="
)
}
pub fn compare(field_value: &JsonValue, operator: &str, compare_value: &AstValue) -> Result<bool> {
match operator {
"eq" | "=" => compare_eq(field_value, compare_value),
"ne" | "!=" => compare_ne(field_value, compare_value),
"eq_ci" => compare_eq_ci(field_value, compare_value),
"gt" | ">" => compare_gt(field_value, compare_value),
"gte" | ">=" => compare_gte(field_value, compare_value),
"lt" | "<" => compare_lt(field_value, compare_value),
"lte" | "<=" => compare_lte(field_value, compare_value),
"contains" => compare_contains_ci(field_value, compare_value),
"contains_cs" => compare_contains(field_value, compare_value),
"startswith" => compare_startswith_ci(field_value, compare_value),
"startswith_cs" => compare_startswith(field_value, compare_value),
"endswith" => compare_endswith_ci(field_value, compare_value),
"endswith_cs" => compare_endswith(field_value, compare_value),
"matches" | "regex" | "regexp" => compare_matches(field_value, compare_value),
"not_contains" => Ok(!compare_contains_ci(field_value, compare_value)?),
"not_contains_cs" => Ok(!compare_contains(field_value, compare_value)?),
"not_startswith" => Ok(!compare_startswith_ci(field_value, compare_value)?),
"not_startswith_cs" => Ok(!compare_startswith(field_value, compare_value)?),
"not_endswith" => Ok(!compare_endswith_ci(field_value, compare_value)?),
"not_endswith_cs" => Ok(!compare_endswith(field_value, compare_value)?),
"not_matches" | "not_regex" | "not_regexp" => {
Ok(!compare_matches(field_value, compare_value)?)
}
"in" => compare_in_ci(field_value, compare_value),
"in_cs" => compare_in(field_value, compare_value),
"not_in" => Ok(!compare_in_ci(field_value, compare_value)?),
"not_in_cs" => Ok(!compare_in(field_value, compare_value)?),
"between" => compare_between(field_value, compare_value),
"not_between" => Ok(!compare_between(field_value, compare_value)?),
"is" => compare_is(field_value, compare_value),
"is_not" => Ok(!compare_is(field_value, compare_value)?),
"cidr" => compare_cidr(field_value, compare_value),
"not_cidr" => Ok(!compare_cidr(field_value, compare_value)?),
"any" => compare_contains_ci(field_value, compare_value),
_ => Err(TqlError::OperatorError(format!(
"Unknown operator: {}",
operator
))),
}
}
fn compare_eq(field_value: &JsonValue, compare_value: &AstValue) -> Result<bool> {
Ok(values_equal(field_value, compare_value))
}
fn compare_ne(field_value: &JsonValue, compare_value: &AstValue) -> Result<bool> {
if field_value.is_null() {
return Ok(*compare_value != AstValue::Null);
}
Ok(!values_equal(field_value, compare_value))
}
fn compare_gt(field_value: &JsonValue, compare_value: &AstValue) -> Result<bool> {
if field_value.is_null() {
return Ok(false);
}
compare_ordered(field_value, compare_value, |a, b| a > b, |a, b| a > b)
}
fn compare_gte(field_value: &JsonValue, compare_value: &AstValue) -> Result<bool> {
if field_value.is_null() {
return Ok(false);
}
compare_ordered(field_value, compare_value, |a, b| a >= b, |a, b| a >= b)
}
fn compare_lt(field_value: &JsonValue, compare_value: &AstValue) -> Result<bool> {
if field_value.is_null() {
return Ok(false);
}
compare_ordered(field_value, compare_value, |a, b| a < b, |a, b| a < b)
}
fn compare_lte(field_value: &JsonValue, compare_value: &AstValue) -> Result<bool> {
if field_value.is_null() {
return Ok(false);
}
compare_ordered(field_value, compare_value, |a, b| a <= b, |a, b| a <= b)
}
fn value_as_comparison_text(value: &JsonValue) -> Option<String> {
match value {
JsonValue::String(s) => Some(s.clone()),
JsonValue::Number(n) => Some(n.to_string()),
JsonValue::Bool(b) => Some(if *b {
"true".to_string()
} else {
"false".to_string()
}),
JsonValue::Null => None,
other => Some(other.to_string()),
}
}
fn compare_contains(field_value: &JsonValue, compare_value: &AstValue) -> Result<bool> {
match field_value {
JsonValue::String(s) => {
let search = ast_value_to_string(compare_value);
Ok(s.contains(&search))
}
JsonValue::Array(arr) => {
for item in arr {
if compare_contains(item, compare_value)? {
return Ok(true);
}
}
Ok(false)
}
other => match value_as_comparison_text(other) {
Some(text) => {
let probe = JsonValue::String(text);
compare_contains(&probe, compare_value)
}
None => Ok(false),
},
}
}
fn compare_contains_ci(field_value: &JsonValue, compare_value: &AstValue) -> Result<bool> {
match field_value {
JsonValue::String(s) => {
let search = ast_value_to_string(compare_value).to_lowercase();
Ok(s.to_lowercase().contains(&search))
}
JsonValue::Array(arr) => {
for item in arr {
if compare_contains_ci(item, compare_value)? {
return Ok(true);
}
}
Ok(false)
}
other => match value_as_comparison_text(other) {
Some(text) => {
let probe = JsonValue::String(text);
compare_contains_ci(&probe, compare_value)
}
None => Ok(false),
},
}
}
fn compare_startswith(field_value: &JsonValue, compare_value: &AstValue) -> Result<bool> {
match field_value {
JsonValue::String(s) => {
let prefix = ast_value_to_string(compare_value);
Ok(s.starts_with(&prefix))
}
JsonValue::Array(arr) => {
for item in arr {
if compare_startswith(item, compare_value)? {
return Ok(true);
}
}
Ok(false)
}
other => match value_as_comparison_text(other) {
Some(text) => {
let probe = JsonValue::String(text);
compare_startswith(&probe, compare_value)
}
None => Ok(false),
},
}
}
fn compare_startswith_ci(field_value: &JsonValue, compare_value: &AstValue) -> Result<bool> {
match field_value {
JsonValue::String(s) => {
let prefix = ast_value_to_string(compare_value).to_lowercase();
Ok(s.to_lowercase().starts_with(&prefix))
}
JsonValue::Array(arr) => {
for item in arr {
if compare_startswith_ci(item, compare_value)? {
return Ok(true);
}
}
Ok(false)
}
other => match value_as_comparison_text(other) {
Some(text) => {
let probe = JsonValue::String(text);
compare_startswith_ci(&probe, compare_value)
}
None => Ok(false),
},
}
}
fn compare_endswith(field_value: &JsonValue, compare_value: &AstValue) -> Result<bool> {
match field_value {
JsonValue::String(s) => {
let suffix = ast_value_to_string(compare_value);
Ok(s.ends_with(&suffix))
}
JsonValue::Array(arr) => {
for item in arr {
if compare_endswith(item, compare_value)? {
return Ok(true);
}
}
Ok(false)
}
other => match value_as_comparison_text(other) {
Some(text) => {
let probe = JsonValue::String(text);
compare_endswith(&probe, compare_value)
}
None => Ok(false),
},
}
}
fn compare_endswith_ci(field_value: &JsonValue, compare_value: &AstValue) -> Result<bool> {
match field_value {
JsonValue::String(s) => {
let suffix = ast_value_to_string(compare_value).to_lowercase();
Ok(s.to_lowercase().ends_with(&suffix))
}
JsonValue::Array(arr) => {
for item in arr {
if compare_endswith_ci(item, compare_value)? {
return Ok(true);
}
}
Ok(false)
}
other => match value_as_comparison_text(other) {
Some(text) => {
let probe = JsonValue::String(text);
compare_endswith_ci(&probe, compare_value)
}
None => Ok(false),
},
}
}
fn compare_eq_ci(field_value: &JsonValue, compare_value: &AstValue) -> Result<bool> {
match (field_value, compare_value) {
(JsonValue::String(a), AstValue::String(b)) => Ok(a.to_lowercase() == b.to_lowercase()),
(JsonValue::Array(items), rhs) if !matches!(rhs, AstValue::List(_)) => {
for item in items {
if compare_eq_ci(item, rhs)? {
return Ok(true);
}
}
Ok(false)
}
_ => compare_eq(field_value, compare_value),
}
}
fn compare_matches(field_value: &JsonValue, compare_value: &AstValue) -> Result<bool> {
let pattern = ast_value_to_string(compare_value);
let pattern_len = pattern.chars().count();
if pattern_len > MAX_REGEX_PATTERN_LENGTH {
return Err(TqlError::ValueError(format!(
"Regex pattern is {} characters, which exceeds the maximum supported length of {}. \
The query cannot run as written.",
pattern_len, MAX_REGEX_PATTERN_LENGTH
)));
}
{
let cache = REGEX_CACHE.lock().unwrap_or_else(|e| e.into_inner());
if let Some(regex) = cache.get(&pattern) {
return Ok(matches_string(field_value, regex));
}
}
let regex = Regex::new(&pattern)
.map_err(|e| TqlError::ValueError(format!("Invalid regex pattern '{}': {}", pattern, e)))?;
let result = matches_string(field_value, ®ex);
let mut cache = REGEX_CACHE.lock().unwrap_or_else(|e| e.into_inner());
if cache.len() >= MAX_REGEX_CACHE_SIZE {
cache.clear();
}
cache.insert(pattern, regex);
Ok(result)
}
fn matches_string(field_value: &JsonValue, regex: &Regex) -> bool {
match field_value {
JsonValue::String(s) => regex.is_match(s),
JsonValue::Array(items) => items.iter().any(|i| matches_string(i, regex)),
JsonValue::Bool(b) => {
let text = if *b { "true" } else { "false" };
let pattern = regex.as_str();
if pattern.eq_ignore_ascii_case("true") || pattern.eq_ignore_ascii_case("false") {
return pattern.eq_ignore_ascii_case(text);
}
regex.is_match(text)
}
other => value_as_comparison_text(other)
.map(|s| regex.is_match(&s))
.unwrap_or(false),
}
}
fn compare_in(field_value: &JsonValue, compare_value: &AstValue) -> Result<bool> {
match compare_value {
AstValue::List(list) => {
for item in list {
if compare_eq(field_value, item)? {
return Ok(true);
}
}
Ok(false)
}
_ => Err(TqlError::OperatorError(
"IN operator requires a list of values".to_string(),
)),
}
}
fn compare_in_ci(field_value: &JsonValue, compare_value: &AstValue) -> Result<bool> {
match compare_value {
AstValue::List(list) => {
for item in list {
if compare_eq_ci(field_value, item)? {
return Ok(true);
}
}
Ok(false)
}
_ => Err(TqlError::OperatorError(
"IN operator requires a list of values".to_string(),
)),
}
}
fn compare_between(field_value: &JsonValue, compare_value: &AstValue) -> Result<bool> {
match compare_value {
AstValue::List(list) if list.len() == 2 => {
let min = &list[0];
let max = &list[1];
if let JsonValue::Array(items) = field_value {
for item in items {
if between_bounds(item, min, max)? {
return Ok(true);
}
}
return Ok(false);
}
between_bounds(field_value, min, max)
}
_ => Err(TqlError::OperatorError(
"BETWEEN operator requires a list of exactly 2 values".to_string(),
)),
}
}
fn between_bounds(field_value: &JsonValue, min: &AstValue, max: &AstValue) -> Result<bool> {
let at_least_min = compare_ordered(field_value, min, |a, b| a >= b, |a, b| a >= b)?;
if !at_least_min {
return Ok(false);
}
compare_ordered(field_value, max, |a, b| a <= b, |a, b| a <= b)
}
fn compare_is(field_value: &JsonValue, compare_value: &AstValue) -> Result<bool> {
match compare_value {
AstValue::Null => Ok(field_value.is_null()),
_ => Ok(values_equal(field_value, compare_value)),
}
}
fn compare_cidr(field_value: &JsonValue, compare_value: &AstValue) -> Result<bool> {
if let JsonValue::Array(items) = field_value {
for item in items {
if compare_cidr_scalar(item, compare_value)? {
return Ok(true);
}
}
return Ok(false);
}
compare_cidr_scalar(field_value, compare_value)
}
fn compare_cidr_scalar(field_value: &JsonValue, compare_value: &AstValue) -> Result<bool> {
let ip_str = match field_value {
JsonValue::String(s) => s.clone(),
_ => json_to_string(field_value),
};
let cidr_str = ast_value_to_string(compare_value);
let ip = match IpAddr::from_str(&ip_str) {
Ok(addr) => addr,
Err(_) => return Ok(false), };
let parts: Vec<&str> = cidr_str.split('/').collect();
if parts.len() != 2 {
return Ok(false); }
let network_ip = match IpAddr::from_str(parts[0]) {
Ok(addr) => addr,
Err(_) => return Ok(false),
};
let prefix_len: u8 = match parts[1].parse() {
Ok(len) => len,
Err(_) => return Ok(false),
};
match (ip, network_ip) {
(IpAddr::V4(ip_v4), IpAddr::V4(net_v4)) => check_ipv4_in_cidr(ip_v4, net_v4, prefix_len),
(IpAddr::V6(ip_v6), IpAddr::V6(net_v6)) => check_ipv6_in_cidr(ip_v6, net_v6, prefix_len),
_ => Ok(false), }
}
fn check_ipv4_in_cidr(
ip: std::net::Ipv4Addr,
network: std::net::Ipv4Addr,
prefix_len: u8,
) -> Result<bool> {
if prefix_len > 32 {
return Ok(false);
}
let ip_u32 = u32::from(ip);
let net_u32 = u32::from(network);
let mask = if prefix_len == 0 {
0
} else {
!0u32 << (32 - prefix_len)
};
Ok((ip_u32 & mask) == (net_u32 & mask))
}
fn check_ipv6_in_cidr(
ip: std::net::Ipv6Addr,
network: std::net::Ipv6Addr,
prefix_len: u8,
) -> Result<bool> {
if prefix_len > 128 {
return Ok(false);
}
let ip_u128 = u128::from(ip);
let net_u128 = u128::from(network);
let mask = if prefix_len == 0 {
0
} else {
!0u128 << (128 - prefix_len)
};
Ok((ip_u128 & mask) == (net_u128 & mask))
}
fn compare_ordered<NumF, StrF>(
field_value: &JsonValue,
compare_value: &AstValue,
num_predicate: NumF,
str_predicate: StrF,
) -> Result<bool>
where
NumF: Fn(f64, f64) -> bool,
StrF: Fn(&str, &str) -> bool,
{
if let JsonValue::Array(items) = field_value {
for item in items {
let matched = (|| -> Result<bool> {
if let (Some(a), Some(b)) =
(json_to_number(item), ast_value_to_number(compare_value))
{
return Ok(num_predicate(a, b));
}
if let (JsonValue::String(f), AstValue::String(c)) = (item, compare_value) {
return Ok(str_predicate(f, c));
}
Ok(false)
})()?;
if matched {
return Ok(true);
}
}
return Ok(false);
}
let field_num = json_to_number(field_value);
let compare_num = ast_value_to_number(compare_value);
if let (Some(a), Some(b)) = (field_num, compare_num) {
return Ok(num_predicate(a, b));
}
if let (JsonValue::String(field_str), AstValue::String(compare_str)) =
(field_value, compare_value)
{
return Ok(str_predicate(field_str, compare_str));
}
Ok(false)
}
fn values_equal(json_value: &JsonValue, ast_value: &AstValue) -> bool {
if let (JsonValue::Array(items), rhs) = (json_value, ast_value) {
if !matches!(rhs, AstValue::List(_)) {
return items.iter().any(|item| values_equal(item, rhs));
}
}
match (json_value, ast_value) {
(JsonValue::String(a), AstValue::String(b)) => a == b,
(JsonValue::Number(a), AstValue::Integer(b)) => {
a.as_i64() == Some(*b)
|| a.as_f64()
.map(|f| (f - *b as f64).abs() < f64::EPSILON)
.unwrap_or(false)
}
(JsonValue::Number(a), AstValue::Float(b)) => {
if let Some(a_f) = a.as_f64() {
(a_f - b).abs() < f64::EPSILON
} else {
false
}
}
(JsonValue::Bool(a), AstValue::Boolean(b)) => a == b,
(JsonValue::Null, AstValue::Null) => true,
(JsonValue::Null, _) => false,
(_, AstValue::Null) => false,
(JsonValue::Array(a), AstValue::List(b)) => {
if a.len() != b.len() {
return false;
}
for (json_item, ast_item) in a.iter().zip(b.iter()) {
if !values_equal(json_item, ast_item) {
return false;
}
}
true
}
(JsonValue::Bool(b), AstValue::Integer(i)) => {
let bool_as_int = if *b { 1 } else { 0 };
bool_as_int == *i
}
(JsonValue::Bool(b), AstValue::Float(f)) => {
let bool_as_float = if *b { 1.0 } else { 0.0 };
(bool_as_float - f).abs() < f64::EPSILON
}
_ => {
if let (Some(json_num), Some(ast_num)) =
(json_to_number(json_value), ast_value_to_number(ast_value))
{
return (json_num - ast_num).abs() < f64::EPSILON;
}
let json_str = json_to_string(json_value);
let ast_str = ast_value_to_string(ast_value);
json_str == ast_str
}
}
}
fn json_to_number(value: &JsonValue) -> Option<f64> {
match value {
JsonValue::Number(n) => n.as_f64(),
JsonValue::String(s) => finite(s.parse::<f64>().ok()),
JsonValue::Bool(true) => Some(1.0),
JsonValue::Bool(false) => Some(0.0),
_ => None,
}
}
fn ast_value_to_number(value: &AstValue) -> Option<f64> {
match value {
AstValue::Integer(i) => Some(*i as f64),
AstValue::Float(f) => finite(Some(*f)),
AstValue::String(s) => finite(s.parse::<f64>().ok()),
AstValue::Boolean(true) => Some(1.0),
AstValue::Boolean(false) => Some(0.0),
_ => None,
}
}
fn finite(value: Option<f64>) -> Option<f64> {
value.filter(|f| f.is_finite())
}
fn json_to_string(value: &JsonValue) -> String {
match value {
JsonValue::String(s) => s.clone(),
JsonValue::Number(n) => n.to_string(),
JsonValue::Bool(b) => b.to_string(),
JsonValue::Null => "null".to_string(),
JsonValue::Array(_) | JsonValue::Object(_) => value.to_string(),
}
}
fn ast_value_to_string(value: &AstValue) -> String {
match value {
AstValue::String(s) => s.clone(),
AstValue::Integer(i) => i.to_string(),
AstValue::Float(f) => f.to_string(),
AstValue::Boolean(b) => b.to_string(),
AstValue::Null => "null".to_string(),
AstValue::List(list) => {
let items: Vec<String> = list.iter().map(ast_value_to_string).collect();
format!("[{}]", items.join(", "))
}
}
}
#[cfg(test)]
mod tests {
mod non_list_membership_operand {
use super::*;
use crate::evaluator::TqlEvaluator;
use crate::parser::TqlParser;
fn evaluate(query: &str) -> Result<Vec<String>> {
let parser = TqlParser::new();
let evaluator = TqlEvaluator::new();
let ast = parser
.parse(query)
.expect("the grammar admits a scalar here");
let records = vec![serde_json::json!({"f": "y"}), serde_json::json!({"f": "x"})];
Ok(evaluator
.filter(&ast, &records)?
.iter()
.map(|r| r["f"].as_str().unwrap().to_string())
.collect())
}
fn assert_refused(query: &str) {
match evaluate(query) {
Err(TqlError::OperatorError(message)) => assert!(
message.contains("IN operator requires a list"),
"wrong message for `{query}`: {message}"
),
Err(other) => panic!(
"`{query}` must be refused as OperatorError, matching `between`; got {other:?}"
),
Ok(hits) => panic!("`{query}` was ANSWERED with {hits:?} instead of refused"),
}
}
#[test]
fn not_in_with_a_scalar_operand_is_refused() {
assert_refused("f not in 'x'");
}
#[test]
fn not_in_cs_with_a_scalar_operand_is_refused() {
assert_refused("f not in_cs 'x'");
}
#[test]
fn in_with_a_scalar_operand_is_refused() {
assert_refused("f in 'x'");
}
#[test]
fn in_cs_with_a_scalar_operand_is_refused() {
assert_refused("f in_cs 'x'");
}
#[test]
fn between_refuses_its_own_non_list_operand_the_same_way() {
assert!(
matches!(evaluate("f between 'x'"), Err(TqlError::OperatorError(_))),
"the precedent must still hold, or the two operators have drifted apart again"
);
}
#[test]
fn a_list_operand_still_works() {
assert_eq!(evaluate("f in ['x']").unwrap(), vec!["x".to_string()]);
assert_eq!(evaluate("f not in ['x']").unwrap(), vec!["y".to_string()]);
assert_eq!(evaluate("f in_cs ['X']").unwrap(), Vec::<String>::new());
assert_eq!(
evaluate("f not in_cs ['X']").unwrap(),
vec!["y".to_string(), "x".to_string()]
);
}
}
use super::*;
use serde_json::json;
#[test]
fn test_compare_eq() {
assert!(compare(&json!("test"), "eq", &AstValue::String("test".to_string())).unwrap());
assert!(!compare(&json!("test"), "eq", &AstValue::String("other".to_string())).unwrap());
assert!(compare(&json!(42), "eq", &AstValue::Integer(42)).unwrap());
assert!(compare(&json!(true), "eq", &AstValue::Boolean(true)).unwrap());
}
#[test]
fn test_compare_ne() {
assert!(compare(&json!("test"), "ne", &AstValue::String("other".to_string())).unwrap());
assert!(!compare(&json!("test"), "ne", &AstValue::String("test".to_string())).unwrap());
}
#[test]
fn test_compare_gt() {
assert!(compare(&json!(10), "gt", &AstValue::Integer(5)).unwrap());
assert!(!compare(&json!(5), "gt", &AstValue::Integer(10)).unwrap());
assert!(!compare(&json!(5), "gt", &AstValue::Integer(5)).unwrap());
}
#[test]
fn test_compare_gte() {
assert!(compare(&json!(10), "gte", &AstValue::Integer(5)).unwrap());
assert!(compare(&json!(5), "gte", &AstValue::Integer(5)).unwrap());
assert!(!compare(&json!(5), "gte", &AstValue::Integer(10)).unwrap());
}
#[test]
fn test_compare_lt() {
assert!(compare(&json!(5), "lt", &AstValue::Integer(10)).unwrap());
assert!(!compare(&json!(10), "lt", &AstValue::Integer(5)).unwrap());
}
#[test]
fn test_compare_lte() {
assert!(compare(&json!(5), "lte", &AstValue::Integer(10)).unwrap());
assert!(compare(&json!(5), "lte", &AstValue::Integer(5)).unwrap());
assert!(!compare(&json!(10), "lte", &AstValue::Integer(5)).unwrap());
}
#[test]
fn test_compare_contains() {
assert!(compare(
&json!("hello world"),
"contains",
&AstValue::String("world".to_string())
)
.unwrap());
assert!(!compare(
&json!("hello world"),
"contains",
&AstValue::String("foo".to_string())
)
.unwrap());
assert!(compare(
&json!(["a", "b", "c"]),
"contains",
&AstValue::String("b".to_string())
)
.unwrap());
assert!(!compare(
&json!(["a", "b", "c"]),
"contains",
&AstValue::String("d".to_string())
)
.unwrap());
}
#[test]
fn test_compare_startswith() {
assert!(compare(
&json!("hello world"),
"startswith",
&AstValue::String("hello".to_string())
)
.unwrap());
assert!(!compare(
&json!("hello world"),
"startswith",
&AstValue::String("world".to_string())
)
.unwrap());
}
#[test]
fn test_compare_endswith() {
assert!(compare(
&json!("hello world"),
"endswith",
&AstValue::String("world".to_string())
)
.unwrap());
assert!(!compare(
&json!("hello world"),
"endswith",
&AstValue::String("hello".to_string())
)
.unwrap());
}
#[test]
fn test_compare_matches() {
assert!(compare(
&json!("test123"),
"matches",
&AstValue::String(r"test\d+".to_string())
)
.unwrap());
assert!(!compare(
&json!("test"),
"matches",
&AstValue::String(r"test\d+".to_string())
)
.unwrap());
}
#[test]
fn test_over_length_regex_errors_rather_than_reporting_no_match() {
let over = "z".repeat(MAX_REGEX_PATTERN_LENGTH + 1);
let err = compare(&json!("alice"), "matches", &AstValue::String(over))
.expect_err("an over-length pattern must not be answered with a bool");
let message = err.to_string();
assert!(
message.contains(&MAX_REGEX_PATTERN_LENGTH.to_string()),
"error should name the limit: {message}"
);
assert!(
message.contains("cannot run"),
"error should say the query cannot run: {message}"
);
}
#[test]
fn test_over_length_regex_errors_under_negation_too() {
let over = "z".repeat(MAX_REGEX_PATTERN_LENGTH + 1);
assert!(compare(&json!("alice"), "not_matches", &AstValue::String(over)).is_err());
}
#[test]
fn test_over_length_regex_errors_for_non_string_fields() {
let over = "z".repeat(MAX_REGEX_PATTERN_LENGTH + 1);
assert!(compare(&json!(42), "matches", &AstValue::String(over)).is_err());
}
#[test]
fn test_regex_at_the_cap_still_runs() {
let mut at_cap = String::from("alice|");
at_cap.push_str(&"z".repeat(MAX_REGEX_PATTERN_LENGTH - at_cap.chars().count()));
assert_eq!(at_cap.chars().count(), MAX_REGEX_PATTERN_LENGTH);
assert!(compare(
&json!("alice"),
"matches",
&AstValue::String(at_cap.clone())
)
.unwrap());
assert!(!compare(&json!("bob"), "matches", &AstValue::String(at_cap)).unwrap());
}
#[test]
fn test_invalid_regex_errors_for_non_string_fields() {
assert!(compare(&json!(42), "matches", &AstValue::String("[a-z".to_string())).is_err());
assert!(compare(
&json!("alice"),
"matches",
&AstValue::String("[a-z".to_string())
)
.is_err());
}
#[test]
fn test_valid_regex_on_non_string_field_is_a_clean_false() {
assert!(!compare(&json!(42), "matches", &AstValue::String("^a".to_string())).unwrap());
}
#[test]
fn test_compare_in() {
let list = AstValue::List(vec![
AstValue::String("a".to_string()),
AstValue::String("b".to_string()),
AstValue::String("c".to_string()),
]);
assert!(compare(&json!("b"), "in", &list).unwrap());
assert!(!compare(&json!("d"), "in", &list).unwrap());
}
#[test]
fn test_compare_not_in() {
let list = AstValue::List(vec![
AstValue::String("a".to_string()),
AstValue::String("b".to_string()),
]);
assert!(compare(&json!("c"), "not_in", &list).unwrap());
assert!(!compare(&json!("a"), "not_in", &list).unwrap());
}
#[test]
fn test_compare_between() {
let range = AstValue::List(vec![AstValue::Integer(10), AstValue::Integer(20)]);
assert!(compare(&json!(15), "between", &range).unwrap());
assert!(compare(&json!(10), "between", &range).unwrap());
assert!(compare(&json!(20), "between", &range).unwrap());
assert!(!compare(&json!(5), "between", &range).unwrap());
assert!(!compare(&json!(25), "between", &range).unwrap());
}
#[test]
fn test_compare_is_null() {
assert!(compare(&json!(null), "is", &AstValue::Null).unwrap());
assert!(!compare(&json!("test"), "is", &AstValue::Null).unwrap());
}
#[test]
fn test_compare_is_not_null() {
assert!(compare(&json!("test"), "is_not", &AstValue::Null).unwrap());
assert!(!compare(&json!(null), "is_not", &AstValue::Null).unwrap());
}
#[test]
fn test_type_coercion() {
assert!(compare(&json!("42"), "eq", &AstValue::Integer(42)).unwrap());
assert!(compare(&json!("42"), "gt", &AstValue::Integer(40)).unwrap());
assert!(compare(&json!(42), "eq", &AstValue::String("42".to_string())).unwrap());
assert!(compare(&json!(true), "eq", &AstValue::Integer(1)).unwrap());
assert!(compare(&json!(false), "eq", &AstValue::Integer(0)).unwrap());
}
#[test]
fn test_contains_ci_array_substring() {
assert!(compare(
&json!(["hello world", "foo bar"]),
"contains",
&AstValue::String("world".to_string())
)
.unwrap());
assert!(compare(
&json!(["Hello World", "foo bar"]),
"contains",
&AstValue::String("hello".to_string())
)
.unwrap());
assert!(!compare(
&json!(["hello world", "foo bar"]),
"contains",
&AstValue::String("baz".to_string())
)
.unwrap());
}
#[test]
fn test_contains_cs_array_substring() {
assert!(compare(
&json!(["a", "b", "c"]),
"contains_cs",
&AstValue::String("b".to_string())
)
.unwrap());
assert!(!compare(
&json!(["a", "b", "c"]),
"contains_cs",
&AstValue::String("B".to_string())
)
.unwrap());
}
#[test]
fn test_startswith_ci_array() {
assert!(compare(
&json!(["hello world", "foo bar"]),
"startswith",
&AstValue::String("hello".to_string())
)
.unwrap());
assert!(compare(
&json!(["Hello World", "foo bar"]),
"startswith",
&AstValue::String("HELLO".to_string())
)
.unwrap());
assert!(!compare(
&json!(["hello world", "foo bar"]),
"startswith",
&AstValue::String("baz".to_string())
)
.unwrap());
}
#[test]
fn test_startswith_cs_array() {
assert!(compare(
&json!(["hello world", "foo bar"]),
"startswith_cs",
&AstValue::String("hello".to_string())
)
.unwrap());
assert!(!compare(
&json!(["Hello World", "foo bar"]),
"startswith_cs",
&AstValue::String("hello".to_string())
)
.unwrap());
}
#[test]
fn test_endswith_ci_array() {
assert!(compare(
&json!(["hello world", "foo bar"]),
"endswith",
&AstValue::String("world".to_string())
)
.unwrap());
assert!(compare(
&json!(["Hello World", "foo bar"]),
"endswith",
&AstValue::String("WORLD".to_string())
)
.unwrap());
assert!(!compare(
&json!(["hello world", "foo bar"]),
"endswith",
&AstValue::String("baz".to_string())
)
.unwrap());
}
#[test]
fn test_endswith_cs_array() {
assert!(compare(
&json!(["hello world", "foo bar"]),
"endswith_cs",
&AstValue::String("world".to_string())
)
.unwrap());
assert!(!compare(
&json!(["Hello World", "foo bar"]),
"endswith_cs",
&AstValue::String("WORLD".to_string())
)
.unwrap());
}
#[test]
fn test_in_ci_case_insensitive() {
let list = AstValue::List(vec![
AstValue::String("Hello".to_string()),
AstValue::String("World".to_string()),
]);
assert!(compare(&json!("hello"), "in", &list).unwrap());
assert!(compare(&json!("WORLD"), "in", &list).unwrap());
assert!(!compare(&json!("foo"), "in", &list).unwrap());
}
#[test]
fn test_in_ci_array_field() {
let list = AstValue::List(vec![
AstValue::String("Hello".to_string()),
AstValue::String("World".to_string()),
]);
assert!(compare(&json!(["hello", "foo"]), "in", &list).unwrap());
assert!(compare(&json!(["bar", "WORLD"]), "in", &list).unwrap());
assert!(!compare(&json!(["bar", "baz"]), "in", &list).unwrap());
}
#[test]
fn test_not_startswith_array() {
assert!(!compare(
&json!(["hello world", "foo bar"]),
"not_startswith",
&AstValue::String("hello".to_string())
)
.unwrap());
assert!(compare(
&json!(["hello world", "foo bar"]),
"not_startswith",
&AstValue::String("baz".to_string())
)
.unwrap());
}
#[test]
fn test_not_endswith_array() {
assert!(!compare(
&json!(["hello world", "foo bar"]),
"not_endswith",
&AstValue::String("world".to_string())
)
.unwrap());
assert!(compare(
&json!(["hello world", "foo bar"]),
"not_endswith",
&AstValue::String("baz".to_string())
)
.unwrap());
}
}