use yo_common::{Code, Error, Result};
use crate::filter::{Arith, Expr, Fun, Item, Num, Op, Operand, Pattern};
use crate::head::{DEPTH_MAX, Kind};
use crate::path::Step;
use crate::read::Value;
#[derive(Debug, Clone)]
pub struct Path<'a> {
sels: Vec<Sel<'a>>,
legacy: bool,
proj: Option<Operand<'a>>,
}
#[derive(Debug, Clone, Copy)]
pub enum Computed<'d> {
Value(Value<'d>),
Name(&'d [u8]),
Int(i64),
Float(f64),
}
impl Computed<'_> {
pub fn write_json_at(
&self,
f: &crate::Format<'_>,
out: &mut Vec<u8>,
depth: usize,
) -> Result<()> {
match self {
Computed::Value(v) => v.write_json_at(f, out, depth),
Computed::Name(k) => {
crate::text::write_string(k, out);
Ok(())
}
Computed::Int(i) => {
crate::text::write_int(*i, out);
Ok(())
}
Computed::Float(x) => crate::text::write_float(*x, out),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum Sel<'a> {
Key(&'a [u8]),
Index(i64),
Wild,
Descend,
Union(Vec<Sel<'a>>),
Slice {
from: Option<i64>,
to: Option<i64>,
step: i64,
},
Filter(Box<Expr<'a>>),
}
impl<'a> Path<'a> {
pub fn parse(path: &'a [u8]) -> Result<Path<'a>> {
if let Some(proj) = projection(path) {
return Ok(Path {
sels: Vec::new(),
legacy: false,
proj: Some(proj),
});
}
let (rest, legacy) = match path.strip_prefix(b"$") {
Some(rest) => (rest, false),
None => (path, true),
};
let mut p = Parse {
rest,
at: 0,
legacy,
sels: Vec::new(),
};
p.run()?;
Ok(Path {
sels: p.sels,
legacy,
proj: None,
})
}
#[must_use]
pub fn is_projection(&self) -> bool {
self.proj.is_some()
}
#[must_use]
pub fn project<'d>(&'d self, root: &Value<'d>) -> Vec<Computed<'d>> {
let Some(o) = &self.proj else {
return Vec::new();
};
crate::filter::project(o, root)
.into_iter()
.map(|it| match it {
Item::Ref(v) => Computed::Value(v),
Item::Key(k) => Computed::Name(k),
Item::Num(Num::Int(i)) => Computed::Int(i),
Item::Num(Num::Float(x)) => Computed::Float(x),
})
.collect()
}
#[must_use]
pub fn legacy(&self) -> bool {
self.legacy
}
#[must_use]
pub fn is_root(&self) -> bool {
self.sels.is_empty()
}
#[must_use]
pub fn is_definite(&self) -> bool {
self.sels
.iter()
.all(|s| matches!(s, Sel::Key(_) | Sel::Index(_)))
}
pub fn select<'d>(&self, root: &Value<'d>, out: &mut Vec<Value<'d>>) {
select_from(&self.sels, root, root, out);
}
#[must_use]
pub fn split_last(&self) -> Option<(Path<'a>, Step<'a>)> {
let step = match self.sels.last()? {
Sel::Key(k) => Step::Key(k),
Sel::Index(i) => Step::Index(*i),
_ => return None,
};
let parent = Path {
sels: self.sels[..self.sels.len() - 1].to_vec(),
legacy: self.legacy,
proj: None,
};
Some((parent, step))
}
#[must_use]
pub fn first<'d>(&self, root: &Value<'d>) -> Option<Value<'d>> {
let mut out = Vec::new();
self.select(root, &mut out);
out.into_iter().next()
}
}
pub(crate) fn select_from<'d>(
sels: &[Sel<'_>],
start: &Value<'d>,
root: &Value<'d>,
out: &mut Vec<Value<'d>>,
) {
let mut cur = vec![*start];
let mut next = Vec::new();
for sel in sels {
next.clear();
for v in &cur {
apply(sel, root, v, &mut next);
}
core::mem::swap(&mut cur, &mut next);
if cur.is_empty() {
return;
}
}
out.append(&mut cur);
}
fn apply<'d>(sel: &Sel<'_>, root: &Value<'d>, v: &Value<'d>, out: &mut Vec<Value<'d>>) {
match sel {
Sel::Key(k) => out.extend(v.get(k)),
Sel::Index(i) => out.extend(index(v, *i)),
Sel::Wild => out.extend(v.iter()),
Sel::Descend => descend(v, out, 0),
Sel::Union(items) => {
for item in items {
apply(item, root, v, out);
}
}
Sel::Slice { from, to, step } => slice(v, *from, *to, *step, out),
Sel::Filter(e) => out.extend(v.iter().filter(|child| e.holds(root, child))),
}
}
fn index<'d>(v: &Value<'d>, i: i64) -> Option<Value<'d>> {
if v.kind() != Kind::Array {
return None;
}
v.at(place(i, v.len())?)
}
fn place(i: i64, n: usize) -> Option<usize> {
if i < 0 {
n.checked_sub(i.unsigned_abs() as usize)
} else {
let at = i as usize;
(at < n).then_some(at)
}
}
fn descend<'d>(v: &Value<'d>, out: &mut Vec<Value<'d>>, depth: usize) {
out.push(*v);
if depth >= DEPTH_MAX {
return;
}
for child in v.iter() {
descend(&child, out, depth + 1);
}
}
fn slice<'d>(
v: &Value<'d>,
from: Option<i64>,
to: Option<i64>,
step: i64,
out: &mut Vec<Value<'d>>,
) {
if v.kind() != Kind::Array || step == 0 {
return;
}
let n = v.len() as i64;
let bound = |i: i64, lo: i64, hi: i64| {
let i = if i < 0 { n + i } else { i };
i.clamp(lo, hi)
};
if step > 0 {
let mut at = bound(from.unwrap_or(0), 0, n);
let end = bound(to.unwrap_or(n), 0, n);
while at < end {
out.extend(v.at(at as usize));
at += step;
}
} else {
let mut at = bound(from.unwrap_or(n - 1), -1, n - 1);
let end = bound(to.unwrap_or(-n - 1), -1, n - 1);
while at > end {
out.extend(v.at(at as usize));
at += step;
}
}
}
struct Parse<'a> {
rest: &'a [u8],
at: usize,
legacy: bool,
sels: Vec<Sel<'a>>,
}
impl<'a> Parse<'a> {
fn run(&mut self) -> Result<()> {
if self.legacy && self.rest == b"." {
return Ok(());
}
if self.legacy && self.at < self.rest.len() && !matches!(self.rest[self.at], b'.' | b'[') {
let name = self.name()?;
self.sels.push(Sel::Key(name));
}
while self.at < self.rest.len() {
match self.rest[self.at] {
b'.' if self.rest.get(self.at + 1) == Some(&b'.') => {
self.at += 2;
self.sels.push(Sel::Descend);
if self.at >= self.rest.len() {
return Err(self.bad("a `..` with nothing after it"));
}
if self.rest.get(self.at) == Some(&b'[') {
continue;
}
let sel = self.after_dot()?;
self.sels.push(sel);
}
b'.' => {
self.at += 1;
let sel = self.after_dot()?;
self.sels.push(sel);
}
b'[' => {
let sel = self.bracket()?;
self.sels.push(sel);
}
_ => return Err(self.bad("a step that does not start with `.` or `[`")),
}
}
Ok(())
}
fn after_dot(&mut self) -> Result<Sel<'a>> {
if self.rest.get(self.at) == Some(&b'*') {
self.at += 1;
return Ok(Sel::Wild);
}
Ok(Sel::Key(self.name()?))
}
fn name(&mut self) -> Result<&'a [u8]> {
let from = self.at;
while self.at < self.rest.len()
&& !matches!(self.rest[self.at], b'.' | b'[')
&& !ends_name(self.rest[self.at])
{
self.at += 1;
}
if self.at == from {
return Err(self.bad("a `.` with no name after it"));
}
Ok(&self.rest[from..self.at])
}
fn bracket(&mut self) -> Result<Sel<'a>> {
let body = &self.rest[self.at + 1..];
let Some(close) = closer(body, b']') else {
return Err(self.bad("a `[` with no `]` after it"));
};
let inner = &body[..close];
self.at += close + 2;
if let Some(rest) = inner.strip_prefix(b"?") {
return self.filter(rest);
}
if inner == b"*" {
return Ok(Sel::Wild);
}
if inner.contains(&b':') {
return self.slice(inner);
}
let mut items = Vec::new();
for part in inner.split(|&c| c == b',') {
items.push(self.one(trim(part))?);
}
match items.len() {
0 => Err(self.bad("an empty `[]`")),
1 => Ok(items.pop().expect("one item")),
_ => Ok(Sel::Union(items)),
}
}
fn one(&self, part: &'a [u8]) -> Result<Sel<'a>> {
if let Some(name) = quoted(part) {
return Ok(Sel::Key(name));
}
Ok(Sel::Index(self.int(part)?))
}
fn slice(&self, inner: &[u8]) -> Result<Sel<'a>> {
let mut parts = inner.split(|&c| c == b':');
let from = self.maybe(parts.next().unwrap_or(b""))?;
let to = self.maybe(parts.next().unwrap_or(b""))?;
let step = self.maybe(parts.next().unwrap_or(b""))?.unwrap_or(1);
if parts.next().is_some() {
return Err(self.bad("a slice has at most a start, an end and a step"));
}
if step == 0 {
return Err(self.bad("a slice with a step of zero"));
}
Ok(Sel::Slice { from, to, step })
}
fn maybe(&self, part: &[u8]) -> Result<Option<i64>> {
let part = trim(part);
if part.is_empty() {
return Ok(None);
}
Ok(Some(self.int(part)?))
}
fn int(&self, part: &[u8]) -> Result<i64> {
core::str::from_utf8(part)
.ok()
.and_then(|t| t.parse().ok())
.ok_or_else(|| self.bad("an index that is not a number"))
}
fn filter(&mut self, body: &'a [u8]) -> Result<Sel<'a>> {
let mut f = Filter {
body,
at: 0,
of: self.at,
top: false,
};
let e = f.or()?;
f.spaces();
if f.at < f.body.len() {
return Err(f.bad("a filter with something left over at the end of it"));
}
Ok(Sel::Filter(Box::new(e)))
}
fn bad(&self, what: &str) -> Error {
Error::fmt(
Code::Invalid,
format_args!("{what}, at byte {} of the path", self.at),
)
}
}
struct Filter<'a> {
body: &'a [u8],
at: usize,
of: usize,
top: bool,
}
impl<'a> Filter<'a> {
fn or(&mut self) -> Result<Expr<'a>> {
let mut e = self.and()?;
while self.word(b"||") {
e = Expr::Or(Box::new(e), Box::new(self.and()?));
}
Ok(e)
}
fn and(&mut self) -> Result<Expr<'a>> {
let mut e = self.unary()?;
while self.word(b"&&") {
e = Expr::And(Box::new(e), Box::new(self.unary()?));
}
Ok(e)
}
fn unary(&mut self) -> Result<Expr<'a>> {
self.spaces();
if self.word(b"!") {
return Ok(Expr::Not(Box::new(self.unary()?)));
}
let from = self.at;
if self.word(b"(") {
let e = self.or()?;
if !self.word(b")") {
return Err(self.bad("a `(` in a filter with no `)` after it"));
}
if !self.operator_next() {
return Ok(e);
}
self.at = from;
}
self.cmp()
}
fn operator_next(&mut self) -> bool {
self.spaces();
let rest = &self.body[self.at..];
if rest.first().is_some_and(|c| {
matches!(c, b'+' | b'-' | b'*' | b'/' | b'%' | b'<' | b'>' | b'=') || *c == b'!'
}) {
return rest[0] != b'!' || rest.starts_with(b"!=");
}
WORD_OPS.iter().any(|(text, _)| word_at(rest, text))
}
fn cmp(&mut self) -> Result<Expr<'a>> {
let left = self.operand()?;
let Some(op) = self.op() else {
return Ok(Expr::Test(left));
};
let mut right = self.operand()?;
if op == Op::Re {
if let Operand::Lit(bytes) = &right
&& let Some(v) = Value::new(bytes)
&& let Some(text) = v.text_bytes()
&& let Ok(pat) = Pattern::new(text)
{
right = Operand::Re(pat);
}
}
Ok(Expr::Cmp(left, op, right))
}
fn op(&mut self) -> Option<Op> {
self.spaces();
for (text, op) in [
(&b"=="[..], Op::Eq),
(b"!=", Op::Ne),
(b"<=", Op::Le),
(b">=", Op::Ge),
(b"=~", Op::Re),
(b"<", Op::Lt),
(b">", Op::Gt),
] {
if self.word(text) {
return Some(op);
}
}
for (text, op) in WORD_OPS {
if word_at(&self.body[self.at..], text) {
self.at += text.len();
return Some(*op);
}
}
None
}
fn operand(&mut self) -> Result<Operand<'a>> {
let mut e = self.product()?;
loop {
self.spaces();
let op = match self.body.get(self.at) {
Some(b'+') => Arith::Add,
Some(b'-') => Arith::Sub,
_ => break,
};
self.at += 1;
e = Operand::Math(Box::new(e), op, Box::new(self.product()?));
}
Ok(e)
}
fn product(&mut self) -> Result<Operand<'a>> {
let mut e = self.signed()?;
loop {
self.spaces();
let op = match self.body.get(self.at) {
Some(b'*') => Arith::Mul,
Some(b'/') => Arith::Div,
Some(b'%') => Arith::Rem,
_ => break,
};
self.at += 1;
e = Operand::Math(Box::new(e), op, Box::new(self.signed()?));
}
Ok(e)
}
fn signed(&mut self) -> Result<Operand<'a>> {
self.spaces();
let neg = match self.body.get(self.at) {
Some(b'-') => true,
Some(b'+') => false,
_ => return self.atom(),
};
self.at += 1;
Ok(Operand::Sign(Box::new(self.atom()?), neg))
}
fn atom(&mut self) -> Result<Operand<'a>> {
self.spaces();
let Some(&c) = self.body.get(self.at) else {
return Err(self.bad("a filter that stops where a value was expected"));
};
let mut e = if c == b'(' {
self.top = false;
self.at += 1;
let inner = self.operand()?;
if !self.word(b")") {
return Err(self.bad("a `(` in a filter with no `)` after it"));
}
inner
} else if c == b'@' || c == b'$' {
self.top = false;
self.at += 1;
self.path(c == b'@', false)?
} else if core::mem::take(&mut self.top) {
self.path(false, true)?
} else {
self.literal()?
};
if self.body.get(self.at) == Some(&b'~') {
self.at += 1;
e = Operand::Keys(Box::new(e));
}
Ok(e)
}
fn path(&mut self, at: bool, legacy: bool) -> Result<Operand<'a>> {
let end = self.at + path_end(&self.body[self.at..]);
let mut to = end;
let mut fun = None;
if self.body[end..].starts_with(b"()")
&& let Some(dot) = self.body[self.at..end].iter().rposition(|&b| b == b'.')
{
fun = Some(Fun::named(&self.body[self.at + dot + 1..end]));
to = self.at + dot;
}
let mut p = Parse {
rest: &self.body[self.at..to],
at: 0,
legacy,
sels: Vec::new(),
};
p.run()?;
self.at = if fun.is_some() { end + 2 } else { end };
let path = Operand::Path { at, sels: p.sels };
Ok(match fun {
Some(f) => Operand::Call(Box::new(path), f),
None => path,
})
}
fn literal(&mut self) -> Result<Operand<'a>> {
let from = self.at;
let text = match self.body[self.at] {
b'"' | b'\'' => self.string()?,
open @ (b'[' | b'{') => {
let body = &self.body[self.at + 1..];
let close = if open == b'[' { b']' } else { b'}' };
let Some(close) = closer(body, close) else {
return Err(self.bad("a value in a filter that is not closed"));
};
self.at += close + 2;
self.body[from..self.at].to_vec()
}
_ => {
while self.at < self.body.len() && !stops(self.body[self.at]) {
self.at += 1;
}
if self.at == from {
return Err(self.bad("a filter with an operator where a value goes"));
}
self.body[from..self.at].to_vec()
}
};
let bytes = crate::from_json(&text)
.map_err(|_| self.bad("a value in a filter that is not a value"))?;
Ok(Operand::Lit(bytes))
}
fn string(&mut self) -> Result<Vec<u8>> {
let quote = self.body[self.at];
let mut out = vec![b'"'];
let mut i = self.at + 1;
while i < self.body.len() {
let c = self.body[i];
if c == b'\\' && i + 1 < self.body.len() {
let next = self.body[i + 1];
if next == b'\'' {
out.push(b'\'');
} else {
out.push(c);
out.push(next);
}
i += 2;
continue;
}
if c == quote {
out.push(b'"');
self.at = i + 1;
return Ok(out);
}
if c == b'"' {
out.push(b'\\');
}
out.push(c);
i += 1;
}
Err(self.bad("a string in a filter with no closing quote"))
}
fn word(&mut self, text: &[u8]) -> bool {
self.spaces();
if self.body[self.at..].starts_with(text) {
self.at += text.len();
return true;
}
false
}
fn spaces(&mut self) {
while matches!(self.body.get(self.at), Some(b' ' | b'\t')) {
self.at += 1;
}
}
fn bad(&self, what: &str) -> Error {
Error::fmt(
Code::Invalid,
format_args!("{what}, at byte {} of the path", self.of + self.at),
)
}
}
fn projection(body: &[u8]) -> Option<Operand<'_>> {
if body.first().is_none_or(|c| matches!(c, b' ' | b'\t')) {
return None;
}
let mut f = Filter {
body,
at: 0,
of: 0,
top: true,
};
let e = f.operand().ok()?;
f.spaces();
if f.at != body.len() || (body[0] != b'(' && matches!(e, Operand::Path { .. })) {
return None;
}
(!mentions_at(&e)).then_some(e)
}
fn mentions_at(o: &Operand<'_>) -> bool {
match o {
Operand::Path { at, .. } => *at,
Operand::Lit(_) | Operand::Re(_) => false,
Operand::Keys(inner) | Operand::Call(inner, _) | Operand::Sign(inner, _) => {
mentions_at(inner)
}
Operand::Math(l, _, r) => mentions_at(l) || mentions_at(r),
}
}
fn path_end(body: &[u8]) -> usize {
let mut depth = 0usize;
let mut quote = 0u8;
let mut i = 0;
while i < body.len() {
let c = body[i];
if quote != 0 {
if c == b'\\' {
i += 2;
continue;
}
if c == quote {
quote = 0;
}
} else if depth == 0 && ends_path(body, i) {
return i;
} else {
match c {
b'"' | b'\'' => quote = c,
b'[' => depth += 1,
b']' => depth = depth.saturating_sub(1),
_ => {}
}
}
i += 1;
}
body.len()
}
fn stops(c: u8) -> bool {
matches!(
c,
b' ' | b'\t'
| b'('
| b')'
| b'!'
| b'<'
| b'>'
| b'='
| b'&'
| b'|'
| b','
| b'~'
| b'+'
| b'*'
| b'/'
| b'%'
)
}
fn ends_path(body: &[u8], i: usize) -> bool {
if matches!(body[i], b'+' | b'-' | b'/' | b'%') {
return i > 0 && body[i - 1] == b']';
}
ends_name(body[i])
}
fn ends_name(c: u8) -> bool {
stops(c) && !matches!(c, b'+' | b'/' | b'%')
}
const WORD_OPS: &[(&[u8], Op)] = &[
(b"subsetof", Op::SubsetOf),
(b"anyof", Op::AnyOf),
(b"noneof", Op::NoneOf),
(b"nin", Op::Nin),
(b"in", Op::In),
(b"sizeof", Op::Size),
(b"size", Op::Size),
(b"empty", Op::Empty),
];
fn word_at(body: &[u8], text: &[u8]) -> bool {
body.starts_with(text)
&& !body[text.len()..]
.first()
.is_some_and(|c| c.is_ascii_alphanumeric() || *c == b'_')
}
fn closer(body: &[u8], close: u8) -> Option<usize> {
let mut depth = 0usize;
let mut quote = 0u8;
let mut i = 0;
while i < body.len() {
let c = body[i];
if quote != 0 {
if c == b'\\' {
i += 2;
continue;
}
if c == quote {
quote = 0;
}
} else if c == close && depth == 0 {
return Some(i);
} else {
match c {
b'"' | b'\'' => quote = c,
b'[' | b'{' => depth += 1,
b']' | b'}' => depth = depth.saturating_sub(1),
_ => {}
}
}
i += 1;
}
None
}
fn quoted(part: &[u8]) -> Option<&[u8]> {
if part.len() >= 2 {
let (first, last) = (part[0], part[part.len() - 1]);
if (first == b'"' || first == b'\'') && last == first {
return Some(&part[1..part.len() - 1]);
}
}
None
}
fn trim(part: &[u8]) -> &[u8] {
let from = part.iter().position(|&c| c != b' ').unwrap_or(part.len());
let to = part
.iter()
.rposition(|&c| c != b' ')
.map_or(from, |i| i + 1);
&part[from..to]
}
#[cfg(test)]
mod tests {
use super::*;
use crate::from_json;
fn doc() -> Vec<u8> {
from_json(
br#"{"store":{"book":[{"title":"a","price":8},{"title":"b","price":22}],
"bike":{"price":19}},"expensive":10}"#,
)
.expect("the text parses")
}
fn ask(bytes: &[u8], path: &str) -> String {
let v = Value::new(bytes).expect("readable");
let mut hits = Vec::new();
Path::parse(path.as_bytes())
.expect("the path parses")
.select(&v, &mut hits);
let mut out = Vec::new();
out.push(b'[');
for (i, hit) in hits.iter().enumerate() {
if i > 0 {
out.push(b',');
}
hit.write_json(&mut out).expect("writable");
}
out.push(b']');
String::from_utf8(out).expect("UTF-8")
}
fn why(path: &str) -> String {
Path::parse(path.as_bytes())
.expect_err("this should not parse")
.message()
.to_string()
}
#[test]
fn a_bare_dot_is_the_root_and_is_the_only_dot_with_nothing_after_it() {
for spelling in ["$", ".", ""] {
let p = Path::parse(spelling.as_bytes()).expect("the path parses");
assert!(p.is_root(), "{spelling} should be the root");
assert!(p.is_definite(), "{spelling} names one place");
}
assert!(!Path::parse(b"$").expect("parses").legacy());
assert!(Path::parse(b".").expect("parses").legacy());
let d = doc();
assert_eq!(
ask(&d, "."),
format!(
"[{}]",
String::from_utf8_lossy(
&Value::new(&d)
.expect("readable")
.to_json()
.expect("writable")
)
)
);
assert!(why("..").contains("`..` with nothing after it"));
assert!(why(".a.").contains("no name after it"));
assert!(why("$.").contains("no name after it"));
}
#[test]
fn a_path_that_names_one_place_names_the_same_place_it_always_did() {
let d = doc();
assert_eq!(ask(&d, "$.expensive"), "[10]");
assert_eq!(ask(&d, "$.store.bike.price"), "[19]");
assert_eq!(ask(&d, "$.store.book[0].title"), r#"["a"]"#);
assert_eq!(ask(&d, "$.store.book[-1].title"), r#"["b"]"#);
assert_eq!(ask(&d, "$['store']['bike']['price']"), "[19]");
assert_eq!(ask(&d, "store.bike.price"), "[19]", "the older syntax");
assert!(why("$.store.no such key").contains("does not start with"));
assert_eq!(ask(&d, r#"$.store["no such key"]"#), "[]");
assert_eq!(ask(&d, "$"), ask(&d, ""), "the root, both ways of asking");
}
#[test]
fn a_path_that_names_nothing_answers_nothing_rather_than_failing() {
let d = doc();
assert_eq!(ask(&d, "$.nope"), "[]");
assert_eq!(ask(&d, "$.store.book[9]"), "[]");
assert_eq!(ask(&d, "$.store.book[-9]"), "[]");
assert_eq!(ask(&d, "$.expensive[0]"), "[]", "an index into a number");
assert_eq!(ask(&d, "$.store.book.title"), "[]", "a name into an array");
assert_eq!(ask(&d, "$.store[0]"), "[]", "an index into an object");
}
#[test]
fn a_wildcard_names_every_child_and_a_descent_names_every_one_below() {
let d = doc();
assert_eq!(ask(&d, "$.store.book[*].price"), "[8,22]");
assert_eq!(ask(&d, "$.store.book[*].title"), r#"["a","b"]"#);
assert_eq!(
ask(&d, "$..price"),
"[19,8,22]",
"the bike sorts before the books"
);
assert_eq!(
ask(&d, "$.store.*.price"),
"[19]",
"the bike and not the books"
);
assert_eq!(ask(&d, "$..book[0].price"), "[8]");
assert_eq!(ask(&d, "$..[0].title"), r#"["a"]"#);
assert_eq!(ask(&d, "$.expensive.*"), "[]");
}
#[test]
fn a_descent_walks_a_node_before_the_nodes_under_it() {
let d = from_json(br#"{"a":{"b":1,"a":{"a":2}}}"#).expect("parses");
assert_eq!(ask(&d, "$..a"), r#"[{"a":{"a":2},"b":1},{"a":2},2]"#);
}
#[test]
fn a_union_names_what_it_lists_in_the_order_it_lists_it() {
let d = from_json(br#"{"a":1,"b":2,"c":3,"xs":[10,11,12,13]}"#).expect("parses");
assert_eq!(ask(&d, "$.xs[0,2]"), "[10,12]");
assert_eq!(ask(&d, "$.xs[2,0]"), "[12,10]", "and not in index order");
assert_eq!(ask(&d, "$.xs[0, 2]"), "[10,12]", "spaces are allowed");
assert_eq!(ask(&d, "$['b','a']"), "[2,1]");
assert_eq!(ask(&d, "$.xs[0,9]"), "[10]", "one of them names nothing");
}
#[test]
fn a_slice_is_the_slice_every_other_language_has() {
let d = from_json(br#"{"xs":[0,1,2,3,4,5]}"#).expect("parses");
assert_eq!(ask(&d, "$.xs[1:3]"), "[1,2]");
assert_eq!(ask(&d, "$.xs[:2]"), "[0,1]");
assert_eq!(ask(&d, "$.xs[4:]"), "[4,5]");
assert_eq!(ask(&d, "$.xs[:]"), "[0,1,2,3,4,5]");
assert_eq!(ask(&d, "$.xs[-2:]"), "[4,5]");
assert_eq!(ask(&d, "$.xs[:-4]"), "[0,1]");
assert_eq!(ask(&d, "$.xs[0:6:2]"), "[0,2,4]");
assert_eq!(ask(&d, "$.xs[::2]"), "[0,2,4]");
assert_eq!(ask(&d, "$.xs[::-1]"), "[5,4,3,2,1,0]");
assert_eq!(ask(&d, "$.xs[4:1:-1]"), "[4,3,2]");
assert_eq!(
ask(&d, "$.xs[0:1000]"),
"[0,1,2,3,4,5]",
"a bound is clamped"
);
assert_eq!(ask(&d, "$.xs[3:1]"), "[]", "an empty run is empty");
}
#[test]
fn a_path_says_whether_it_could_ever_name_two_places() {
let definite = |p: &str| Path::parse(p.as_bytes()).expect("parses").is_definite();
assert!(definite("$.a.b[0]"));
assert!(definite("$['a'][-1]"));
assert!(definite(""), "the root is one place");
assert!(!definite("$.a[*]"));
assert!(!definite("$..a"));
assert!(!definite("$.a[0,1]"));
assert!(!definite("$.a[0:2]"));
assert!(!definite("$.*"));
}
#[test]
fn a_path_says_which_of_the_two_syntaxes_it_was_written_in() {
let legacy = |p: &str| Path::parse(p.as_bytes()).expect("parses").legacy();
assert!(!legacy("$.a"));
assert!(!legacy("$"));
assert!(legacy(".a"));
assert!(legacy("a.b"));
}
#[test]
fn the_first_match_is_the_first_one_in_document_order() {
let d = doc();
let v = Value::new(&d).expect("readable");
let p = Path::parse(b"$..price").expect("parses");
assert_eq!(p.first(&v).expect("there").as_int(), Some(19));
assert!(Path::parse(b"$.nope").expect("parses").first(&v).is_none());
}
#[test]
fn a_path_that_does_not_parse_says_so_and_says_where() {
assert!(why("$.a[").contains("no `]`"));
assert!(why("$.a[x]").contains("not a number"));
assert!(why("$.a.").contains("no name after it"));
assert!(why("$..").contains("nothing after it"));
assert!(why("$.a[]").contains("not a number"));
assert!(why("$.a[::0]").contains("step of zero"));
assert!(why("$.a[1:2:3:4]").contains("at most a start"));
assert!(why("$a").contains("does not start with"));
assert!(why("$.a[x]").contains("at byte "));
assert!(why("$.a[?(@.b > 1]").contains("no `)`"));
assert!(why("$.a[?(@.b >)]").contains("where a value goes"));
assert!(why("$.a[?(@.b > 1) 2]").contains("left over"));
assert!(why("$.a[?(@.b == nope)]").contains("not a value"));
assert!(why("$.a[?]").contains("where a value was expected"));
assert!(why("$.a[?(@.b in)]").contains("where a value goes"));
assert!(why("$.a[?(@.b size)]").contains("where a value goes"));
assert!(why("$.a[?(@.b + )]").contains("where a value goes"));
assert!(why("$.a[?(@.b~~)]").contains("no `)`"));
}
#[test]
fn the_walk_stops_at_the_depth_limit_rather_than_running_out_of_stack() {
let text = format!("{}1{}", "[".repeat(DEPTH_MAX), "]".repeat(DEPTH_MAX));
let d = from_json(text.as_bytes()).expect("parses");
let v = Value::new(&d).expect("readable");
let mut hits = Vec::new();
Path::parse(b"$..*").expect("parses").select(&v, &mut hits);
assert_eq!(hits.len(), DEPTH_MAX, "every level below the root, once");
}
#[test]
fn selecting_appends_so_that_one_buffer_serves_many_documents() {
let one = from_json(br#"{"a":1}"#).expect("parses");
let two = from_json(br#"{"a":2}"#).expect("parses");
let p = Path::parse(b"$.a").expect("parses");
let mut hits = Vec::new();
p.select(&Value::new(&one).expect("readable"), &mut hits);
p.select(&Value::new(&two).expect("readable"), &mut hits);
let got: Vec<i64> = hits.iter().filter_map(Value::as_int).collect();
assert_eq!(got, [1, 2]);
}
fn types() -> Vec<u8> {
from_json(
br#"[{"p":1,"id":"i"},{"p":2.5,"id":"f"},{"p":"s","id":"t"},
{"p":null,"id":"n"},{"p":false,"id":"b"},{"p":[1],"id":"a"},
{"p":{"x":1},"id":"o"},{"q":9,"id":"m"}]"#,
)
.expect("the text parses")
}
fn kept(filter: &str) -> String {
let d = types();
ask(&d, &format!("$[?{filter}].id"))
.replace(['[', ']', '"'], "")
.replace(',', "")
}
#[test]
fn a_filter_keeps_the_children_its_expression_is_true_of() {
let d = doc();
assert_eq!(ask(&d, "$.store.book[?(@.price < 10)].title"), r#"["a"]"#);
assert_eq!(ask(&d, "$.store.book[?(@.price > 10)].title"), r#"["b"]"#);
assert_eq!(
ask(&d, "$.store.book[?(@.price < $.expensive)].title"),
r#"["a"]"#
);
assert_eq!(ask(&d, "$.store.book[?@.price<10].title"), r#"["a"]"#);
assert_eq!(ask(&d, "$.store.book[? (@.price < 10) ].title"), r#"["a"]"#);
}
#[test]
fn ordering_is_within_a_type_and_not_across_one() {
assert_eq!(kept("(@.p < 2)"), "i");
assert_eq!(kept("(@.p > 2)"), "f");
assert_eq!(kept("(@.p >= 1)"), "if");
assert_eq!(kept("(@.p <= 2.5)"), "if");
assert_eq!(kept(r#"(@.p > "")"#), "t");
assert_eq!(kept(r#"(@.p < "s")"#), "");
assert_eq!(kept(r#"(@.p <= "s")"#), "t");
assert_eq!(kept(r#"(@.p > "1")"#), "t");
assert_eq!(kept(r#"(@.p < "1")"#), "");
assert_eq!(kept("(@.p > false)"), "");
assert_eq!(kept("(@.p >= false)"), "b");
assert_eq!(kept("(@.p < true)"), "b");
assert_eq!(kept("(@.p > null)"), "");
assert_eq!(kept("(@.p >= null)"), "n");
assert_eq!(kept("(@.p >= [1])"), "");
assert_eq!(kept("(@.p <= [1])"), "");
assert_eq!(kept(r#"(@.p >= {"x":1})"#), "");
assert_eq!(kept("(@.p > [])"), "");
assert_eq!(kept("(@.p > {})"), "");
}
#[test]
fn equality_crosses_the_number_split_and_no_other() {
assert_eq!(kept("(@.p == 1)"), "i");
assert_eq!(kept("(@.p == 1.0)"), "i");
assert_eq!(kept("(@.p == 2.5)"), "f");
assert_eq!(kept(r#"(@.p == "s")"#), "t");
assert_eq!(kept("(@.p == null)"), "n");
assert_eq!(kept("(@.p == false)"), "b");
assert_eq!(kept("(@.p == [1])"), "a");
assert_eq!(kept(r#"(@.p == {"x":1})"#), "o");
assert_eq!(kept("(@.p == 0)"), "");
assert_eq!(kept(r#"(@.p == "1")"#), "");
assert_eq!(kept("(@.p == [2])"), "");
assert_eq!(kept(r#"(@.p == {"x":2})"#), "");
}
#[test]
fn not_equal_is_the_negation_of_the_whole_comparison() {
assert_eq!(kept("(@.p != 1)"), "ftnbaom");
assert_eq!(kept("(@.p != 9)"), "iftnbaom");
assert_eq!(kept("(@.zz == @.yy)"), "");
assert_eq!(kept("(@.zz != @.yy)"), "iftnbaom");
}
#[test]
fn a_bare_operand_asks_whether_it_is_there() {
assert_eq!(kept("(@.p)"), "iftnbao");
assert_eq!(kept("(!@.p)"), "m");
assert_eq!(kept("!@.p"), "m");
assert_eq!(kept("(@.q)"), "m");
assert_eq!(kept("(false)"), "");
assert_eq!(kept("(0)"), "iftnbaom");
assert_eq!(kept(r#"("")"#), "iftnbaom");
assert_eq!(kept("(null)"), "iftnbaom");
assert_eq!(kept("(true)"), "iftnbaom");
}
#[test]
fn and_binds_tighter_than_or() {
assert_eq!(kept(r#"(@.id == "i" || @.id == "f" && @.p > 100)"#), "i");
assert_eq!(kept(r#"(@.id == "i" && @.p == 1 || @.id == "t")"#), "it");
assert_eq!(kept(r#"((@.id == "i" || @.id == "f") && @.p > 2)"#), "f");
assert_eq!(kept(r#"(!(@.id == "i") && @.p == 2.5)"#), "f");
}
#[test]
fn a_comparison_holds_when_any_pair_of_answers_does() {
let d = from_json(br#"[{"t":["x","y"]},{"t":["z"]},{"t":[]}]"#).expect("parses");
assert_eq!(ask(&d, r#"$[?(@.t[*] == "y")].t"#), r#"[["x","y"]]"#);
assert_eq!(ask(&d, r#"$[?(@.t[*] == "q")].t"#), "[]");
assert_eq!(ask(&d, r#"$[?(@.t[*] != "z")].t"#), r#"[["x","y"],[]]"#);
assert_eq!(kept("(@..x == 1)"), "o");
assert_eq!(kept("(@.p[*] == 1)"), "ao");
}
#[test]
fn a_pattern_is_unanchored_and_minds_its_case() {
assert_eq!(kept(r#"(@.p =~ "s")"#), "t");
assert_eq!(kept(r#"(@.p =~ "^s$")"#), "t");
assert_eq!(kept(r#"(@.p =~ "S")"#), "");
assert_eq!(kept(r#"(@.p =~ "^x")"#), "");
assert_eq!(kept("(@.p =~ 1)"), "");
assert_eq!(kept("(@.p =~ null)"), "");
assert_eq!(kept(r#"(@.p =~ "[")"#), "");
}
#[test]
fn a_filter_reads_an_object_the_same_way_it_reads_an_array() {
let d = from_json(br#"{"one":{"p":1},"two":{"p":9}}"#).expect("parses");
assert_eq!(ask(&d, "$[?(@.p < 5)]"), r#"[{"p":1}]"#);
assert_eq!(ask(&d, "$.*[?(@.p < 5)]"), "[]");
let flat = from_json(br#"[1,"a",null]"#).expect("parses");
assert_eq!(ask(&flat, "$[*][?(@.p)]"), "[]");
}
#[test]
fn a_filter_is_a_selector_like_the_others() {
let d = from_json(
br#"{"runs":[{"ok":true,"steps":[{"ms":9},{"ms":31}]},
{"ok":false,"steps":[{"ms":2}]}]}"#,
)
.expect("parses");
assert_eq!(
ask(&d, "$.runs[?(@.ok == true)].steps[?(@.ms > 10)].ms"),
"[31]"
);
assert_eq!(ask(&d, "$..steps[?(@.ms < 10)].ms"), "[9,2]");
assert!(
!Path::parse(b"$.runs[?(@.ok)]")
.expect("parses")
.is_definite()
);
}
#[test]
fn the_membership_operators_read_an_array_on_the_right() {
assert_eq!(kept("(@.p in [1,2])"), "i");
assert_eq!(kept("(@.p nin [1,2])"), "ftnbaom");
assert_eq!(kept(r#"(@.p in [[1],{"x":1},null,false,"s"])"#), "tnbao");
assert_eq!(kept("(@.p anyof [1,9])"), "a");
assert_eq!(kept("(@.p noneof [1,9])"), "iftnbom");
assert_eq!(kept("(@.p subsetof [1,2,3])"), "a");
assert_eq!(kept("(@.p subsetof [])"), "");
}
#[test]
fn size_and_empty_are_about_the_three_types_with_a_length() {
assert_eq!(kept("(@.p size 1)"), "tao");
assert_eq!(kept("(@.p size 0)"), "");
assert_eq!(kept("(@.p empty false)"), "tao");
assert_eq!(kept("(@.p empty true)"), "");
let d = from_json(br#"[{"p":"","id":"s"},{"p":[],"id":"a"},{"p":{},"id":"o"}]"#)
.expect("parses");
assert_eq!(ask(&d, "$[?(@.p empty true)].id"), r#"["s","a","o"]"#);
assert_eq!(ask(&d, "$[?(@.p size 0)].id"), r#"["s","a","o"]"#);
}
#[test]
fn a_method_answers_something_the_document_does_not_hold() {
assert_eq!(kept("(@.p.length() == 1)"), "tao");
assert_eq!(kept("(@.p.count() == 1)"), "iftnbao");
assert_eq!(kept("(@.p.count() == 0)"), "m");
assert_eq!(kept("(@.p.min() == 1)"), "a");
assert_eq!(kept("(@.p.max() == 1)"), "a");
assert_eq!(kept("(@.p.sum() == 1)"), "a");
assert_eq!(kept("(@.p.avg() == 1)"), "a");
assert_eq!(kept("(@.p.size() == 1)"), "");
assert_eq!(kept("(@.p.nope() == 1)"), "");
}
#[test]
fn arithmetic_is_numbers_and_the_usual_precedence() {
assert_eq!(kept("(@.p + 1 == 2)"), "i");
assert_eq!(kept("(@.p - 1 == 0)"), "i");
assert_eq!(kept("(@.p * 2 == 5)"), "f");
assert_eq!(kept("(@.p / 2 == 0.5)"), "i");
assert_eq!(kept("(@.p % 2 == 1)"), "i");
assert_eq!(kept("(@.p + @.p == 2)"), "i");
assert_eq!(kept("(@.p.length() + 1 == 2)"), "tao");
assert_eq!(kept("(1 + 2 * 3 == 7)"), "iftnbaom");
assert_eq!(kept("((1 + 2) * 3 == 9)"), "iftnbaom");
assert_eq!(kept("(1 + 2 * 3 == 9)"), "");
assert_eq!(kept("(@.p*2==2)"), "i");
assert_eq!(kept("(@.p+1==2)"), "");
assert_eq!(kept("(@.p/2==0.5)"), "");
assert_eq!(kept("(@.p%2==1)"), "");
let d = from_json(br#"[{"a-b":1,"a+b":2,"a/b":3,"id":"k"}]"#).expect("parses");
assert_eq!(ask(&d, r#"$[?(@.a-b == 1)].id"#), r#"["k"]"#);
assert_eq!(ask(&d, r#"$[?(@.a+b == 2)].id"#), r#"["k"]"#);
assert_eq!(ask(&d, r#"$[?(@.a/b == 3)].id"#), r#"["k"]"#);
let d = from_json(br#"[{"l":[4],"id":"k"}]"#).expect("parses");
assert_eq!(ask(&d, r#"$[?(@.l[0]-1 == 3)].id"#), r#"["k"]"#);
assert_eq!(ask(&d, r#"$[?(@.l[0]+1 == 5)].id"#), r#"["k"]"#);
}
#[test]
fn arithmetic_and_the_methods_want_one_node() {
let d = from_json(
br#"[{"l":[1,2],"s":["ab","cd"],"id":"two"},{"l":[1],"s":["a"],"id":"one"}]"#,
)
.expect("parses");
assert_eq!(ask(&d, "$[?(@.l[*] + 1 == 2)].id"), r#"["one"]"#);
assert_eq!(ask(&d, "$[?(@.s[*].length() == 1)].id"), r#"["one"]"#);
assert_eq!(ask(&d, "$[?(-@.l[*] == -1)].id"), r#"["one"]"#);
assert_eq!(ask(&d, "$[?(@.l[*].count() == 2)].id"), r#"["two"]"#);
}
#[test]
fn the_keys_operator_answers_a_name_at_a_time() {
assert_eq!(kept("(@.p~)"), "o");
assert_eq!(kept(r#"(@.p~ == "x")"#), "o");
assert_eq!(kept(r#"(@.p~ != "x")"#), "iftnbam");
assert_eq!(kept("(@.p~ size 1)"), "o");
assert_eq!(kept(r#"(@.p~ subsetof ["x"])"#), "o");
assert_eq!(kept(r#"(@.p~ anyof ["x"])"#), "o");
assert_eq!(kept(r#"(@.p~ noneof ["x"])"#), "iftnbam");
assert_eq!(kept("(@.p~ empty false)"), "o");
assert_eq!(kept("(@.p~ empty true)"), "");
assert_eq!(kept(r#"(@.p~ in ["x"])"#), "");
assert_eq!(kept(r#"(@.p~ =~ "x")"#), "");
assert_eq!(kept(r#"(@.p~ nin ["x"])"#), "iftnbaom");
let d = from_json(br#"[{"p":{"abc":1},"id":"one"},{"p":{"a":1,"b":2},"id":"two"}]"#)
.expect("parses");
assert_eq!(ask(&d, "$[?(@.p~ size 1)].id"), r#"["one"]"#);
assert_eq!(ask(&d, "$[?(@.p~ size 2)].id"), r#"["two"]"#);
assert_eq!(ask(&d, "$[?(@.p~ size 3)].id"), "[]");
}
#[test]
fn a_key_set_reads_as_a_collection_on_either_side() {
assert_eq!(kept(r#"("x" in @.p~)"#), "o");
assert_eq!(kept(r#"("q" in @.p~)"#), "");
assert_eq!(kept("(@.p~ anyof @.p~)"), "o");
assert_eq!(kept("(@.p~ subsetof @.p~)"), "o");
assert_eq!(kept("(@.p~ noneof @.p~)"), "iftnbam");
assert_eq!(kept(r#"(["x"] subsetof @.p~)"#), "o");
assert_eq!(kept("(1 in @.p)"), "a");
assert_eq!(kept("(2 in @.p)"), "");
}
#[test]
fn an_empty_object_has_a_key_set_and_a_scalar_has_none() {
let d = from_json(br#"[{"p":{},"id":"e"},{"p":1,"id":"s"},{"id":"m"}]"#).expect("parses");
for (path, want) in [
(r#"$[?(@.p~ subsetof ["x"])].id"#, r#"["e"]"#),
(r#"$[?(@.p~ anyof ["x"])].id"#, "[]"),
(r#"$[?(@.p~ noneof ["x"])].id"#, r#"["e","s","m"]"#),
("$[?(@.p~ empty true)].id", r#"["e"]"#),
("$[?(@.p~ empty false)].id", "[]"),
("$[?(@.p~ size 0)].id", r#"["e"]"#),
("$[?(@.p~)].id", "[]"),
] {
assert_eq!(ask(&d, path), want, "{path}");
}
}
#[test]
fn the_alias_and_the_signs_read_the_way_the_reference_reads_them() {
assert_eq!(kept("(@.p sizeof 1)"), "tao");
assert_eq!(kept("(@.p size 1)"), "tao");
assert_eq!(kept("(-@.p == -1)"), "i");
assert_eq!(kept("(+@.p == 1)"), "i");
assert_eq!(kept("(@.p == +1)"), "i");
assert_eq!(kept("(@.p > -1)"), "if");
assert_eq!(kept("(@.p - -1 == 2)"), "i");
assert_eq!(kept("(-@.p)"), "if");
assert_eq!(kept("(-(-@.p) == 1)"), "i");
assert!(why("$[?(--@.p == 1)]").contains("not a value"));
}
fn sum(bytes: &[u8], path: &str) -> String {
let v = Value::new(bytes).expect("readable");
let p = Path::parse(path.as_bytes()).expect("the path parses");
assert!(p.is_projection(), "{path} should be a projection");
let mut out = Vec::new();
out.push(b'[');
for (i, got) in p.project(&v).iter().enumerate() {
if i > 0 {
out.push(b',');
}
got.write_json_at(&crate::Format::default(), &mut out, 0)
.expect("writable");
}
out.push(b']');
String::from_utf8(out).expect("UTF-8")
}
#[test]
fn a_projection_works_something_out_rather_than_naming_a_place() {
let d = doc();
assert_eq!(sum(&d, "$.expensive + 1"), "[11]");
assert_eq!(sum(&d, "$.expensive * 2"), "[20]");
assert_eq!(sum(&d, "-$.expensive"), "[-10]");
assert_eq!(sum(&d, "$.store.book.length()"), "[2]");
assert_eq!(sum(&d, "$.store.book[*].count()"), "[2]");
assert_eq!(sum(&d, "$.store.bike~"), r#"["price"]"#);
assert_eq!(sum(&d, "$.expensive / 1"), "[10.0]");
assert_eq!(sum(&d, "$.store.book[*].price.sum()"), "[]");
assert_eq!(sum(&d, "$.nope + 1"), "[]");
assert_eq!(sum(&d, "$.nope.count()"), "[0]");
assert_eq!(sum(&d, "$.expensive / 0"), "[]");
assert_eq!(sum(&d, ".expensive + 1"), "[11]");
assert_eq!(sum(&d, ".store.book.length()"), "[2]");
assert_eq!(sum(&d, "2 + 3"), "[]");
assert_eq!(sum(&d, "(2) + 3"), "[5]");
for path in ["$.expensive", "$..price", "$.store.book[?(@.price < 10)]"] {
let p = Path::parse(path.as_bytes()).expect("parses");
assert!(!p.is_projection(), "{path} is a path");
}
assert!(why("@.expensive + 1").contains("does not start with"));
}
}