use memchr::{memchr, memchr2};
use serde_json::Value;
pub fn find_key_positions(bytes: &[u8], key: &str) -> Vec<usize> {
let needle = {
let mut s = String::with_capacity(key.len() + 3);
s.push('"');
s.push_str(key);
s.push_str("\":");
s
};
let needle_b = needle.as_bytes();
if needle_b.len() > bytes.len() { return Vec::new(); }
let mut out = Vec::new();
let mut i = 0usize;
let mut in_string = false;
let mut escape = false;
while i < bytes.len() {
if escape {
escape = false;
i += 1;
continue;
}
if in_string {
let rest = &bytes[i..];
match memchr2(b'\\', b'"', rest) {
Some(off) => {
i += off;
match bytes[i] {
b'\\' => { escape = true; i += 1; }
b'"' => { in_string = false; i += 1; }
_ => unreachable!(),
}
}
None => break,
}
} else {
let rest = &bytes[i..];
match memchr(b'"', rest) {
Some(off) => {
let q = i + off;
if q + needle_b.len() <= bytes.len()
&& &bytes[q..q + needle_b.len()] == needle_b {
out.push(q);
i = q + needle_b.len();
} else {
in_string = true;
i = q + 1;
}
}
None => break,
}
}
}
out
}
pub fn extract_values(bytes: &[u8], key: &str) -> Vec<Value> {
let positions = find_key_positions(bytes, key);
let mut out = Vec::with_capacity(positions.len());
let prefix_len = key.len() + 3; for pos in positions {
let mut start = pos + prefix_len;
while start < bytes.len()
&& matches!(bytes[start], b' ' | b'\t' | b'\n' | b'\r') {
start += 1;
}
if start >= bytes.len() { continue; }
let mut stream = serde_json::Deserializer::from_slice(&bytes[start..])
.into_iter::<Value>();
if let Some(Ok(v)) = stream.next() {
out.push(v);
}
}
out
}
#[derive(Debug, Clone, Copy)]
pub struct ValueSpan {
pub start: usize,
pub end: usize,
}
pub fn find_first_key_value_span(bytes: &[u8], key: &str) -> Option<ValueSpan> {
let needle = {
let mut s = String::with_capacity(key.len() + 3);
s.push('"');
s.push_str(key);
s.push_str("\":");
s
};
let needle_b = needle.as_bytes();
if needle_b.len() > bytes.len() { return None; }
let prefix_len = needle_b.len();
let mut i = 0usize;
let mut in_string = false;
let mut escape = false;
while i < bytes.len() {
if escape { escape = false; i += 1; continue; }
if in_string {
let rest = &bytes[i..];
match memchr2(b'\\', b'"', rest) {
Some(off) => {
i += off;
match bytes[i] {
b'\\' => { escape = true; i += 1; }
b'"' => { in_string = false; i += 1; }
_ => unreachable!(),
}
}
None => return None,
}
} else {
let rest = &bytes[i..];
match memchr(b'"', rest) {
Some(off) => {
let q = i + off;
if q + prefix_len <= bytes.len()
&& &bytes[q..q + prefix_len] == needle_b
{
let mut start = q + prefix_len;
while start < bytes.len()
&& matches!(bytes[start], b' ' | b'\t' | b'\n' | b'\r')
{ start += 1; }
if start >= bytes.len() { return None; }
return value_end(bytes, start)
.map(|end| ValueSpan { start, end });
} else {
in_string = true;
i = q + 1;
}
}
None => return None,
}
}
}
None
}
pub fn find_key_value_spans(bytes: &[u8], key: &str) -> Vec<ValueSpan> {
let positions = find_key_positions(bytes, key);
let prefix_len = key.len() + 3;
let mut out = Vec::with_capacity(positions.len());
for pos in positions {
let mut start = pos + prefix_len;
while start < bytes.len()
&& matches!(bytes[start], b' ' | b'\t' | b'\n' | b'\r') {
start += 1;
}
if start >= bytes.len() { continue; }
if let Some(end) = value_end(bytes, start) {
out.push(ValueSpan { start, end });
}
}
out
}
pub fn extract_values_eq(bytes: &[u8], key: &str, lit: &[u8]) -> Vec<Value> {
let mut out = Vec::new();
for span in find_key_value_spans(bytes, key) {
if span.end - span.start == lit.len()
&& &bytes[span.start..span.end] == lit
{
if let Ok(v) = serde_json::from_slice::<Value>(&bytes[span.start..span.end]) {
out.push(v);
}
}
}
out
}
pub fn count_key_value_eq(bytes: &[u8], key: &str, lit: &[u8]) -> usize {
let mut n = 0;
for span in find_key_value_spans(bytes, key) {
if span.end - span.start == lit.len()
&& &bytes[span.start..span.end] == lit
{
n += 1;
}
}
n
}
pub fn find_enclosing_objects_eq(bytes: &[u8], key: &str, lit: &[u8]) -> Vec<ValueSpan> {
let needle = {
let mut s = String::with_capacity(key.len() + 3);
s.push('"');
s.push_str(key);
s.push_str("\":");
s
};
let needle_b = needle.as_bytes();
let mut out = Vec::new();
let mut stack: Vec<(usize, bool)> = Vec::new();
let mut i = 0usize;
let mut in_string = false;
let mut escape = false;
while i < bytes.len() {
if escape { escape = false; i += 1; continue; }
if in_string {
let rest = &bytes[i..];
match memchr2(b'\\', b'"', rest) {
Some(off) => {
i += off;
match bytes[i] {
b'\\' => { escape = true; i += 1; }
b'"' => { in_string = false; i += 1; }
_ => unreachable!(),
}
}
None => break,
}
continue;
}
match bytes[i] {
b'{' => { stack.push((i, false)); i += 1; }
b'}' => {
if let Some((start, matched)) = stack.pop() {
if matched { out.push(ValueSpan { start, end: i + 1 }); }
}
i += 1;
}
b'"' => {
if i + needle_b.len() <= bytes.len()
&& &bytes[i..i + needle_b.len()] == needle_b
{
let mut vs = i + needle_b.len();
while vs < bytes.len()
&& matches!(bytes[vs], b' ' | b'\t' | b'\n' | b'\r')
{ vs += 1; }
if let Some(ve) = value_end(bytes, vs) {
if ve - vs == lit.len() && &bytes[vs..ve] == lit {
if let Some(top) = stack.last_mut() { top.1 = true; }
}
i = ve;
} else {
i = vs;
}
} else {
in_string = true;
i += 1;
}
}
_ => i += 1,
}
}
out.sort_by_key(|s| s.start);
out
}
pub fn find_enclosing_objects_eq_multi(
bytes: &[u8],
conjuncts: &[(String, Vec<u8>)],
) -> Vec<ValueSpan> {
assert!(conjuncts.len() <= 64, "at most 64 conjuncts supported");
if conjuncts.is_empty() { return Vec::new(); }
let needles: Vec<Vec<u8>> = conjuncts.iter().map(|(k, _)| {
let mut s = Vec::with_capacity(k.len() + 3);
s.push(b'"');
s.extend_from_slice(k.as_bytes());
s.extend_from_slice(b"\":");
s
}).collect();
let full_mask: u64 = if conjuncts.len() == 64 { u64::MAX }
else { (1u64 << conjuncts.len()) - 1 };
let mut out = Vec::new();
let mut stack: Vec<(usize, u64)> = Vec::new();
let mut i = 0usize;
let mut in_string = false;
let mut escape = false;
while i < bytes.len() {
if escape { escape = false; i += 1; continue; }
if in_string {
let rest = &bytes[i..];
match memchr2(b'\\', b'"', rest) {
Some(off) => {
i += off;
match bytes[i] {
b'\\' => { escape = true; i += 1; }
b'"' => { in_string = false; i += 1; }
_ => unreachable!(),
}
}
None => break,
}
continue;
}
match bytes[i] {
b'{' => { stack.push((i, 0u64)); i += 1; }
b'}' => {
if let Some((start, mask)) = stack.pop() {
if mask == full_mask {
out.push(ValueSpan { start, end: i + 1 });
}
}
i += 1;
}
b'"' => {
let mut matched_idx: Option<usize> = None;
for (idx, nb) in needles.iter().enumerate() {
if i + nb.len() <= bytes.len() && &bytes[i..i + nb.len()] == &nb[..] {
matched_idx = Some(idx);
break;
}
}
if let Some(idx) = matched_idx {
let nb = &needles[idx];
let mut vs = i + nb.len();
while vs < bytes.len()
&& matches!(bytes[vs], b' ' | b'\t' | b'\n' | b'\r')
{ vs += 1; }
if let Some(ve) = value_end(bytes, vs) {
let lit = &conjuncts[idx].1;
if ve - vs == lit.len() && &bytes[vs..ve] == &lit[..] {
if let Some(top) = stack.last_mut() {
top.1 |= 1u64 << idx;
}
}
i = ve;
} else {
i = vs;
}
} else {
in_string = true;
i += 1;
}
}
_ => i += 1,
}
}
out.sort_by_key(|s| s.start);
out
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ScanCmp { Lt, Lte, Gt, Gte }
impl ScanCmp {
#[inline]
fn holds(self, lhs: f64, rhs: f64) -> bool {
match self {
ScanCmp::Lt => lhs < rhs,
ScanCmp::Lte => lhs <= rhs,
ScanCmp::Gt => lhs > rhs,
ScanCmp::Gte => lhs >= rhs,
}
}
}
#[derive(Debug, Clone)]
pub enum ScanPred {
Eq(Vec<u8>),
Cmp(ScanCmp, f64),
}
pub fn find_enclosing_objects_cmp(
bytes: &[u8],
key: &str,
op: ScanCmp,
threshold: f64,
) -> Vec<ValueSpan> {
let needle = {
let mut s = String::with_capacity(key.len() + 3);
s.push('"');
s.push_str(key);
s.push_str("\":");
s
};
let needle_b = needle.as_bytes();
let mut out = Vec::new();
let mut stack: Vec<(usize, bool)> = Vec::new();
let mut i = 0usize;
let mut in_string = false;
let mut escape = false;
while i < bytes.len() {
if escape { escape = false; i += 1; continue; }
if in_string {
let rest = &bytes[i..];
match memchr2(b'\\', b'"', rest) {
Some(off) => {
i += off;
match bytes[i] {
b'\\' => { escape = true; i += 1; }
b'"' => { in_string = false; i += 1; }
_ => unreachable!(),
}
}
None => break,
}
continue;
}
match bytes[i] {
b'{' => { stack.push((i, false)); i += 1; }
b'}' => {
if let Some((start, matched)) = stack.pop() {
if matched { out.push(ValueSpan { start, end: i + 1 }); }
}
i += 1;
}
b'"' => {
if i + needle_b.len() <= bytes.len()
&& &bytes[i..i + needle_b.len()] == needle_b
{
let mut vs = i + needle_b.len();
while vs < bytes.len()
&& matches!(bytes[vs], b' ' | b'\t' | b'\n' | b'\r')
{ vs += 1; }
if let Some(ve) = value_end(bytes, vs) {
if let Some((_, as_f, _)) = parse_num_span(&bytes[vs..ve]) {
if op.holds(as_f, threshold) {
if let Some(top) = stack.last_mut() { top.1 = true; }
}
}
i = ve;
} else {
i = vs;
}
} else {
in_string = true;
i += 1;
}
}
_ => i += 1,
}
}
out.sort_by_key(|s| s.start);
out
}
pub fn find_direct_field(obj_bytes: &[u8], key: &str) -> Option<ValueSpan> {
if obj_bytes.is_empty() || obj_bytes[0] != b'{' { return None; }
let needle = {
let mut s = String::with_capacity(key.len() + 3);
s.push('"');
s.push_str(key);
s.push_str("\":");
s
};
let needle_b = needle.as_bytes();
let mut depth: usize = 0;
let mut i = 0usize;
let mut in_string = false;
let mut escape = false;
while i < obj_bytes.len() {
if escape { escape = false; i += 1; continue; }
if in_string {
match memchr2(b'\\', b'"', &obj_bytes[i..]) {
Some(off) => {
i += off;
match obj_bytes[i] {
b'\\' => { escape = true; i += 1; }
b'"' => { in_string = false; i += 1; }
_ => unreachable!(),
}
}
None => return None,
}
continue;
}
match obj_bytes[i] {
b'{' | b'[' => { depth += 1; i += 1; }
b'}' | b']' => {
if depth == 0 { return None; }
depth -= 1;
i += 1;
}
b'"' => {
if depth == 1
&& i + needle_b.len() <= obj_bytes.len()
&& &obj_bytes[i..i + needle_b.len()] == needle_b
{
let mut vs = i + needle_b.len();
while vs < obj_bytes.len()
&& matches!(obj_bytes[vs], b' ' | b'\t' | b'\n' | b'\r')
{ vs += 1; }
return value_end(obj_bytes, vs)
.map(|end| ValueSpan { start: vs, end });
}
in_string = true;
i += 1;
}
_ => i += 1,
}
}
None
}
pub fn find_enclosing_objects_mixed(
bytes: &[u8],
conjuncts: &[(String, ScanPred)],
) -> Vec<ValueSpan> {
assert!(conjuncts.len() <= 64, "at most 64 conjuncts supported");
if conjuncts.is_empty() { return Vec::new(); }
let needles: Vec<Vec<u8>> = conjuncts.iter().map(|(k, _)| {
let mut s = Vec::with_capacity(k.len() + 3);
s.push(b'"');
s.extend_from_slice(k.as_bytes());
s.extend_from_slice(b"\":");
s
}).collect();
let full_mask: u64 = if conjuncts.len() == 64 { u64::MAX }
else { (1u64 << conjuncts.len()) - 1 };
let mut out = Vec::new();
let mut stack: Vec<(usize, u64)> = Vec::new();
let mut i = 0usize;
let mut in_string = false;
let mut escape = false;
while i < bytes.len() {
if escape { escape = false; i += 1; continue; }
if in_string {
let rest = &bytes[i..];
match memchr2(b'\\', b'"', rest) {
Some(off) => {
i += off;
match bytes[i] {
b'\\' => { escape = true; i += 1; }
b'"' => { in_string = false; i += 1; }
_ => unreachable!(),
}
}
None => break,
}
continue;
}
match bytes[i] {
b'{' => { stack.push((i, 0u64)); i += 1; }
b'}' => {
if let Some((start, mask)) = stack.pop() {
if mask == full_mask {
out.push(ValueSpan { start, end: i + 1 });
}
}
i += 1;
}
b'"' => {
let mut matched_idx: Option<usize> = None;
for (idx, nb) in needles.iter().enumerate() {
if i + nb.len() <= bytes.len() && &bytes[i..i + nb.len()] == &nb[..] {
matched_idx = Some(idx);
break;
}
}
if let Some(idx) = matched_idx {
let nb = &needles[idx];
let mut vs = i + nb.len();
while vs < bytes.len()
&& matches!(bytes[vs], b' ' | b'\t' | b'\n' | b'\r')
{ vs += 1; }
if let Some(ve) = value_end(bytes, vs) {
let fires = match &conjuncts[idx].1 {
ScanPred::Eq(lit) =>
ve - vs == lit.len() && &bytes[vs..ve] == &lit[..],
ScanPred::Cmp(op, thresh) =>
parse_num_span(&bytes[vs..ve])
.map(|(_, f, _)| op.holds(f, *thresh))
.unwrap_or(false),
};
if fires {
if let Some(top) = stack.last_mut() {
top.1 |= 1u64 << idx;
}
}
i = ve;
} else {
i = vs;
}
} else {
in_string = true;
i += 1;
}
}
_ => i += 1,
}
}
out.sort_by_key(|s| s.start);
out
}
#[derive(Debug, Clone, Copy, Default)]
pub struct NumFold {
pub int_sum: i64,
pub float_sum: f64,
pub is_float: bool,
pub count: usize,
pub min_i: i64,
pub max_i: i64,
pub min_f: f64,
pub max_f: f64,
pub any: bool,
}
#[inline]
pub fn parse_num_span(s: &[u8]) -> Option<(i64, f64, bool)> {
let s = std::str::from_utf8(s).ok()?;
let has_frac_or_exp = s.bytes().any(|b| matches!(b, b'.' | b'e' | b'E'));
if !has_frac_or_exp {
if let Ok(n) = s.parse::<i64>() {
return Some((n, n as f64, true));
}
}
s.parse::<f64>().ok().map(|f| (f as i64, f, false))
}
pub fn fold_nums(bytes: &[u8], spans: &[ValueSpan]) -> NumFold {
let mut f = NumFold::default();
for s in spans {
let slice = &bytes[s.start..s.end];
let Some((i, x, is_int)) = parse_num_span(slice) else { continue };
f.count += 1;
if !f.any {
f.any = true;
f.min_i = i; f.max_i = i;
f.min_f = x; f.max_f = x;
} else {
if i < f.min_i { f.min_i = i; }
if i > f.max_i { f.max_i = i; }
if x < f.min_f { f.min_f = x; }
if x > f.max_f { f.max_f = x; }
}
if is_int && !f.is_float {
f.int_sum = f.int_sum.wrapping_add(i);
f.float_sum += x;
} else {
if !f.is_float { f.float_sum = f.int_sum as f64; f.is_float = true; }
f.float_sum += x;
}
}
f
}
pub fn fold_direct_field_nums(
bytes: &[u8], spans: &[ValueSpan], key: &str,
) -> NumFold {
let mut f = NumFold::default();
for s in spans {
let obj_bytes = &bytes[s.start..s.end];
let Some(vs) = find_direct_field(obj_bytes, key) else { continue };
let Some((i, x, is_int)) = parse_num_span(&obj_bytes[vs.start..vs.end])
else { continue };
f.count += 1;
if !f.any {
f.any = true;
f.min_i = i; f.max_i = i;
f.min_f = x; f.max_f = x;
} else {
if i < f.min_i { f.min_i = i; }
if i > f.max_i { f.max_i = i; }
if x < f.min_f { f.min_f = x; }
if x > f.max_f { f.max_f = x; }
}
if is_int && !f.is_float {
f.int_sum = f.int_sum.wrapping_add(i);
f.float_sum += x;
} else {
if !f.is_float { f.float_sum = f.int_sum as f64; f.is_float = true; }
f.float_sum += x;
}
}
f
}
fn value_end(bytes: &[u8], start: usize) -> Option<usize> {
if start >= bytes.len() { return None; }
match bytes[start] {
b'"' => {
let mut i = start + 1;
let mut escape = false;
while i < bytes.len() {
if escape { escape = false; i += 1; continue; }
match bytes[i] {
b'\\' => { escape = true; i += 1; }
b'"' => return Some(i + 1),
_ => i += 1,
}
}
None
}
b'{' | b'[' => {
let open = bytes[start];
let close = if open == b'{' { b'}' } else { b']' };
let mut depth = 1usize;
let mut i = start + 1;
let mut in_string = false;
let mut escape = false;
while i < bytes.len() {
let b = bytes[i];
if escape { escape = false; i += 1; continue; }
if in_string {
match b {
b'\\' => escape = true,
b'"' => in_string = false,
_ => {}
}
i += 1;
continue;
}
match b {
b'"' => in_string = true,
c if c == open => depth += 1,
c if c == close => {
depth -= 1;
if depth == 0 { return Some(i + 1); }
}
_ => {}
}
i += 1;
}
None
}
_ => {
let mut i = start;
while i < bytes.len() {
match bytes[i] {
b',' | b'}' | b']' | b' ' | b'\t' | b'\n' | b'\r' => break,
_ => i += 1,
}
}
if i == start { None } else { Some(i) }
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn finds_top_level_key() {
let doc = br#"{"test": 42, "other": 7}"#;
let pos = find_key_positions(doc, "test");
assert_eq!(pos, vec![1]);
}
#[test]
fn finds_nested_keys() {
let doc = br#"{"a":{"test":1},"b":[{"test":2},{"test":3}]}"#;
let pos = find_key_positions(doc, "test");
assert_eq!(pos.len(), 3);
}
#[test]
fn ignores_hits_inside_string_values() {
let doc = br#"{"comment":"the \"test\": lie","test":99}"#;
let vals = extract_values(doc, "test");
assert_eq!(vals, vec![serde_json::json!(99)]);
}
#[test]
fn does_not_match_longer_key_suffix() {
let doc = br#"{"nottest":1,"test":2}"#;
let vals = extract_values(doc, "test");
assert_eq!(vals, vec![serde_json::json!(2)]);
}
#[test]
fn handles_escaped_backslash_then_quote() {
let doc = br#"{"path":"c:\\","test":"ok"}"#;
let vals = extract_values(doc, "test");
assert_eq!(vals, vec![serde_json::json!("ok")]);
}
#[test]
fn empty_on_missing_key() {
let doc = br#"{"a":1,"b":2}"#;
assert!(find_key_positions(doc, "zzz").is_empty());
assert!(extract_values(doc, "zzz").is_empty());
}
#[test]
fn extracts_nested_object_value() {
let doc = br#"{"test":{"nested":[1,2,3]}}"#;
let vals = extract_values(doc, "test");
assert_eq!(vals, vec![serde_json::json!({"nested":[1,2,3]})]);
}
#[test]
fn extracts_all_nested_hits_in_order() {
let doc = br#"{"a":{"test":1},"b":[{"test":2},{"test":3}]}"#;
let vals = extract_values(doc, "test");
assert_eq!(vals, vec![
serde_json::json!(1),
serde_json::json!(2),
serde_json::json!(3),
]);
}
#[test]
fn spans_cover_every_value_kind() {
let doc = br#"{"a":1,"b":"two","c":[1,2,3],"d":{"x":1},"e":true}"#;
let keys_and_expected: &[(&str, &[u8])] = &[
("a", b"1"),
("b", b"\"two\""),
("c", b"[1,2,3]"),
("d", b"{\"x\":1}"),
("e", b"true"),
];
for (k, want) in keys_and_expected {
let spans = find_key_value_spans(doc, k);
assert_eq!(spans.len(), 1, "key {} not found", k);
assert_eq!(&doc[spans[0].start..spans[0].end], *want);
}
}
#[test]
fn count_eq_matches_only_literal_equals() {
let doc = br#"{"a":[{"type":"action"},{"type":"idle"},{"type":"action"},{"type":"noop"}]}"#;
assert_eq!(count_key_value_eq(doc, "type", br#""action""#), 2);
assert_eq!(count_key_value_eq(doc, "type", br#""missing""#), 0);
}
#[test]
fn count_eq_numeric_literal() {
let doc = br#"{"xs":[{"n":10},{"n":42},{"n":10},{"n":42}]}"#;
assert_eq!(count_key_value_eq(doc, "n", b"42"), 2);
assert_eq!(count_key_value_eq(doc, "n", b"10"), 2);
}
#[test]
fn spans_skip_whitespace_after_colon() {
let doc = br#"{"a": 42 ,"b": "x"}"#;
let a = find_key_value_spans(doc, "a");
assert_eq!(&doc[a[0].start..a[0].end], b"42");
let b = find_key_value_spans(doc, "b");
assert_eq!(&doc[b[0].start..b[0].end], b"\"x\"");
}
#[test]
fn enclosing_object_simple_match() {
let doc = br#"{"events":[{"type":"action","id":1},{"type":"idle","id":2},{"type":"action","id":3}]}"#;
let spans = find_enclosing_objects_eq(doc, "type", br#""action""#);
assert_eq!(spans.len(), 2);
let objs: Vec<_> = spans.iter()
.map(|s| serde_json::from_slice::<serde_json::Value>(&doc[s.start..s.end]).unwrap())
.collect();
assert_eq!(objs[0], serde_json::json!({"type":"action","id":1}));
assert_eq!(objs[1], serde_json::json!({"type":"action","id":3}));
}
#[test]
fn enclosing_object_nested_both_match() {
let doc = br#"{"type":"x","child":{"type":"x","n":2}}"#;
let spans = find_enclosing_objects_eq(doc, "type", br#""x""#);
assert_eq!(spans.len(), 2);
assert!(spans[0].start < spans[1].start);
assert_eq!(&doc[spans[0].start..spans[0].end], doc);
assert_eq!(&doc[spans[1].start..spans[1].end], br#"{"type":"x","n":2}"#);
}
#[test]
fn enclosing_object_nested_inner_only() {
let doc = br#"{"type":"a","child":{"type":"b","n":2}}"#;
let spans = find_enclosing_objects_eq(doc, "type", br#""b""#);
assert_eq!(spans.len(), 1);
assert_eq!(&doc[spans[0].start..spans[0].end], br#"{"type":"b","n":2}"#);
}
#[test]
fn enclosing_object_ignores_string_value_containing_needle() {
let doc = br#"{"comment":"the \"type\":\"action\" label","events":[{"type":"action"}]}"#;
let spans = find_enclosing_objects_eq(doc, "type", br#""action""#);
assert_eq!(spans.len(), 1);
assert_eq!(&doc[spans[0].start..spans[0].end], br#"{"type":"action"}"#);
}
#[test]
fn enclosing_object_numeric_literal() {
let doc = br#"[{"v":10},{"v":42},{"v":42}]"#;
let spans = find_enclosing_objects_eq(doc, "v", b"42");
assert_eq!(spans.len(), 2);
}
#[test]
fn enclosing_object_no_match() {
let doc = br#"{"xs":[{"v":1},{"v":2}]}"#;
let spans = find_enclosing_objects_eq(doc, "v", b"99");
assert!(spans.is_empty());
}
#[test]
fn enclosing_object_multi_and_both_match() {
let doc = br#"[{"t":"a","v":1},{"t":"a","v":2},{"t":"b","v":1}]"#;
let c = vec![
("t".to_string(), br#""a""#.to_vec()),
("v".to_string(), b"1".to_vec()),
];
let spans = find_enclosing_objects_eq_multi(doc, &c);
assert_eq!(spans.len(), 1);
assert_eq!(&doc[spans[0].start..spans[0].end], br#"{"t":"a","v":1}"#);
}
#[test]
fn enclosing_object_multi_and_nested_propagates() {
let doc = br#"{"t":"a","child":{"t":"a","v":1},"v":1}"#;
let c = vec![
("t".to_string(), br#""a""#.to_vec()),
("v".to_string(), b"1".to_vec()),
];
let spans = find_enclosing_objects_eq_multi(doc, &c);
assert_eq!(spans.len(), 2);
assert!(spans[0].start < spans[1].start);
}
#[test]
fn enclosing_object_multi_and_partial_no_match() {
let doc = br#"[{"t":"a","v":2},{"t":"b","v":1}]"#;
let c = vec![
("t".to_string(), br#""a""#.to_vec()),
("v".to_string(), b"1".to_vec()),
];
let spans = find_enclosing_objects_eq_multi(doc, &c);
assert!(spans.is_empty());
}
}