mod amount;
mod aviation;
mod color;
mod contact;
mod crypto;
mod date;
mod financial;
mod insurance;
mod numeric;
mod reference;
mod registry;
mod text;
mod tin;
#[cfg(feature = "address")]
mod address;
use crate::error::{Result, WelError};
use crate::ir::{Predicate, TemplateLiteralPart};
use crate::path::JsonPointer;
use memchr::memchr;
use regex::Regex;
use serde_json::Value;
use std::collections::HashMap;
use std::sync::{LazyLock, RwLock};
pub use registry::{NamedPredicate, PredicateRegistry};
pub static REGISTRY: LazyLock<PredicateRegistry> = LazyLock::new(PredicateRegistry::with_builtins);
static REGEX_CACHE: LazyLock<RwLock<HashMap<String, Regex>>> =
LazyLock::new(|| RwLock::new(HashMap::new()));
pub use amount::register_amount_predicates;
pub use aviation::register_aviation_predicates;
pub use color::register_color_predicates;
pub use contact::register_contact_predicates;
pub use crypto::register_crypto_predicates;
pub use date::register_date_predicates;
pub use financial::register_financial_predicates;
pub use insurance::register_insurance_predicates;
pub use numeric::register_numeric_predicates;
pub use reference::register_reference_predicates;
pub use text::register_text_predicates;
pub use tin::register_tin_predicates;
#[cfg(feature = "address")]
pub use address::register_address_predicates;
impl PredicateRegistry {
pub fn with_builtins() -> Self {
let mut registry = Self::new();
register_tin_predicates(&mut registry);
register_financial_predicates(&mut registry);
register_date_predicates(&mut registry);
register_amount_predicates(&mut registry);
register_reference_predicates(&mut registry);
register_aviation_predicates(&mut registry);
register_color_predicates(&mut registry);
register_insurance_predicates(&mut registry);
register_numeric_predicates(&mut registry);
register_crypto_predicates(&mut registry);
register_text_predicates(&mut registry);
register_contact_predicates(&mut registry);
#[cfg(feature = "address")]
register_address_predicates(&mut registry);
registry
}
}
pub struct EvalContext<'a> {
pub registry: &'a PredicateRegistry,
}
impl<'a> EvalContext<'a> {
pub fn new(registry: &'a PredicateRegistry) -> Self {
Self { registry }
}
}
fn get_cached_regex(pattern: &str, flags: Option<&str>) -> Result<Regex> {
let key = format!("{}:{}", pattern, flags.unwrap_or(""));
{
let cache = REGEX_CACHE.read().unwrap();
if let Some(regex) = cache.get(&key) {
return Ok(regex.clone());
}
}
let regex = compile_regex(pattern, flags)?;
{
let mut cache = REGEX_CACHE.write().unwrap();
cache.entry(key).or_insert_with(|| regex.clone());
}
Ok(regex)
}
fn compile_regex(pattern: &str, flags: Option<&str>) -> Result<Regex> {
let mut builder = regex::RegexBuilder::new(pattern);
if let Some(flags) = flags {
for flag in flags.chars() {
match flag {
'i' => {
builder.case_insensitive(true);
}
'm' => {
builder.multi_line(true);
}
's' => {
builder.dot_matches_new_line(true);
}
_ => {} }
}
}
builder.build().map_err(WelError::InvalidRegex)
}
pub fn evaluate(pred: &Predicate, value: &Value, ctx: &mut EvalContext) -> Result<bool> {
match pred {
Predicate::True => Ok(true),
Predicate::False => Ok(false),
Predicate::Regex { pattern, flags } => {
let regex = get_cached_regex(pattern, flags.as_deref())?;
Ok(match value {
Value::String(s) => regex.is_match(s),
_ => true,
})
}
Predicate::TemplateLiteral { parts } => Ok(match value {
Value::String(s) => matches_template_literal(s.as_bytes(), parts),
_ => true,
}),
Predicate::MinLen { len } => Ok(get_length(value).is_some_and(|l| l >= *len)),
Predicate::MaxLen { len } => Ok(get_length(value).is_some_and(|l| l <= *len)),
Predicate::Range { min, max } => {
let n = value.as_f64();
Ok(n.is_some_and(|n| {
let above_min = min.is_none_or(|m| n >= m);
let below_max = max.is_none_or(|m| n <= m);
above_min && below_max
}))
}
Predicate::Exists { path } => {
let ptr = JsonPointer::parse(path)?;
let results = ptr.resolve(value);
Ok(!results.is_empty() && results.iter().all(|v| !v.is_null()))
}
Predicate::Eq {
path,
value: expected,
} => {
let ptr = JsonPointer::parse(path)?;
let results = ptr.resolve(value);
Ok(!results.is_empty() && results.iter().all(|v| *v == expected))
}
Predicate::In { path, values } => {
let ptr = JsonPointer::parse(path)?;
let results = ptr.resolve(value);
Ok(!results.is_empty() && results.iter().all(|v| values.contains(v)))
}
Predicate::RequiredWith { field, with } => {
let with_exists = path_exists(value, with)?;
let field_exists = path_exists(value, field)?;
Ok(!with_exists || field_exists)
}
Predicate::RequiredWithout { field, without } => {
let without_exists = path_exists(value, without)?;
let field_exists = path_exists(value, field)?;
Ok(without_exists || field_exists)
}
Predicate::ExactlyOneOf { paths } => {
let mut present = 0usize;
for path in paths {
if path_exists(value, path)? {
present += 1;
if present > 1 {
return Ok(false);
}
}
}
Ok(present == 1)
}
Predicate::EqFields { left, right } => {
let left_ptr = JsonPointer::parse(left)?;
let right_ptr = JsonPointer::parse(right)?;
let left_vals = left_ptr.resolve(value);
let right_vals = right_ptr.resolve(value);
Ok(!left_vals.is_empty()
&& !right_vals.is_empty()
&& left_vals.len() == right_vals.len()
&& left_vals.iter().zip(right_vals.iter()).all(|(l, r)| l == r))
}
Predicate::GtField { left, right } => compare_fields(value, left, right, |l, r| l > r),
Predicate::GteField { left, right } => compare_fields(value, left, right, |l, r| l >= r),
Predicate::LtField { left, right } => compare_fields(value, left, right, |l, r| l < r),
Predicate::LteField { left, right } => compare_fields(value, left, right, |l, r| l <= r),
Predicate::SumEquals { paths, target } => {
let target_ptr = JsonPointer::parse(target)?;
let target_vals = target_ptr.resolve(value);
if target_vals.is_empty() {
return Ok(false);
}
let target_val = target_vals[0].as_f64();
if target_val.is_none() {
return Ok(false);
}
let mut sum = 0.0;
for path in paths {
let ptr = JsonPointer::parse(path)?;
let vals = ptr.resolve(value);
if vals.is_empty() {
return Ok(false);
}
if let Some(n) = vals[0].as_f64() {
sum += n;
} else {
return Ok(false);
}
}
Ok((sum - target_val.unwrap()).abs() < 1e-10)
}
Predicate::SumEqualsValue {
paths,
value: expected,
} => {
let mut sum = 0.0;
for path in paths {
let ptr = JsonPointer::parse(path)?;
let vals = ptr.resolve(value);
if vals.is_empty() {
return Ok(false);
}
if let Some(n) = vals[0].as_f64() {
sum += n;
} else {
return Ok(false);
}
}
Ok((sum - expected).abs() < 1e-10)
}
Predicate::And { predicates } => {
for p in predicates {
if !evaluate(p, value, ctx)? {
return Ok(false);
}
}
Ok(true)
}
Predicate::Or { predicates } => {
for p in predicates {
if evaluate(p, value, ctx)? {
return Ok(true);
}
}
Ok(false)
}
Predicate::Not { predicate } => {
let result = evaluate(predicate, value, ctx)?;
Ok(!result)
}
Predicate::Implies {
antecedent,
consequent,
} => {
let p = evaluate(antecedent, value, ctx)?;
if !p {
return Ok(true); }
evaluate(consequent, value, ctx)
}
Predicate::Call { name, args } => {
let predicate = ctx
.registry
.get(name)
.ok_or_else(|| WelError::UnknownPredicate(name.clone()))?;
Ok(predicate.evaluate(value, args))
}
}
}
fn matches_template_literal(input: &[u8], parts: &[TemplateLiteralPart]) -> bool {
matches_template_from(input, 0, parts, 0)
}
fn matches_template_from(
input: &[u8],
input_pos: usize,
parts: &[TemplateLiteralPart],
part_pos: usize,
) -> bool {
if part_pos == parts.len() {
return input_pos == input.len();
}
let Some(part) = parts.get(part_pos) else {
return false;
};
match part {
TemplateLiteralPart::Literal { value } => {
let literal = value.as_bytes();
if input[input_pos..].starts_with(literal) {
matches_template_from(input, input_pos + literal.len(), parts, part_pos + 1)
} else {
false
}
}
_ => match_template_segment(input, input_pos, parts, part_pos),
}
}
fn match_template_segment(
input: &[u8],
input_pos: usize,
parts: &[TemplateLiteralPart],
part_pos: usize,
) -> bool {
let Some(part) = parts.get(part_pos) else {
return false;
};
let Some((min_len, max_len)) = template_segment_bounds(part) else {
return false;
};
let remaining = input.len().saturating_sub(input_pos);
let max_len = max_len.min(remaining);
if min_len > max_len {
return false;
}
let run_len = template_segment_run_len(part, &input[input_pos..]);
if run_len < min_len {
return false;
}
let max_run = max_len.min(run_len);
if part_pos + 1 == parts.len() {
let tail_len = remaining;
return tail_len >= min_len && tail_len <= max_run;
}
let next_part = &parts[part_pos + 1];
if let TemplateLiteralPart::Literal { value } = next_part {
let next_lit = value.as_bytes();
if !next_lit.is_empty() {
let mut search_start = input_pos + min_len;
while search_start <= input_pos + max_run {
let Some(found) = find_literal_at_or_after(input, search_start, next_lit) else {
break;
};
let consumed = found - input_pos;
if consumed > max_run {
break;
}
if matches_template_from(input, found, parts, part_pos + 1) {
return true;
}
search_start = found + 1;
}
return false;
}
}
for consumed in (min_len..=max_run).rev() {
if matches_template_from(input, input_pos + consumed, parts, part_pos + 1) {
return true;
}
}
false
}
fn template_segment_bounds(part: &TemplateLiteralPart) -> Option<(usize, usize)> {
let (min, max) = match part {
TemplateLiteralPart::Digits { min, max }
| TemplateLiteralPart::AsciiLetters { min, max }
| TemplateLiteralPart::AsciiAlphanumeric { min, max }
| TemplateLiteralPart::Uppercase { min, max }
| TemplateLiteralPart::Lowercase { min, max }
| TemplateLiteralPart::Hex { min, max } => (min.unwrap_or(1), max.unwrap_or(usize::MAX)),
TemplateLiteralPart::Literal { .. } => return None,
};
if min > max {
None
} else {
Some((min, max))
}
}
fn template_segment_run_len(part: &TemplateLiteralPart, input: &[u8]) -> usize {
let mut len = 0usize;
for &byte in input {
if template_segment_matches_byte(part, byte) {
len += 1;
} else {
break;
}
}
len
}
fn template_segment_matches_byte(part: &TemplateLiteralPart, byte: u8) -> bool {
match part {
TemplateLiteralPart::Digits { .. } => byte.is_ascii_digit(),
TemplateLiteralPart::AsciiLetters { .. } => byte.is_ascii_alphabetic(),
TemplateLiteralPart::AsciiAlphanumeric { .. } => byte.is_ascii_alphanumeric(),
TemplateLiteralPart::Uppercase { .. } => byte.is_ascii_uppercase(),
TemplateLiteralPart::Lowercase { .. } => byte.is_ascii_lowercase(),
TemplateLiteralPart::Hex { .. } => byte.is_ascii_hexdigit(),
TemplateLiteralPart::Literal { .. } => false,
}
}
fn find_literal_at_or_after(input: &[u8], mut at: usize, needle: &[u8]) -> Option<usize> {
if needle.is_empty() {
return Some(at.min(input.len()));
}
if at >= input.len() {
return None;
}
let first = needle[0];
loop {
let rel = memchr(first, &input[at..])?;
let idx = at + rel;
if input[idx..].starts_with(needle) {
return Some(idx);
}
at = idx + 1;
if at >= input.len() {
return None;
}
}
}
fn get_length(value: &Value) -> Option<usize> {
match value {
Value::String(s) => Some(s.chars().count()),
Value::Array(a) => Some(a.len()),
_ => None,
}
}
fn path_exists(value: &Value, path: &str) -> Result<bool> {
let ptr = JsonPointer::parse(path)?;
let results = ptr.resolve(value);
Ok(!results.is_empty() && results.iter().all(|v| !v.is_null()))
}
fn compare_fields<F>(value: &Value, left: &str, right: &str, cmp: F) -> Result<bool>
where
F: Fn(f64, f64) -> bool,
{
let left_ptr = JsonPointer::parse(left)?;
let right_ptr = JsonPointer::parse(right)?;
let left_vals = left_ptr.resolve(value);
let right_vals = right_ptr.resolve(value);
if left_vals.is_empty() || right_vals.is_empty() {
return Ok(false);
}
let left_val = left_vals[0].as_f64();
let right_val = right_vals[0].as_f64();
match (left_val, right_val) {
(Some(l), Some(r)) => Ok(cmp(l, r)),
_ => Ok(false),
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_regex_match() {
let registry = PredicateRegistry::new();
let mut ctx = EvalContext::new(®istry);
let pred = Predicate::regex(r"^\d{9}$");
assert!(evaluate(&pred, &json!("123456789"), &mut ctx).unwrap());
assert!(!evaluate(&pred, &json!("12345678"), &mut ctx).unwrap());
assert!(!evaluate(&pred, &json!("1234567890"), &mut ctx).unwrap());
}
#[test]
fn test_regex_case_insensitive() {
let registry = PredicateRegistry::new();
let mut ctx = EvalContext::new(®istry);
let pred = Predicate::regex_with_flags("^hello$", "i");
assert!(evaluate(&pred, &json!("HELLO"), &mut ctx).unwrap());
assert!(evaluate(&pred, &json!("hello"), &mut ctx).unwrap());
assert!(evaluate(&pred, &json!("HeLLo"), &mut ctx).unwrap());
}
#[test]
fn test_template_literal_match() {
let registry = PredicateRegistry::new();
let mut ctx = EvalContext::new(®istry);
let pred = Predicate::template_literal(vec![
TemplateLiteralPart::literal("SFO-"),
TemplateLiteralPart::digits(Some(3), Some(4)),
TemplateLiteralPart::literal("-"),
TemplateLiteralPart::uppercase(Some(2), Some(2)),
]);
assert!(evaluate(&pred, &json!("SFO-123-AB"), &mut ctx).unwrap());
assert!(evaluate(&pred, &json!("SFO-1234-ZZ"), &mut ctx).unwrap());
assert!(!evaluate(&pred, &json!("SFO-12-AB"), &mut ctx).unwrap());
assert!(!evaluate(&pred, &json!("SFO-123-abc"), &mut ctx).unwrap());
}
#[test]
fn test_template_literal_backtracking_with_literal_boundary() {
let registry = PredicateRegistry::new();
let mut ctx = EvalContext::new(®istry);
let pred = Predicate::template_literal(vec![
TemplateLiteralPart::ascii_letters(Some(1), Some(3)),
TemplateLiteralPart::literal("-"),
TemplateLiteralPart::digits(Some(2), Some(2)),
]);
assert!(evaluate(&pred, &json!("AB-12"), &mut ctx).unwrap());
assert!(evaluate(&pred, &json!("A-12"), &mut ctx).unwrap());
assert!(!evaluate(&pred, &json!("ABCD-12"), &mut ctx).unwrap());
}
#[test]
fn test_min_max_len() {
let registry = PredicateRegistry::new();
let mut ctx = EvalContext::new(®istry);
let min = Predicate::min_len(3);
let max = Predicate::max_len(5);
assert!(evaluate(&min, &json!("abc"), &mut ctx).unwrap());
assert!(!evaluate(&min, &json!("ab"), &mut ctx).unwrap());
assert!(evaluate(&max, &json!("abcde"), &mut ctx).unwrap());
assert!(!evaluate(&max, &json!("abcdef"), &mut ctx).unwrap());
}
#[test]
fn test_range() {
let registry = PredicateRegistry::new();
let mut ctx = EvalContext::new(®istry);
let pred = Predicate::range(Some(0.0), Some(100.0));
assert!(evaluate(&pred, &json!(50), &mut ctx).unwrap());
assert!(evaluate(&pred, &json!(0), &mut ctx).unwrap());
assert!(evaluate(&pred, &json!(100), &mut ctx).unwrap());
assert!(!evaluate(&pred, &json!(-1), &mut ctx).unwrap());
assert!(!evaluate(&pred, &json!(101), &mut ctx).unwrap());
}
#[test]
fn test_exists() {
let registry = PredicateRegistry::new();
let mut ctx = EvalContext::new(®istry);
let pred = Predicate::exists("/foo");
assert!(evaluate(&pred, &json!({"foo": "bar"}), &mut ctx).unwrap());
assert!(!evaluate(&pred, &json!({"foo": null}), &mut ctx).unwrap());
assert!(!evaluate(&pred, &json!({"bar": "baz"}), &mut ctx).unwrap());
}
#[test]
fn test_eq() {
let registry = PredicateRegistry::new();
let mut ctx = EvalContext::new(®istry);
let pred = Predicate::eq("/status", json!("active"));
assert!(evaluate(&pred, &json!({"status": "active"}), &mut ctx).unwrap());
assert!(!evaluate(&pred, &json!({"status": "inactive"}), &mut ctx).unwrap());
}
#[test]
fn test_in() {
let registry = PredicateRegistry::new();
let mut ctx = EvalContext::new(®istry);
let pred = Predicate::in_values("/state", vec![json!("CA"), json!("NY"), json!("TX")]);
assert!(evaluate(&pred, &json!({"state": "CA"}), &mut ctx).unwrap());
assert!(!evaluate(&pred, &json!({"state": "FL"}), &mut ctx).unwrap());
}
#[test]
fn test_and() {
let registry = PredicateRegistry::new();
let mut ctx = EvalContext::new(®istry);
let pred = Predicate::and(vec![Predicate::min_len(1), Predicate::max_len(10)]);
assert!(evaluate(&pred, &json!("hello"), &mut ctx).unwrap());
assert!(!evaluate(&pred, &json!(""), &mut ctx).unwrap());
assert!(!evaluate(&pred, &json!("this is way too long"), &mut ctx).unwrap());
}
#[test]
fn test_or() {
let registry = PredicateRegistry::new();
let mut ctx = EvalContext::new(®istry);
let pred = Predicate::or(vec![
Predicate::eq("/type", json!("A")),
Predicate::eq("/type", json!("B")),
]);
assert!(evaluate(&pred, &json!({"type": "A"}), &mut ctx).unwrap());
assert!(evaluate(&pred, &json!({"type": "B"}), &mut ctx).unwrap());
assert!(!evaluate(&pred, &json!({"type": "C"}), &mut ctx).unwrap());
}
#[test]
fn test_not() {
let registry = PredicateRegistry::new();
let mut ctx = EvalContext::new(®istry);
let pred = Predicate::not(Predicate::exists("/deleted"));
assert!(evaluate(&pred, &json!({"active": true}), &mut ctx).unwrap());
assert!(!evaluate(&pred, &json!({"deleted": true}), &mut ctx).unwrap());
}
#[test]
fn test_implies() {
let registry = PredicateRegistry::new();
let mut ctx = EvalContext::new(®istry);
let pred = Predicate::implies(
Predicate::eq("/isForeign", json!(false)),
Predicate::exists("/zip"),
);
assert!(evaluate(
&pred,
&json!({"isForeign": false, "zip": "12345"}),
&mut ctx
)
.unwrap());
assert!(!evaluate(&pred, &json!({"isForeign": false}), &mut ctx).unwrap());
assert!(evaluate(&pred, &json!({"isForeign": true}), &mut ctx).unwrap());
}
#[test]
fn test_named_predicates_with_builtins() {
let registry = PredicateRegistry::with_builtins();
let mut ctx = EvalContext::new(®istry);
let pred = Predicate::call("is_tin", json!({"kind": "ANY"}));
assert!(evaluate(&pred, &json!("123-45-6789"), &mut ctx).unwrap());
assert!(!evaluate(&pred, &json!("000-00-0000"), &mut ctx).unwrap());
}
#[test]
fn test_unknown_predicate() {
let registry = PredicateRegistry::new();
let mut ctx = EvalContext::new(®istry);
let pred = Predicate::call_no_args("unknown_predicate");
let result = evaluate(&pred, &json!("test"), &mut ctx);
assert!(matches!(result, Err(WelError::UnknownPredicate(_))));
}
#[test]
fn test_eq_fields() {
let registry = PredicateRegistry::new();
let mut ctx = EvalContext::new(®istry);
let pred = Predicate::EqFields {
left: "/a".to_string(),
right: "/b".to_string(),
};
assert!(evaluate(&pred, &json!({"a": 100, "b": 100}), &mut ctx).unwrap());
assert!(!evaluate(&pred, &json!({"a": 100, "b": 200}), &mut ctx).unwrap());
assert!(!evaluate(&pred, &json!({"a": 100}), &mut ctx).unwrap());
}
#[test]
fn test_field_comparisons() {
let registry = PredicateRegistry::new();
let mut ctx = EvalContext::new(®istry);
let gt = Predicate::GtField {
left: "/a".to_string(),
right: "/b".to_string(),
};
assert!(evaluate(>, &json!({"a": 100, "b": 50}), &mut ctx).unwrap());
assert!(!evaluate(>, &json!({"a": 50, "b": 100}), &mut ctx).unwrap());
let gte = Predicate::GteField {
left: "/a".to_string(),
right: "/b".to_string(),
};
assert!(evaluate(>e, &json!({"a": 100, "b": 100}), &mut ctx).unwrap());
assert!(evaluate(>e, &json!({"a": 100, "b": 50}), &mut ctx).unwrap());
assert!(!evaluate(>e, &json!({"a": 50, "b": 100}), &mut ctx).unwrap());
let lt = Predicate::LtField {
left: "/a".to_string(),
right: "/b".to_string(),
};
assert!(evaluate(<, &json!({"a": 50, "b": 100}), &mut ctx).unwrap());
assert!(!evaluate(<, &json!({"a": 100, "b": 50}), &mut ctx).unwrap());
let lte = Predicate::LteField {
left: "/a".to_string(),
right: "/b".to_string(),
};
assert!(evaluate(<e, &json!({"a": 100, "b": 100}), &mut ctx).unwrap());
assert!(evaluate(<e, &json!({"a": 50, "b": 100}), &mut ctx).unwrap());
assert!(!evaluate(<e, &json!({"a": 100, "b": 50}), &mut ctx).unwrap());
}
#[test]
fn test_sum_equals() {
let registry = PredicateRegistry::new();
let mut ctx = EvalContext::new(®istry);
let pred = Predicate::SumEquals {
paths: vec!["/a".to_string(), "/b".to_string(), "/c".to_string()],
target: "/total".to_string(),
};
assert!(evaluate(
&pred,
&json!({"a": 10, "b": 20, "c": 30, "total": 60}),
&mut ctx
)
.unwrap());
assert!(!evaluate(
&pred,
&json!({"a": 10, "b": 20, "c": 30, "total": 50}),
&mut ctx
)
.unwrap());
}
#[test]
fn test_sum_equals_value() {
let registry = PredicateRegistry::new();
let mut ctx = EvalContext::new(®istry);
let pred = Predicate::SumEqualsValue {
paths: vec![
"/percent1".to_string(),
"/percent2".to_string(),
"/percent3".to_string(),
],
value: 100.0,
};
assert!(evaluate(
&pred,
&json!({"percent1": 50, "percent2": 30, "percent3": 20}),
&mut ctx
)
.unwrap());
assert!(!evaluate(
&pred,
&json!({"percent1": 50, "percent2": 30, "percent3": 10}),
&mut ctx
)
.unwrap());
}
#[test]
fn test_required_with() {
let registry = PredicateRegistry::new();
let mut ctx = EvalContext::new(®istry);
let pred = Predicate::RequiredWith {
field: "/confirm_password".to_string(),
with: "/password".to_string(),
};
assert!(evaluate(&pred, &json!({"password": null}), &mut ctx).unwrap());
assert!(!evaluate(&pred, &json!({"password": "secret"}), &mut ctx).unwrap());
assert!(evaluate(
&pred,
&json!({"password": "secret", "confirm_password": "secret"}),
&mut ctx
)
.unwrap());
}
#[test]
fn test_required_without() {
let registry = PredicateRegistry::new();
let mut ctx = EvalContext::new(®istry);
let pred = Predicate::RequiredWithout {
field: "/tax_id".to_string(),
without: "/ssn".to_string(),
};
assert!(!evaluate(&pred, &json!({}), &mut ctx).unwrap());
assert!(evaluate(&pred, &json!({"tax_id": "12-3456789"}), &mut ctx).unwrap());
assert!(evaluate(&pred, &json!({"ssn": "123-45-6789"}), &mut ctx).unwrap());
}
#[test]
fn test_exactly_one_of() {
let registry = PredicateRegistry::new();
let mut ctx = EvalContext::new(®istry);
let pred = Predicate::ExactlyOneOf {
paths: vec!["/ssn".to_string(), "/ein".to_string()],
};
assert!(evaluate(&pred, &json!({"ssn": "123-45-6789"}), &mut ctx).unwrap());
assert!(evaluate(&pred, &json!({"ein": "12-3456789"}), &mut ctx).unwrap());
assert!(!evaluate(
&pred,
&json!({"ssn": "123-45-6789", "ein": "12-3456789"}),
&mut ctx
)
.unwrap());
assert!(!evaluate(&pred, &json!({}), &mut ctx).unwrap());
}
}