use std::collections::HashSet;
use std::fmt;
use std::net::IpAddr;
use std::result::Result as StdResult;
use std::str::FromStr;
use ipnet::IpNet;
use ipnet_trie::IpnetTrie;
use regex::{Regex, RegexSet};
use serde_json::{Map, Value as JsonValue, json};
use crate::types::{AsyncCheckFn, BoxFuture, CheckFn, DynError, MaybeSend, MaybeSync};
use crate::{Error, JsonValueExt as _, Result, Value};
pub enum Operator<Ctx: ?Sized> {
Equal(Value<'static>),
LessThan(Value<'static>),
LessThanOrEqual(Value<'static>),
GreaterThan(Value<'static>),
GreaterThanOrEqual(Value<'static>),
InSet(HashSet<Value<'static>>),
Regex(regex::Regex),
RegexSet(regex::RegexSet),
IpSet(IpnetTrie<()>),
Custom(Box<CheckFn<Ctx>>),
CustomAsync(Box<AsyncCheckFn<Ctx>>),
}
impl<Ctx: ?Sized> fmt::Debug for Operator<Ctx> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Operator::Equal(v) => write!(f, "Equal({v:?})"),
Operator::LessThan(v) => write!(f, "LessThan({v:?})"),
Operator::LessThanOrEqual(v) => write!(f, "LessThanOrEqual({v:?})"),
Operator::GreaterThan(v) => write!(f, "GreaterThan({v:?})"),
Operator::GreaterThanOrEqual(v) => write!(f, "GreaterThanOrEqual({v:?})"),
Operator::InSet(set) => write!(f, "InSet({set:?})",),
Operator::Regex(regex) => write!(f, "Regex({regex:?})"),
Operator::RegexSet(regex_set) => write!(f, "RegexSet({regex_set:?})"),
Operator::IpSet(_) => f.write_str("IpSet"),
Operator::Custom(_) => f.write_str("Custom"),
Operator::CustomAsync(_) => f.write_str("CustomAsync"),
}
}
}
impl<Ctx: ?Sized> Operator<Ctx> {
pub fn new<F>(func: F) -> Self
where
F: Fn(&Ctx, Value) -> StdResult<bool, DynError> + MaybeSend + MaybeSync + 'static,
{
Operator::Custom(Box::new(func))
}
pub fn new_async<F>(func: F) -> Self
where
F: for<'a> Fn(&'a Ctx, Value<'a>) -> BoxFuture<'a, StdResult<bool, DynError>>
+ MaybeSend
+ MaybeSync
+ 'static,
{
Operator::CustomAsync(Box::new(func))
}
}
pub trait Matcher<Ctx: ?Sized>: MaybeSend + MaybeSync {
fn compile(&self, value: &JsonValue) -> Result<Operator<Ctx>>;
fn json_schema(&self, custom_ops: &[(&str, JsonValue)]) -> JsonValue {
let _ = custom_ops;
json!({})
}
}
macro_rules! operator_error {
($op:expr, $($arg:tt)*) => {
Err(Error::operator($op, format!($($arg)*)))
};
}
macro_rules! check_operator {
($map:expr) => {{
let len = $map.len();
if len != 1 {
let msg = format!("operator object must have exactly one key (got {len})");
return Err(Error::json(msg));
}
$map.iter().next().unwrap()
}};
}
const IPV4_PATTERN: &str =
r"(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)(?:/\d{1,2})?";
const IPV6_PATTERN: &str = r"(?:[0-9a-f]{1,4}:){1,7}[0-9a-f]{0,4}|::(?:[0-9a-f:]{1,})?|[0-9a-f]{1,4}::(?:[0-9a-f:]{1,})?(?:/\d{1,3})?";
pub struct DefaultMatcher;
impl<Ctx: ?Sized> Matcher<Ctx> for DefaultMatcher {
fn compile(&self, value: &JsonValue) -> Result<Operator<Ctx>> {
match value {
JsonValue::Null | JsonValue::Bool(_) | JsonValue::Number(_) | JsonValue::String(_) => {
Ok(Operator::Equal(Value::from(value).into_static()))
}
JsonValue::Array(seq) => Ok(Operator::InSet(Self::make_hashset(seq))),
JsonValue::Object(map) => Self::compile_op(map),
}
}
fn json_schema(&self, custom_ops: &[(&str, JsonValue)]) -> JsonValue {
DefaultMatcher::json_schema(self, custom_ops)
}
}
impl DefaultMatcher {
fn compile_op<Ctx: ?Sized>(map: &Map<String, JsonValue>) -> Result<Operator<Ctx>> {
let (op, value) = check_operator!(map);
match (op.as_str(), value) {
("<", v) => Ok(Operator::LessThan(Value::from(v).into_static())),
("<=", v) => Ok(Operator::LessThanOrEqual(Value::from(v).into_static())),
(">", v) => Ok(Operator::GreaterThan(Value::from(v).into_static())),
(">=", v) => Ok(Operator::GreaterThanOrEqual(Value::from(v).into_static())),
("==", v) => Ok(Operator::Equal(Value::from(v).into_static())),
("in", JsonValue::Array(arr)) => Ok(Operator::InSet(Self::make_hashset(arr))),
("in", _) => operator_error!(op, "expected array, got {}", value.type_name()),
("re", JsonValue::String(pattern)) => {
let regex = Regex::new(pattern).map_err(|err| Error::operator(op, err))?;
Ok(Operator::Regex(regex))
}
("re", JsonValue::Array(patterns)) => RegexMatcher::make_regex_set(patterns)
.map(Operator::RegexSet)
.map_err(|err| Error::operator(op, err)),
("re", _) => operator_error!(op, "expected string or array, got {}", value.type_name()),
("ip", JsonValue::Array(arr)) => IpMatcher::make_ipnet(arr)
.map(Operator::IpSet)
.map_err(|err| Error::operator(op, err)),
("ip", _) => operator_error!(op, "expected array, got {}", value.type_name()),
_ => Err(Error::UnknownOperator(op.clone())),
}
}
fn make_hashset(arr: &[JsonValue]) -> HashSet<Value<'static>> {
arr.iter().map(|v| Value::from(v).into_static()).collect()
}
fn json_schema(&self, custom_ops: &[(&str, JsonValue)]) -> JsonValue {
let any_schema = json!({ "type": ["null", "boolean", "number", "string"] });
let any_array_schema = json!({ "type": "array", "items": any_schema });
let string_schema = json!({ "type": "string" });
let string_array_schema = json!({ "type": "array", "items": string_schema });
let ip_schema = json!({ "type": "string", "pattern": format!(r"^(?:{IPV4_PATTERN}|(?i:{IPV6_PATTERN}))") });
let ip_array_schema = json!({ "type": "array", "items": ip_schema });
let mut properties = Map::new();
properties.insert("<".to_string(), any_schema.clone());
properties.insert("<=".to_string(), any_schema.clone());
properties.insert(">".to_string(), any_schema.clone());
properties.insert(">=".to_string(), any_schema.clone());
properties.insert("==".to_string(), any_schema.clone());
properties.insert("in".to_string(), any_array_schema.clone());
properties.insert(
"re".to_string(),
json!({ "oneOf": [string_schema, string_array_schema] }),
);
properties.insert("ip".to_string(), ip_array_schema);
for (op, schema) in custom_ops {
properties.insert(op.to_string(), schema.clone());
}
json!({
"oneOf": [
any_schema,
any_array_schema,
{
"type": "object",
"properties": properties,
"additionalProperties": false,
"minProperties": 1,
"maxProperties": 1
}
]
})
}
}
pub struct StringMatcher;
impl<Ctx: ?Sized> Matcher<Ctx> for StringMatcher {
fn compile(&self, value: &JsonValue) -> Result<Operator<Ctx>> {
match value {
JsonValue::String(s) => Ok(Operator::Equal(Value::from(s.clone()))),
JsonValue::Array(seq) => Ok(Operator::InSet(Self::make_hashset(seq)?)),
JsonValue::Object(map) => Self::compile_op(map),
_ => {
let msg = format!("unexpected JSON {}", value.type_name());
Err(Error::json(msg))
}
}
}
fn json_schema(&self, custom_ops: &[(&str, JsonValue)]) -> JsonValue {
StringMatcher::json_schema(self, custom_ops)
}
}
impl StringMatcher {
fn compile_op<Ctx: ?Sized>(map: &Map<String, JsonValue>) -> Result<Operator<Ctx>> {
let (op, value) = check_operator!(map);
match (op.as_str(), value) {
("<", JsonValue::String(s)) => Ok(Operator::LessThan(Value::from(s).into_static())),
("<=", JsonValue::String(s)) => {
Ok(Operator::LessThanOrEqual(Value::from(s).into_static()))
}
(">", JsonValue::String(s)) => Ok(Operator::GreaterThan(Value::from(s).into_static())),
(">=", JsonValue::String(s)) => {
Ok(Operator::GreaterThanOrEqual(Value::from(s).into_static()))
}
("==", JsonValue::String(s)) => Ok(Operator::Equal(Value::from(s).into_static())),
("<" | "<=" | ">" | ">=" | "==", _) => {
operator_error!(op, "expected string, got {}", value.type_name())
}
("in", JsonValue::Array(arr)) => Self::make_hashset(arr)
.map(Operator::InSet)
.map_err(|err| Error::operator(op, err)),
("in", _) => operator_error!(op, "expected array, got {}", value.type_name()),
("re", JsonValue::String(pattern)) => {
let regex = Regex::new(pattern).map_err(|err| Error::operator(op, err))?;
Ok(Operator::Regex(regex))
}
("re", JsonValue::Array(patterns)) => RegexMatcher::make_regex_set(patterns)
.map(Operator::RegexSet)
.map_err(|err| Error::operator(op, err)),
("re", _) => operator_error!(op, "expected string or array, got {}", value.type_name()),
_ => Err(Error::UnknownOperator(op.clone())),
}
}
fn make_hashset(arr: &[JsonValue]) -> Result<HashSet<Value<'static>>> {
arr.iter()
.map(|v| match v {
JsonValue::String(s) => Ok(Value::from(s.clone())),
_ => {
let msg = format!("got {} in string array", v.type_name());
Err(Error::json(msg))
}
})
.collect()
}
fn json_schema(&self, custom_ops: &[(&str, JsonValue)]) -> JsonValue {
let string_schema = json!({ "type": "string" });
let string_array_schema = json!({ "type": "array", "items": string_schema });
let mut properties = Map::new();
properties.insert("<".to_string(), string_schema.clone());
properties.insert("<=".to_string(), string_schema.clone());
properties.insert(">".to_string(), string_schema.clone());
properties.insert(">=".to_string(), string_schema.clone());
properties.insert("==".to_string(), string_schema.clone());
properties.insert("in".to_string(), string_array_schema.clone());
properties.insert(
"re".to_string(),
json!({ "oneOf": [string_schema, string_array_schema] }),
);
for (op, schema) in custom_ops {
properties.insert(op.to_string(), schema.clone());
}
json!({
"oneOf": [
string_schema,
string_array_schema,
{
"type": "object",
"properties": properties,
"additionalProperties": false,
"minProperties": 1,
"maxProperties": 1
}
]
})
}
}
pub struct RegexMatcher;
impl<Ctx: ?Sized> Matcher<Ctx> for RegexMatcher {
fn compile(&self, value: &JsonValue) -> Result<Operator<Ctx>> {
match value {
JsonValue::String(pattern) => Ok(Operator::Regex(Regex::new(pattern)?)),
JsonValue::Array(patterns) => Ok(Operator::RegexSet(Self::make_regex_set(patterns)?)),
JsonValue::Object(map) => Self::compile_op(map),
_ => {
let msg = format!("unexpected JSON {}", value.type_name());
Err(Error::json(msg))
}
}
}
fn json_schema(&self, custom_ops: &[(&str, JsonValue)]) -> JsonValue {
RegexMatcher::json_schema(self, custom_ops)
}
}
impl RegexMatcher {
fn compile_op<Ctx: ?Sized>(map: &Map<String, JsonValue>) -> Result<Operator<Ctx>> {
let (op, _value) = check_operator!(map);
Err(Error::UnknownOperator(op.clone()))
}
fn make_regex_set(patterns: &[JsonValue]) -> Result<RegexSet> {
let patterns = (patterns.iter())
.map(|v| match v {
JsonValue::String(s) => Ok(s),
_ => {
let msg = format!("expected string, got {} in regex array", v.type_name());
Err(Error::json(msg))
}
})
.collect::<Result<Vec<_>>>()?;
Ok(RegexSet::new(&patterns)?)
}
fn json_schema(&self, custom_ops: &[(&str, JsonValue)]) -> JsonValue {
let string_schema = json!({ "type": "string" });
let string_array_schema = json!({ "type": "array", "items": string_schema });
let mut properties = Map::new();
for (op, schema) in custom_ops {
properties.insert(op.to_string(), schema.clone());
}
json!({
"oneOf": [
string_schema,
string_array_schema,
{
"type": "object",
"properties": properties,
"additionalProperties": false,
"minProperties": 1,
"maxProperties": 1
}
]
})
}
}
pub struct NumberMatcher;
impl<Ctx: ?Sized> Matcher<Ctx> for NumberMatcher {
fn compile(&self, value: &JsonValue) -> Result<Operator<Ctx>> {
match value {
JsonValue::Number(n) => Ok(Operator::Equal(Value::Number(n.clone()))),
JsonValue::Array(seq) => Ok(Operator::InSet(Self::make_hashset(seq)?)),
JsonValue::Object(map) => Self::compile_op(map),
_ => {
let msg = format!("unexpected JSON {}", value.type_name());
Err(Error::json(msg))
}
}
}
fn json_schema(&self, custom_ops: &[(&str, JsonValue)]) -> JsonValue {
NumberMatcher::json_schema(self, custom_ops)
}
}
impl NumberMatcher {
fn compile_op<Ctx: ?Sized>(map: &Map<String, JsonValue>) -> Result<Operator<Ctx>> {
let (op, value) = check_operator!(map);
match (op.as_str(), value) {
("<", JsonValue::Number(n)) => Ok(Operator::LessThan(Value::Number(n.clone()))),
("<=", JsonValue::Number(n)) => Ok(Operator::LessThanOrEqual(Value::Number(n.clone()))),
(">", JsonValue::Number(n)) => Ok(Operator::GreaterThan(Value::Number(n.clone()))),
(">=", JsonValue::Number(n)) => {
Ok(Operator::GreaterThanOrEqual(Value::Number(n.clone())))
}
("==", JsonValue::Number(n)) => Ok(Operator::Equal(Value::Number(n.clone()))),
("<" | "<=" | ">" | ">=" | "==", _) => {
operator_error!(op, "expected number, got {}", value.type_name())
}
("in", JsonValue::Array(seq)) => Self::make_hashset(seq)
.map(Operator::InSet)
.map_err(|err| Error::operator(op, err)),
("in", _) => operator_error!(op, "expected array, got {}", value.type_name()),
_ => Err(Error::UnknownOperator(op.clone())),
}
}
fn make_hashset(arr: &[JsonValue]) -> Result<HashSet<Value<'static>>> {
arr.iter()
.map(|v| match v {
JsonValue::Number(n) => Ok(Value::Number(n.clone())),
_ => {
let msg = format!("got {} in number array", v.type_name());
Err(Error::json(msg))
}
})
.collect()
}
fn json_schema(&self, custom_ops: &[(&str, JsonValue)]) -> JsonValue {
let number_schema = json!({ "type": "number" });
let number_array_schema = json!({ "type": "array", "items": number_schema });
let mut properties = Map::new();
properties.insert("<".to_string(), number_schema.clone());
properties.insert("<=".to_string(), number_schema.clone());
properties.insert(">".to_string(), number_schema.clone());
properties.insert(">=".to_string(), number_schema.clone());
properties.insert("==".to_string(), number_schema.clone());
properties.insert("in".to_string(), number_array_schema.clone());
for (op, schema) in custom_ops {
properties.insert(op.to_string(), schema.clone());
}
json!({
"oneOf": [
number_schema,
number_array_schema,
{
"type": "object",
"properties": properties,
"additionalProperties": false,
"minProperties": 1,
"maxProperties": 1
}
]
})
}
}
pub struct BoolMatcher;
impl<Ctx: ?Sized> Matcher<Ctx> for BoolMatcher {
fn compile(&self, value: &JsonValue) -> Result<Operator<Ctx>> {
match value {
JsonValue::Bool(b) => Ok(Operator::Equal(Value::Bool(*b))),
_ => {
let msg = format!("expected boolean, got {}", value.type_name());
Err(Error::json(msg))
}
}
}
fn json_schema(&self, custom_ops: &[(&str, JsonValue)]) -> JsonValue {
BoolMatcher::json_schema(self, custom_ops)
}
}
impl BoolMatcher {
fn json_schema(&self, _custom_ops: &[(&str, JsonValue)]) -> JsonValue {
json!({ "type": "boolean" })
}
}
pub struct IpMatcher;
impl<Ctx: ?Sized> Matcher<Ctx> for IpMatcher {
fn compile(&self, value: &JsonValue) -> Result<Operator<Ctx>> {
match value {
JsonValue::String(_) => {
let value_slice = std::slice::from_ref(value);
Ok(Operator::IpSet(Self::make_ipnet(value_slice)?))
}
JsonValue::Array(addrs) => Ok(Operator::IpSet(Self::make_ipnet(addrs)?)),
JsonValue::Object(map) => Self::compile_op(map),
_ => {
let msg = format!("unexpected JSON {}", value.type_name());
Err(Error::json(msg))
}
}
}
fn json_schema(&self, custom_ops: &[(&str, JsonValue)]) -> JsonValue {
IpMatcher::json_schema(self, custom_ops)
}
}
impl IpMatcher {
fn compile_op<Ctx: ?Sized>(map: &Map<String, JsonValue>) -> Result<Operator<Ctx>> {
let (op, _value) = check_operator!(map);
Err(Error::UnknownOperator(op.clone()))
}
fn make_ipnet(addrs: &[JsonValue]) -> Result<IpnetTrie<()>> {
let mut table = IpnetTrie::new();
for addr in addrs {
let addr = match addr {
JsonValue::String(s) => s,
_ => {
let msg = format!("got {} in ipnet array", addr.type_name());
return Err(Error::json(msg));
}
};
let net = if addr.contains('/') {
IpNet::from_str(addr)?
} else {
IpNet::from(IpAddr::from_str(addr)?)
};
table.insert(net, ());
}
Ok(table)
}
fn json_schema(&self, custom_ops: &[(&str, JsonValue)]) -> JsonValue {
let ip_pattern = format!(r"^(?:{IPV4_PATTERN}|(?i:{IPV6_PATTERN}))");
let ip_schema = json!({ "type": "string", "pattern": ip_pattern });
let ip_array_schema = json!({ "type": "array", "items": ip_schema });
let mut properties = Map::new();
for (op, schema) in custom_ops {
properties.insert(op.to_string(), schema.clone());
}
json!({
"oneOf": [
ip_schema,
ip_array_schema,
{
"type": "object",
"properties": properties,
"additionalProperties": false,
"minProperties": 1,
"maxProperties": 1
}
]
})
}
}
#[cfg(test)]
mod tests {
use std::any::{Any, TypeId};
use serde_json::json;
use super::*;
#[track_caller]
fn assert_compile_error<M>(matcher: M, value: JsonValue, expected_msg: &str)
where
M: Matcher<()>,
{
let result = matcher.compile(&value);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(
err.contains(expected_msg),
"Expected error message to contain `{expected_msg}` but got `{err}`",
);
}
#[track_caller]
fn compile_op<T: Any>(matcher: impl Matcher<()>, value: JsonValue) -> (T, &'static str) {
let type_id = TypeId::of::<T>();
let op = (matcher.compile(&value)).expect("Failed to compile operator");
let (boxed, variant): (Box<dyn Any>, &'static str) = match op {
Operator::Equal(val) if type_id == val.type_id() => (Box::new(val), "Equal"),
Operator::LessThan(val) if type_id == val.type_id() => (Box::new(val), "LessThan"),
Operator::LessThanOrEqual(val) if type_id == val.type_id() => {
(Box::new(val), "LessThanOrEqual")
}
Operator::GreaterThan(val) if type_id == val.type_id() => {
(Box::new(val), "GreaterThan")
}
Operator::GreaterThanOrEqual(val) if type_id == val.type_id() => {
(Box::new(val), "GreaterThanOrEqual")
}
Operator::InSet(val) if type_id == val.type_id() => (Box::new(val), "InSet"),
Operator::Regex(val) if type_id == val.type_id() => (Box::new(val), "Regex"),
Operator::RegexSet(val) if type_id == val.type_id() => (Box::new(val), "RegexSet"),
Operator::IpSet(val) if type_id == TypeId::of::<IpnetTrie<()>>() => {
(Box::new(val), "IpSet")
}
op => panic!("Unexpected operator type or value type mismatch: {op:?}"),
};
(*boxed.downcast::<T>().unwrap(), variant)
}
#[test]
fn test_default_matcher() {
#[track_caller]
fn assert_default_compile_error(value: JsonValue, expected_msg: &str) {
assert_compile_error(DefaultMatcher, value, expected_msg);
}
let (v, variant) = compile_op::<Value>(DefaultMatcher, json!(null));
assert_eq!(variant, "Equal");
assert_eq!(v, Value::None);
let (v, variant) = compile_op::<Value>(DefaultMatcher, json!(true));
assert_eq!(variant, "Equal");
assert_eq!(v, Value::Bool(true));
let (v, variant) = compile_op::<Value>(DefaultMatcher, json!(42));
assert_eq!(variant, "Equal");
assert_eq!(v, Value::from(42));
let (v, variant) = compile_op::<Value>(DefaultMatcher, json!("hello"));
assert_eq!(variant, "Equal");
assert_eq!(v, Value::from("hello"));
let (set, variant) =
compile_op::<HashSet<Value>>(DefaultMatcher, json!([1, "hello", true]));
assert_eq!(variant, "InSet");
assert_eq!(set.len(), 3);
assert!(set.contains(&Value::from(1)));
assert!(set.contains(&Value::from("hello")));
assert!(set.contains(&Value::Bool(true)));
let (v, variant) = compile_op::<Value>(DefaultMatcher, json!({"<": 100}));
assert_eq!(variant, "LessThan");
assert_eq!(v, Value::from(100));
let (v, variant) = compile_op::<Value>(DefaultMatcher, json!({"<=": "hello"}));
assert_eq!(variant, "LessThanOrEqual");
assert_eq!(v, Value::from("hello"));
let (v, variant) = compile_op::<Value>(DefaultMatcher, json!({">": 100}));
assert_eq!(variant, "GreaterThan");
assert_eq!(v, Value::from(100));
let (v, variant) = compile_op::<Value>(DefaultMatcher, json!({">=": true}));
assert_eq!(variant, "GreaterThanOrEqual");
assert_eq!(v, Value::Bool(true));
let (v, variant) = compile_op::<Value>(DefaultMatcher, json!({"==": null}));
assert_eq!(variant, "Equal");
assert_eq!(v, Value::None);
let (set, variant) =
compile_op::<HashSet<Value>>(DefaultMatcher, json!({"in": [1, "hello", true, null]}));
assert_eq!(variant, "InSet");
assert_eq!(set.len(), 4);
assert!(set.contains(&Value::from(1)));
assert!(set.contains(&Value::from("hello")));
assert!(set.contains(&Value::Bool(true)));
assert!(set.contains(&Value::None));
let (re, variant) = compile_op::<Regex>(DefaultMatcher, json!({"re": "^hello$"}));
assert_eq!(variant, "Regex");
assert!(re.is_match("hello"));
assert!(!re.is_match("hello world"));
let (re_set, variant) =
compile_op::<RegexSet>(DefaultMatcher, json!({"re": ["^hello$", "^world$"]}));
assert_eq!(variant, "RegexSet");
assert!(re_set.is_match("hello"));
assert!(re_set.is_match("world"));
assert!(!re_set.is_match("hello world"));
let (_, variant) = compile_op::<IpnetTrie<()>>(
DefaultMatcher,
json!({"ip": ["192.168.1.1", "10.0.0.0/8"]}),
);
assert_eq!(variant, "IpSet");
assert_default_compile_error(
json!({"in": true}),
"error in 'in' operator: expected array, got boolean",
);
assert_default_compile_error(
json!({"re": true}),
"error in 're' operator: expected string or array, got boolean",
);
assert_default_compile_error(
json!({"ip": true}),
"error in 'ip' operator: expected array, got boolean",
);
assert_default_compile_error(json!({"unknown": "value"}), "unknown operator 'unknown'");
}
#[test]
fn test_string_matcher() {
#[track_caller]
fn assert_str_compile_error(value: JsonValue, expected_msg: &str) {
assert_compile_error(StringMatcher, value, expected_msg);
}
let (s, variant) = compile_op::<Value>(StringMatcher, json!("hello"));
assert_eq!(variant, "Equal");
assert_eq!(s, Value::from("hello"));
let (set, variant) = compile_op::<HashSet<Value>>(StringMatcher, json!(["hello", "world"]));
assert_eq!(variant, "InSet");
assert_eq!(set.len(), 2);
assert!(set.contains(&Value::from("hello")));
assert!(set.contains(&Value::from("world")));
let (s, variant) = compile_op::<Value>(StringMatcher, json!({"<": "hello"}));
assert_eq!(variant, "LessThan");
assert_eq!(s, Value::from("hello"));
let (s, variant) = compile_op::<Value>(StringMatcher, json!({"<=": "hello"}));
assert_eq!(variant, "LessThanOrEqual");
assert_eq!(s, Value::from("hello"));
let (s, variant) = compile_op::<Value>(StringMatcher, json!({">": "hello"}));
assert_eq!(variant, "GreaterThan");
assert_eq!(s, Value::from("hello"));
let (s, variant) = compile_op::<Value>(StringMatcher, json!({">=": "hello"}));
assert_eq!(variant, "GreaterThanOrEqual");
assert_eq!(s, Value::from("hello"));
let (s, variant) = compile_op::<Value>(StringMatcher, json!({"==": "hello"}));
assert_eq!(variant, "Equal");
assert_eq!(s, Value::from("hello"));
let (set, variant) =
compile_op::<HashSet<Value>>(StringMatcher, json!({"in": ["hello", "world"]}));
assert_eq!(variant, "InSet");
assert_eq!(set.len(), 2);
assert!(set.contains(&Value::from("hello")));
assert!(set.contains(&Value::from("world")));
let (re, variant) = compile_op::<Regex>(StringMatcher, json!({"re": "^hello$"}));
assert_eq!(variant, "Regex");
assert!(re.is_match("hello"));
let (re, variant) =
compile_op::<RegexSet>(StringMatcher, json!({"re": ["^hello$", "^world$"]}));
assert_eq!(variant, "RegexSet");
assert!(re.is_match("hello"));
assert!(!re.is_match("hello world"));
assert_str_compile_error(json!(true), "unexpected JSON boolean");
assert_str_compile_error(json!({"in": true}), "expected array, got boolean");
assert_str_compile_error(
json!({"<": true}),
"error in '<' operator: expected string, got boolean",
);
assert_str_compile_error(
json!({"re": true}),
"error in 're' operator: expected string or array, got boolean",
);
assert_str_compile_error(json!({"unknown": "value"}), "unknown operator 'unknown'");
}
#[test]
fn test_regex_matcher() {
#[track_caller]
fn assert_regex_compile_error(value: JsonValue, expected_msg: &str) {
assert_compile_error(RegexMatcher, value, expected_msg);
}
let (re, variant) = compile_op::<Regex>(RegexMatcher, json!("^hello$"));
assert_eq!(variant, "Regex");
assert!(re.is_match("hello"));
assert!(!re.is_match("hello world"));
let (re_set, variant) = compile_op::<RegexSet>(RegexMatcher, json!(["^hello$", "^world$"]));
assert_eq!(variant, "RegexSet");
assert!(re_set.is_match("hello"));
assert!(re_set.is_match("world"));
assert!(!re_set.is_match("hello world"));
assert_regex_compile_error(json!(123), "unexpected JSON number");
assert_regex_compile_error(json!(true), "unexpected JSON boolean");
assert_regex_compile_error(json!({"invalid": "pattern"}), "unknown operator 'invalid'");
assert_regex_compile_error(json!("(invalid"), "regex parse error");
}
#[test]
fn test_number_matcher() {
#[track_caller]
fn assert_num_compile_error(value: JsonValue, expected_msg: &str) {
assert_compile_error(NumberMatcher, value, expected_msg);
}
let (n, variant) = compile_op::<Value>(NumberMatcher, json!(42));
assert_eq!(variant, "Equal");
assert_eq!(n, Value::from(42));
let (n, variant) = compile_op::<Value>(NumberMatcher, json!(3.14));
assert_eq!(variant, "Equal");
assert_eq!(n, Value::try_from(3.14).unwrap());
let (set, variant) = compile_op::<HashSet<Value>>(NumberMatcher, json!([1, 3]));
assert_eq!(variant, "InSet");
assert_eq!(set.len(), 2);
assert!(set.contains(&Value::from(1)));
assert!(!set.contains(&Value::from(2)));
assert!(set.contains(&Value::from(3)));
let (n, variant) = compile_op::<Value>(NumberMatcher, json!({"<": 100}));
assert_eq!(variant, "LessThan");
assert_eq!(n, Value::from(100));
let (n, variant) = compile_op::<Value>(NumberMatcher, json!({"<=": 100}));
assert_eq!(variant, "LessThanOrEqual");
assert_eq!(n, Value::from(100));
let (n, variant) = compile_op::<Value>(NumberMatcher, json!({">": 100}));
assert_eq!(variant, "GreaterThan");
assert_eq!(n, Value::from(100));
let (n, variant) = compile_op::<Value>(NumberMatcher, json!({">=": 100}));
assert_eq!(variant, "GreaterThanOrEqual");
assert_eq!(n, Value::from(100));
let (n, variant) = compile_op::<Value>(NumberMatcher, json!({"==": 100}));
assert_eq!(variant, "Equal");
assert_eq!(n, Value::from(100));
let (set, variant) = compile_op::<HashSet<Value>>(NumberMatcher, json!({"in": [1, 3]}));
assert_eq!(variant, "InSet");
assert_eq!(set.len(), 2);
assert!(set.contains(&Value::from(1)));
assert!(!set.contains(&Value::from(2)));
assert!(set.contains(&Value::from(3)));
let (set, variant) = compile_op::<HashSet<Value>>(NumberMatcher, json!({"in": [1.5, 3.5]}));
assert_eq!(variant, "InSet");
assert_eq!(set.len(), 2);
assert!(set.contains(&Value::try_from(1.5).unwrap()));
assert!(!set.contains(&Value::try_from(2.5).unwrap()));
assert!(set.contains(&Value::try_from(3.5).unwrap()));
assert_num_compile_error(json!("string"), "unexpected JSON string");
assert_num_compile_error(json!(true), "unexpected JSON boolean");
assert_num_compile_error(
json!({"<": "string"}),
"error in '<' operator: expected number, got string",
);
assert_num_compile_error(
json!({"in": true}),
"error in 'in' operator: expected array, got boolean",
);
assert_num_compile_error(
json!({"in": [1, "string"]}),
"error in 'in' operator: got string in number array",
);
assert_num_compile_error(json!({"unknown": 100}), "unknown operator 'unknown'");
}
#[test]
fn test_bool_matcher() {
let (b, variant) = compile_op::<Value>(BoolMatcher, json!(true));
assert_eq!(variant, "Equal");
assert_eq!(b, Value::Bool(true));
let (b, variant) = compile_op::<Value>(BoolMatcher, json!(false));
assert_eq!(variant, "Equal");
assert_eq!(b, Value::Bool(false));
assert_compile_error(BoolMatcher, json!("string"), "expected boolean, got string");
assert_compile_error(BoolMatcher, json!(123), "expected boolean, got number");
assert_compile_error(BoolMatcher, json!([true]), "expected boolean, got array");
assert_compile_error(
BoolMatcher,
json!({"==": true}),
"expected boolean, got object",
);
}
#[test]
fn test_ip_matcher() {
fn ip(s: &str) -> IpNet {
IpNet::from(IpAddr::from_str(s).unwrap())
}
#[track_caller]
fn assert_ip_compile_error(value: JsonValue, expected_msg: &str) {
assert_compile_error(IpMatcher, value, expected_msg);
}
#[track_caller]
fn assert_ip_matches(trie: &IpnetTrie<()>, addr: &str) {
let ip = ip(addr);
assert!(trie.longest_match(&ip).is_some(), "{addr} should match");
}
#[track_caller]
fn assert_ip_not_matches(trie: &IpnetTrie<()>, addr: &str) {
let ip = ip(addr);
assert!(trie.longest_match(&ip).is_none(), "{addr} should not match");
}
let (trie, variant) = compile_op::<IpnetTrie<()>>(IpMatcher, json!("192.168.1.1"));
assert_eq!(variant, "IpSet");
assert_ip_matches(&trie, "192.168.1.1");
assert_ip_not_matches(&trie, "192.168.1.2");
let (trie, variant) = compile_op::<IpnetTrie<()>>(IpMatcher, json!("192.168.1.0/24"));
assert_eq!(variant, "IpSet");
assert_ip_matches(&trie, "192.168.1.1");
assert_ip_matches(&trie, "192.168.1.254");
assert_ip_not_matches(&trie, "192.168.2.1");
let (trie, variant) =
compile_op::<IpnetTrie<()>>(IpMatcher, json!(["192.168.1.1", "10.0.0.0/8"]));
assert_eq!(variant, "IpSet");
assert_ip_matches(&trie, "192.168.1.1");
assert_ip_matches(&trie, "10.1.2.3");
assert_ip_not_matches(&trie, "11.0.0.1");
let (trie, variant) =
compile_op::<IpnetTrie<()>>(IpMatcher, json!(["2001:db8::1", "2001:db8::/32"]));
assert_eq!(variant, "IpSet");
assert_ip_matches(&trie, "2001:db8:1::1");
assert_ip_compile_error(json!("invalid-ip"), "invalid IP address syntax");
assert_ip_compile_error(json!(123), "unexpected JSON number");
assert_ip_compile_error(json!({"invalid": "pattern"}), "unknown operator 'invalid'");
}
}