use serde_json::{Map, Value as Json};
use super::ast::{ComparisonOp, FieldPath, Literal, Predicate, Projection, Query, QueryError};
pub fn matches(q: &Query, target: &Json) -> Result<bool, QueryError> {
eval_pred(&q.predicate, target)
}
pub fn project(q: &Query, target: &Json) -> Json {
match &q.projection {
Projection::All => target.clone(),
Projection::Fields(paths) => {
let mut out = Map::new();
for p in paths {
if let Some(v) = lookup(p, target) {
insert_path(&mut out, p.segments(), v.clone());
}
}
Json::Object(out)
}
}
}
fn eval_pred(p: &Predicate, t: &Json) -> Result<bool, QueryError> {
match p {
Predicate::True => Ok(true),
Predicate::False => Ok(false),
Predicate::And(items) => {
for i in items {
if !eval_pred(i, t)? {
return Ok(false);
}
}
Ok(true)
}
Predicate::Or(items) => {
for i in items {
if eval_pred(i, t)? {
return Ok(true);
}
}
Ok(false)
}
Predicate::Not(inner) => Ok(!eval_pred(inner, t)?),
Predicate::Compare { left, op, right } => Ok(compare(op, lookup(left, t), right)),
Predicate::Contains { path, value } => match lookup(path, t) {
Some(Json::Array(items)) => Ok(items.iter().any(|i| json_eq(i, value))),
_ => Ok(false),
},
Predicate::Exists(path) => Ok(lookup(path, t).is_some()),
}
}
fn lookup<'a>(p: &FieldPath, t: &'a Json) -> Option<&'a Json> {
let mut cur = t;
for seg in p.segments() {
match cur {
Json::Object(m) => match m.get(seg) {
Some(v) => cur = v,
None => return None,
},
_ => return None,
}
}
Some(cur)
}
fn compare(op: &ComparisonOp, got: Option<&Json>, rhs: &Literal) -> bool {
use ComparisonOp::*;
match op {
Eq => match got {
Some(v) => json_eq(v, rhs),
None => matches!(rhs, Literal::Null),
},
Ne => match got {
Some(v) => !json_eq(v, rhs),
None => !matches!(rhs, Literal::Null),
},
Lt | Le | Gt | Ge => match (got, rhs) {
(Some(Json::Number(n)), Literal::Number(r)) => match n.as_f64() {
Some(l) => match op {
Lt => l < *r,
Le => l <= *r,
Gt => l > *r,
Ge => l >= *r,
_ => unreachable!(),
},
None => false,
},
(Some(Json::String(s)), Literal::String(r)) => match op {
Lt => s.as_str() < r.as_str(),
Le => s.as_str() <= r.as_str(),
Gt => s.as_str() > r.as_str(),
Ge => s.as_str() >= r.as_str(),
_ => unreachable!(),
},
_ => false,
},
Like => match (got, rhs) {
(Some(Json::String(s)), Literal::String(pat)) => glob_match(pat, s),
_ => false,
},
InSet => match rhs {
Literal::Array(items) => match got {
Some(g) => items.iter().any(|i| json_eq(g, i)),
None => false,
},
_ => false,
},
}
}
fn json_eq(j: &Json, c: &Literal) -> bool {
match (j, c) {
(Json::String(a), Literal::String(b)) => a == b,
(Json::Number(a), Literal::Number(b)) => a.as_f64() == Some(*b),
(Json::Bool(a), Literal::Bool(b)) => a == b,
(Json::Null, Literal::Null) => true,
_ => false,
}
}
fn glob_match(pat: &str, s: &str) -> bool {
fn helper(p: &[char], s: &[char]) -> bool {
match (p.first(), s.first()) {
(None, None) => true,
(Some('*'), _) => {
if helper(&p[1..], s) {
return true;
}
if s.is_empty() {
return false;
}
helper(p, &s[1..])
}
(Some('?'), Some(_)) => helper(&p[1..], &s[1..]),
(Some(pc), Some(sc)) if pc == sc => helper(&p[1..], &s[1..]),
_ => false,
}
}
let p: Vec<char> = pat.chars().collect();
let s: Vec<char> = s.chars().collect();
helper(&p, &s)
}
fn insert_path(out: &mut Map<String, Json>, segs: &[String], value: Json) {
if segs.is_empty() {
return;
}
if segs.len() == 1 {
out.insert(segs[0].clone(), value);
return;
}
let next = out
.entry(segs[0].clone())
.or_insert_with(|| Json::Object(Map::new()));
if let Json::Object(m) = next {
insert_path(m, &segs[1..], value);
}
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::super::ast::{ComparisonOp, FieldPath, Literal, Predicate, Projection, Query};
use super::*;
fn q_with(predicate: Predicate) -> Query {
Query {
projection: Projection::All,
predicate,
}
}
fn cmp(path: &str, op: ComparisonOp, right: Literal) -> Predicate {
Predicate::Compare {
left: FieldPath::parse(path),
op,
right,
}
}
#[test]
fn matches_compare_eq_on_top_level_field() {
let q = q_with(cmp("kind", ComparisonOp::Eq, Literal::String("led".into())));
assert!(matches(&q, &json!({"kind": "led"})).unwrap());
assert!(!matches(&q, &json!({"kind": "motion"})).unwrap());
}
#[test]
fn type_field_does_not_alias_kind_field() {
let q = q_with(cmp("type", ComparisonOp::Eq, Literal::String("led".into())));
assert!(!matches(&q, &json!({"kind": "led"})).unwrap());
assert!(matches(&q, &json!({"type": "led"})).unwrap());
}
#[test]
fn nested_properties_type_resolves_literally() {
let q = q_with(cmp(
"properties.type",
ComparisonOp::Eq,
Literal::String("shadow".into()),
));
assert!(
matches(
&q,
&json!({"kind": "led", "properties": {"type": "shadow"}})
)
.unwrap()
);
assert!(
!matches(
&q,
&json!({"kind": "shadow", "properties": {"type": "led"}})
)
.unwrap()
);
}
#[test]
fn matches_compare_ne_with_missing_field_is_true() {
let q = q_with(cmp("kind", ComparisonOp::Ne, Literal::String("x".into())));
assert!(matches(&q, &json!({})).unwrap());
}
#[test]
fn matches_compare_lt_le_gt_ge_on_number() {
let target = json!({"n": 50.0});
assert!(
matches(
&q_with(cmp("n", ComparisonOp::Lt, Literal::Number(60.0))),
&target
)
.unwrap()
);
assert!(
matches(
&q_with(cmp("n", ComparisonOp::Le, Literal::Number(50.0))),
&target
)
.unwrap()
);
assert!(
matches(
&q_with(cmp("n", ComparisonOp::Gt, Literal::Number(40.0))),
&target
)
.unwrap()
);
assert!(
matches(
&q_with(cmp("n", ComparisonOp::Ge, Literal::Number(50.0))),
&target
)
.unwrap()
);
assert!(
!matches(
&q_with(cmp("n", ComparisonOp::Lt, Literal::Number(40.0))),
&target
)
.unwrap()
);
}
#[test]
fn matches_compare_like_glob() {
let q = q_with(cmp(
"name",
ComparisonOp::Like,
Literal::String("kitchen-*".into()),
));
assert!(matches(&q, &json!({"name": "kitchen-led"})).unwrap());
assert!(matches(&q, &json!({"name": "kitchen-"})).unwrap());
assert!(!matches(&q, &json!({"name": "living-led"})).unwrap());
}
#[test]
fn matches_compare_in_set() {
let q = q_with(cmp(
"kind",
ComparisonOp::InSet,
Literal::Array(vec![
Literal::String("led".into()),
Literal::String("switch".into()),
]),
));
assert!(matches(&q, &json!({"kind": "led"})).unwrap());
assert!(matches(&q, &json!({"kind": "switch"})).unwrap());
assert!(!matches(&q, &json!({"kind": "motion"})).unwrap());
}
#[test]
fn matches_and_or_not_compose() {
let on = cmp("state", ComparisonOp::Eq, Literal::String("on".into()));
let led = cmp("kind", ComparisonOp::Eq, Literal::String("led".into()));
let q_and = q_with(Predicate::and(vec![on.clone(), led.clone()]));
assert!(matches(&q_and, &json!({"kind": "led", "state": "on"})).unwrap());
assert!(!matches(&q_and, &json!({"kind": "led", "state": "off"})).unwrap());
let q_or = q_with(Predicate::or(vec![on.clone(), led.clone()]));
assert!(matches(&q_or, &json!({"kind": "switch", "state": "on"})).unwrap());
assert!(matches(&q_or, &json!({"kind": "led", "state": "off"})).unwrap());
assert!(!matches(&q_or, &json!({"kind": "switch", "state": "off"})).unwrap());
let q_not = q_with(Predicate::not(on));
assert!(!matches(&q_not, &json!({"state": "on"})).unwrap());
assert!(matches(&q_not, &json!({"state": "off"})).unwrap());
}
#[test]
fn matches_nested_path_exists_and_contains() {
let target = json!({
"affordances": { "transitions": { "available": ["turn-on"] } }
});
let q_exists = q_with(Predicate::exists(FieldPath::parse(
"affordances.transitions.available",
)));
assert!(matches(&q_exists, &target).unwrap());
let q_contains = q_with(Predicate::contains(
FieldPath::parse("affordances.transitions.available"),
Literal::String("turn-on".into()),
));
assert!(matches(&q_contains, &target).unwrap());
}
#[test]
fn matches_contains_on_array_returns_true_when_value_present() {
let q = q_with(Predicate::contains(
FieldPath::parse("labels"),
Literal::String("urgent".into()),
));
assert!(matches(&q, &json!({"labels": ["urgent", "cron"]})).unwrap());
}
#[test]
fn matches_contains_on_array_returns_false_when_absent() {
let q = q_with(Predicate::contains(
FieldPath::parse("labels"),
Literal::String("urgent".into()),
));
assert!(!matches(&q, &json!({"labels": ["cron"]})).unwrap());
}
#[test]
fn matches_contains_on_scalar_field_returns_false() {
let q = q_with(Predicate::contains(
FieldPath::parse("labels"),
Literal::String("urgent".into()),
));
assert!(!matches(&q, &json!({"labels": "urgent"})).unwrap());
}
#[test]
fn matches_exists_returns_true_for_present_field_including_null() {
let q = q_with(Predicate::exists(FieldPath::parse("name")));
assert!(matches(&q, &json!({"name": null})).unwrap());
assert!(matches(&q, &json!({"name": "led"})).unwrap());
}
#[test]
fn matches_exists_returns_false_for_missing_field() {
let q = q_with(Predicate::exists(FieldPath::parse("name")));
assert!(!matches(&q, &json!({})).unwrap());
}
#[test]
fn project_all_returns_target_unchanged() {
let q = Query {
projection: Projection::All,
predicate: Predicate::True,
};
let t = json!({"a": 1, "b": 2});
assert_eq!(project(&q, &t), t);
}
#[test]
fn project_fields_returns_only_requested_paths() {
let q = Query {
projection: Projection::Fields(vec![FieldPath::parse("data.degreesC")]),
predicate: Predicate::True,
};
let t = json!({"data": {"degreesC": 30, "degreesF": 90}, "other": 1});
assert_eq!(project(&q, &t), json!({"data": {"degreesC": 30}}));
}
#[test]
fn project_missing_field_is_omitted() {
let q = Query {
projection: Projection::Fields(vec![
FieldPath::parse("present"),
FieldPath::parse("missing"),
]),
predicate: Predicate::True,
};
let t = json!({"present": 1});
assert_eq!(project(&q, &t), json!({"present": 1}));
}
}