Skip to main content

sassi/predicate/
jsahibon.rs

1//! JSON predicate builders and evaluator for [`JSahibON`].
2//!
3//! Predicates over `JSahibON` cache fields are constructed through the
4//! [`Field<T, JSahibON>::jsahibon`] / [`Field<T, Option<JSahibON>>::jsahibon`]
5//! extension methods and live under [`LookupOp::Json`]. The body is captured as
6//! [`JSahibONPredicateBody`] so downstream walkers (debug formatters, future
7//! lowering) can inspect the AST through
8//! [`FieldPredicate::value_as`](crate::predicate::FieldPredicate::value_as).
9
10use super::basic::BasicPredicate;
11use super::field_predicate::{FieldPredicate, LookupOp};
12use crate::cacheable::Field;
13use crate::jsahibon::{JObject, JSahibON, compare_jsahibon_numbers};
14use std::any::Any;
15use std::cmp::Ordering;
16use std::marker::PhantomData;
17use std::sync::Arc;
18
19/// JSON path expressed as an ordered sequence of UTF-8 object key segments.
20///
21/// The empty sequence is the root. Segments address literal object keys —
22/// dotted paths are a convenience over plain ASCII identifiers; arbitrary
23/// keys that are not plain identifiers must be added with
24/// [`JPath::from_segments`] or via [`JSahibONPathRef::key`] /
25/// [`JSahibONPathRef::path_segments`]. There is no array indexing in v1.
26#[derive(Clone, Debug, PartialEq, Eq)]
27pub struct JPath(Arc<[String]>);
28
29impl JPath {
30    /// Construct the root path (zero segments).
31    pub fn root() -> Self {
32        Self(Arc::from([]))
33    }
34
35    /// Construct a path from any iterable of segment strings.
36    ///
37    /// Each segment is taken as a literal object key without parsing.
38    pub fn from_segments<I, S>(segments: I) -> Self
39    where
40        I: IntoIterator<Item = S>,
41        S: Into<String>,
42    {
43        Self(
44            segments
45                .into_iter()
46                .map(Into::into)
47                .collect::<Vec<_>>()
48                .into(),
49        )
50    }
51
52    /// Parse a dotted plain-identifier path into segments.
53    ///
54    /// Each segment must be a non-empty ASCII identifier (starting with an
55    /// ASCII letter or `_`, continuing with ASCII alphanumerics or `_`) of at
56    /// most 63 bytes. Keys containing dots, hyphens, empty strings,
57    /// non-ASCII text, or an initial digit must be addressed through
58    /// [`JPath::from_segments`] or [`JSahibONPathRef::key`].
59    ///
60    /// # Panics
61    ///
62    /// Panics when any segment fails the plain-identifier check above. The
63    /// function is intended for `'static` literals authored at compile time;
64    /// invalid input is a programmer error rather than a runtime concern.
65    pub fn parse_dotted(path: &'static str) -> Self {
66        let segments = path.split('.').collect::<Vec<_>>();
67        assert!(
68            segments.iter().all(|segment| valid_plain_segment(segment)),
69            "JSahibON dotted paths require non-empty ASCII identifier segments of at most 63 bytes"
70        );
71        Self::from_segments(segments)
72    }
73
74    /// Return the path's segments in declaration order.
75    pub fn segments(&self) -> &[String] {
76        &self.0
77    }
78
79    fn push(&self, key: String) -> Self {
80        let mut segments = Vec::with_capacity(self.0.len() + 1);
81        segments.extend(self.0.iter().cloned());
82        segments.push(key);
83        Self(segments.into())
84    }
85
86    fn extend<I, S>(&self, segments: I) -> Self
87    where
88        I: IntoIterator<Item = S>,
89        S: Into<String>,
90    {
91        let mut next = Vec::from(self.0.as_ref());
92        next.extend(segments.into_iter().map(Into::into));
93        Self(next.into())
94    }
95}
96
97impl Default for JPath {
98    fn default() -> Self {
99        Self::root()
100    }
101}
102
103impl FromIterator<String> for JPath {
104    fn from_iter<T: IntoIterator<Item = String>>(iter: T) -> Self {
105        Self::from_segments(iter)
106    }
107}
108
109impl<'a> FromIterator<&'a str> for JPath {
110    fn from_iter<T: IntoIterator<Item = &'a str>>(iter: T) -> Self {
111        Self::from_segments(iter)
112    }
113}
114
115fn valid_plain_segment(segment: &str) -> bool {
116    let mut bytes = segment.bytes();
117    let Some(first) = bytes.next() else {
118        return false;
119    };
120    if segment.len() > 63 || !(first.is_ascii_alphabetic() || first == b'_') {
121        return false;
122    }
123    bytes.all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
124}
125
126/// Discriminant for the scalar variant carried in a JSON predicate operand.
127///
128/// V1 has exactly five accepted scalar kinds; new kinds may be added in
129/// future versions, hence `#[non_exhaustive]`.
130#[derive(Clone, Copy, Debug, PartialEq, Eq)]
131#[non_exhaustive]
132pub enum JScalarKind {
133    /// Exact signed-integer scalar.
134    I64,
135    /// Exact unsigned-integer scalar.
136    U64,
137    /// Finite binary64 scalar.
138    F64,
139    /// UTF-8 string scalar (`eq`/`neq`/`in_`/`not_in` only — no ordering).
140    String,
141    /// Boolean scalar (`eq`/`neq`/`in_`/`not_in` only).
142    Bool,
143}
144
145/// Discriminant for JSON value-type assertions used by `is_type` predicates.
146///
147/// Each kind matches one shape of [`JSahibON`]; numeric kinds collapse the
148/// three numeric carriers (`I64`, `U64`, `F64`) into a single `Number` kind.
149#[derive(Clone, Copy, Debug, PartialEq, Eq)]
150#[non_exhaustive]
151pub enum JTypeKind {
152    /// Matches [`JSahibON::Null`].
153    Null,
154    /// Matches [`JSahibON::Bool`].
155    Bool,
156    /// Matches any of [`JSahibON::I64`], [`JSahibON::U64`], or [`JSahibON::F64`].
157    Number,
158    /// Matches [`JSahibON::String`].
159    String,
160    /// Matches [`JSahibON::Array`].
161    Array,
162    /// Matches [`JSahibON::Object`].
163    Object,
164}
165
166/// Type-erased scalar operand carried inside a JSON predicate body.
167///
168/// Equality across numeric variants follows [`JSahibON`]'s cross-numeric
169/// softening (e.g. `I64(1)` equals `U64(1)` equals `F64(1.0)`).
170#[derive(Clone, Debug)]
171#[non_exhaustive]
172pub enum JScalarValue {
173    /// Exact signed integer.
174    I64(i64),
175    /// Exact unsigned integer.
176    U64(u64),
177    /// Finite binary64 value.
178    F64(crate::JFiniteF64),
179    /// UTF-8 string.
180    String(String),
181    /// Boolean.
182    Bool(bool),
183}
184
185impl PartialEq for JScalarValue {
186    fn eq(&self, other: &Self) -> bool {
187        scalar_to_jsahibon(self) == scalar_to_jsahibon(other)
188    }
189}
190
191/// Comparison operator used by scalar JSON predicates.
192#[derive(Clone, Copy, Debug, PartialEq, Eq)]
193#[non_exhaustive]
194pub enum JCompareOp {
195    /// `field == operand`.
196    Eq,
197    /// `field != operand`.
198    Neq,
199    /// `field > operand`.
200    Gt,
201    /// `field >= operand`.
202    Gte,
203    /// `field < operand`.
204    Lt,
205    /// `field <= operand`.
206    Lte,
207}
208
209/// Polarity of an `in_` membership predicate.
210#[derive(Clone, Copy, Debug, PartialEq, Eq)]
211#[non_exhaustive]
212pub enum JInPolarity {
213    /// `field IN (operands…)`.
214    In,
215    /// `field NOT IN (operands…)`.
216    NotIn,
217}
218
219/// Inspectable body of a JSON predicate stored under [`LookupOp::Json`].
220///
221/// Walkers downcast the [`FieldPredicate`] operand value via
222/// [`FieldPredicate::value_as`](crate::predicate::FieldPredicate::value_as)
223/// against this type, then pattern-match on the variant. Marked
224/// `#[non_exhaustive]` so future variants can be added without breaking
225/// downstream matchers.
226#[derive(Clone, Debug, PartialEq)]
227#[non_exhaustive]
228pub enum JSahibONPredicateBody {
229    /// True when the path resolves to a value (any JSON kind, including
230    /// `null`).
231    Exists {
232        /// JSON path the predicate was built against.
233        path: JPath,
234    },
235    /// True when the path does not resolve.
236    Missing {
237        /// JSON path the predicate was built against.
238        path: JPath,
239    },
240    /// True only when the resolved value exists and is JSON `null`.
241    IsJsonNull {
242        /// JSON path the predicate was built against.
243        path: JPath,
244    },
245    /// True only when the resolved value exists and is not JSON `null`.
246    IsNotJsonNull {
247        /// JSON path the predicate was built against.
248        path: JPath,
249    },
250    /// True when the resolved value matches the requested JSON value-type.
251    Type {
252        /// JSON path the predicate was built against.
253        path: JPath,
254        /// JSON kind to match against.
255        kind: JTypeKind,
256    },
257    /// True when the resolved value is an object containing `key`.
258    HasKey {
259        /// JSON path the predicate was built against.
260        path: JPath,
261        /// Object key required to be present.
262        key: Arc<String>,
263    },
264    /// True when the resolved value is an object containing at least one of
265    /// `keys`.
266    HasAnyKey {
267        /// JSON path the predicate was built against.
268        path: JPath,
269        /// Object keys; the predicate is true when any one is present.
270        keys: Arc<[String]>,
271    },
272    /// True when the resolved value is an object containing every key in
273    /// `keys`.
274    HasAllKeys {
275        /// JSON path the predicate was built against.
276        path: JPath,
277        /// Object keys; the predicate is true when all are present.
278        keys: Arc<[String]>,
279    },
280    /// Compare the resolved scalar to a single operand under the requested
281    /// scalar kind.
282    ScalarCompare {
283        /// JSON path the predicate was built against.
284        path: JPath,
285        /// Comparison operator.
286        op: JCompareOp,
287        /// Scalar kind requested by the typed builder; the resolved value
288        /// must match this kind (with cross-numeric softening for numbers).
289        scalar_kind: JScalarKind,
290        /// Operand value to compare against.
291        operand: JScalarValue,
292    },
293    /// Test the resolved scalar against a membership set.
294    ScalarIn {
295        /// JSON path the predicate was built against.
296        path: JPath,
297        /// Scalar kind requested by the typed builder.
298        scalar_kind: JScalarKind,
299        /// Operands forming the membership set.
300        operands: Arc<[JScalarValue]>,
301        /// In or NotIn polarity.
302        polarity: JInPolarity,
303    },
304    /// Test that the resolved scalar lies in the inclusive range
305    /// `[low, high]`.
306    ScalarBetween {
307        /// JSON path the predicate was built against.
308        path: JPath,
309        /// Scalar kind requested by the typed builder.
310        scalar_kind: JScalarKind,
311        /// Inclusive lower bound.
312        low: JScalarValue,
313        /// Inclusive upper bound.
314        high: JScalarValue,
315    },
316    /// True when the resolved JSON value structurally equals `value` under
317    /// [`JSahibON`]'s manual equality.
318    JsonEq {
319        /// JSON path the predicate was built against.
320        path: JPath,
321        /// Expected JSON value.
322        value: JSahibON,
323    },
324    /// True when the resolved JSON value differs from `value` under
325    /// [`JSahibON`] manual equality.
326    JsonNeq {
327        /// JSON path the predicate was built against.
328        path: JPath,
329        /// JSON value the resolved value must differ from.
330        value: JSahibON,
331    },
332    /// True when the resolved JSON value is an array containing `element`
333    /// under [`JSahibON`] manual equality.
334    ArrayContains {
335        /// JSON path the predicate was built against.
336        path: JPath,
337        /// Element value the array must contain.
338        element: JSahibON,
339    },
340    /// Compare the resolved JSON array's length against `len`.
341    ArrayLen {
342        /// JSON path the predicate was built against.
343        path: JPath,
344        /// Comparison operator.
345        op: JCompareOp,
346        /// Length operand. `usize` lengths are widened to `u64` at
347        /// construction time.
348        len: u64,
349    },
350}
351
352/// Sealed marker trait for scalar operand types accepted by
353/// [`JSahibONValueRef`].
354///
355/// V1 has exactly five impls: `i64`, `u64`, `f64`, `String`, and `bool`.
356/// Narrow numeric widths (`i32`, `u16`, etc.) are widened at the call site.
357pub trait JScalar: private::Sealed + Send + Sync + 'static {
358    /// The scalar kind discriminant for this type.
359    const KIND: JScalarKind;
360
361    /// Convert the typed scalar into the type-erased
362    /// [`JScalarValue`] operand.
363    ///
364    /// # Panics
365    ///
366    /// The `f64` impl panics when the input is NaN, `+Infinity`, or
367    /// `-Infinity`; non-finite operands are rejected at predicate
368    /// construction. Other impls (`i64`, `u64`, `String`, `bool`) never
369    /// panic.
370    fn into_scalar_value(self) -> JScalarValue;
371}
372
373/// Sealed marker trait for [`JScalar`] types that also support ordering
374/// predicates (`gt`, `gte`, `lt`, `lte`, `between`).
375///
376/// V1 impls: `i64`, `u64`, `f64`. String ordering is intentionally absent in
377/// v1 — locale collation is out of scope.
378pub trait JOrderedScalar: JScalar {}
379
380mod private {
381    pub trait Sealed {}
382}
383
384impl private::Sealed for i64 {}
385impl JScalar for i64 {
386    const KIND: JScalarKind = JScalarKind::I64;
387    fn into_scalar_value(self) -> JScalarValue {
388        JScalarValue::I64(self)
389    }
390}
391impl JOrderedScalar for i64 {}
392
393impl private::Sealed for u64 {}
394impl JScalar for u64 {
395    const KIND: JScalarKind = JScalarKind::U64;
396    fn into_scalar_value(self) -> JScalarValue {
397        JScalarValue::U64(self)
398    }
399}
400impl JOrderedScalar for u64 {}
401
402impl private::Sealed for f64 {}
403impl JScalar for f64 {
404    const KIND: JScalarKind = JScalarKind::F64;
405
406    /// Convert an `f64` into a [`JScalarValue::F64`].
407    ///
408    /// # Panics
409    ///
410    /// Panics when `self` is `NaN`, `+Infinity`, or `-Infinity`. Per
411    /// [`JSahibON`]'s finite-floats invariant, non-finite
412    /// values cannot be carried in the value model and so cannot meaningfully
413    /// participate in a scalar predicate. Treat the panic as construction-time
414    /// rejection: validate the operand before chaining `.gte(...)` / `.eq(...)`
415    /// when it might be non-finite.
416    fn into_scalar_value(self) -> JScalarValue {
417        JScalarValue::F64(
418            crate::JFiniteF64::try_new(self)
419                .expect("JSahibON scalar predicates only accept finite f64 operands"),
420        )
421    }
422}
423impl JOrderedScalar for f64 {}
424
425impl private::Sealed for String {}
426impl JScalar for String {
427    const KIND: JScalarKind = JScalarKind::String;
428    fn into_scalar_value(self) -> JScalarValue {
429        JScalarValue::String(self)
430    }
431}
432
433impl private::Sealed for bool {}
434impl JScalar for bool {
435    const KIND: JScalarKind = JScalarKind::Bool;
436    fn into_scalar_value(self) -> JScalarValue {
437        JScalarValue::Bool(self)
438    }
439}
440
441enum JRoot<T> {
442    Required(fn(&T) -> &JSahibON),
443    Optional(fn(&T) -> &Option<JSahibON>),
444}
445
446impl<T> Copy for JRoot<T> {}
447
448impl<T> Clone for JRoot<T> {
449    fn clone(&self) -> Self {
450        *self
451    }
452}
453
454impl<T> JRoot<T> {
455    fn resolve(self, value: &T) -> Option<&JSahibON> {
456        match self {
457            Self::Required(extract) => Some(extract(value)),
458            Self::Optional(extract) => extract(value).as_ref(),
459        }
460    }
461}
462
463/// Builder anchored at a specific JSON path within a [`JSahibON`] cache field.
464///
465/// Path refs are produced by `Field<T, JSahibON>::jsahibon().path(...)` /
466/// `key(...)` / `path_segments(...)` (and their `Option<JSahibON>` siblings)
467/// and carry the predicate-construction surface for the resolved value.
468pub struct JSahibONPathRef<T> {
469    field_name: &'static str,
470    root: JRoot<T>,
471    path: JPath,
472}
473
474/// Field-level JSON predicate builder for `Field<T, JSahibON>`.
475///
476/// Constructed via [`Field<T, JSahibON>::jsahibon`]. Re-exposes the same
477/// predicate surface as [`JSahibONPathRef`] anchored at the field root, plus
478/// path-walking entry points (`path`, `key`, `path_segments`).
479pub struct JSahibONFieldRef<T> {
480    inner: JSahibONPathRef<T>,
481}
482
483/// Field-level JSON predicate builder for `Field<T, Option<JSahibON>>`.
484///
485/// Constructed via [`Field<T, Option<JSahibON>>::jsahibon`]. Same surface as
486/// [`JSahibONFieldRef`], but `exists` / `missing` distinguish `None` (missing)
487/// from `Some(JSahibON::Null)` (present, JSON `null`).
488pub struct JSahibONOptionFieldRef<T> {
489    inner: JSahibONPathRef<T>,
490}
491
492/// Typed scalar comparison builder produced by
493/// [`JSahibONPathRef::value`] / [`JSahibONFieldRef::value`] /
494/// [`JSahibONOptionFieldRef::value`].
495///
496/// The type parameter `V` must implement [`JScalar`] (or [`JOrderedScalar`]
497/// for ordering methods).
498pub struct JSahibONValueRef<T, V> {
499    inner: JSahibONPathRef<T>,
500    _marker: PhantomData<V>,
501}
502
503impl<T> Clone for JSahibONPathRef<T> {
504    fn clone(&self) -> Self {
505        Self {
506            field_name: self.field_name,
507            root: self.root,
508            path: self.path.clone(),
509        }
510    }
511}
512
513impl<T> Clone for JSahibONFieldRef<T> {
514    fn clone(&self) -> Self {
515        Self {
516            inner: self.inner.clone(),
517        }
518    }
519}
520
521impl<T> Clone for JSahibONOptionFieldRef<T> {
522    fn clone(&self) -> Self {
523        Self {
524            inner: self.inner.clone(),
525        }
526    }
527}
528
529impl<T, V> Clone for JSahibONValueRef<T, V> {
530    fn clone(&self) -> Self {
531        Self {
532            inner: self.inner.clone(),
533            _marker: PhantomData,
534        }
535    }
536}
537
538impl<T: 'static> Field<T, JSahibON> {
539    /// Build a JSON predicate over a required `JSahibON` field.
540    ///
541    /// At root, `exists()` is always true and `missing()` is always false
542    /// because the field cannot be absent. Use [`JSahibONFieldRef::path`] /
543    /// [`JSahibONFieldRef::key`] / [`JSahibONFieldRef::path_segments`] to
544    /// navigate into the value.
545    pub fn jsahibon(&self) -> JSahibONFieldRef<T> {
546        JSahibONFieldRef {
547            inner: JSahibONPathRef {
548                field_name: self.name,
549                root: JRoot::Required(self.extract),
550                path: JPath::root(),
551            },
552        }
553    }
554}
555
556impl<T: 'static> Field<T, Option<JSahibON>> {
557    /// Build a JSON predicate over an optional `JSahibON` field.
558    ///
559    /// `exists()` is true only for `Some(_)`; `missing()` is true only for
560    /// `None`. `Some(JSahibON::Null)` exists and is JSON `null`.
561    pub fn jsahibon(&self) -> JSahibONOptionFieldRef<T> {
562        JSahibONOptionFieldRef {
563            inner: JSahibONPathRef {
564                field_name: self.name,
565                root: JRoot::Optional(self.extract),
566                path: JPath::root(),
567            },
568        }
569    }
570}
571
572macro_rules! delegate_json_ref_methods {
573    ($ty:ident) => {
574        impl<T: 'static> $ty<T> {
575            /// Return a path ref anchored at the JSON field root.
576            pub fn root(&self) -> JSahibONPathRef<T> {
577                let mut inner = self.inner.clone();
578                inner.path = JPath::root();
579                inner
580            }
581
582            /// Return a path ref for a dotted plain-identifier path.
583            ///
584            /// Panics when any segment is not a plain identifier; use
585            /// [`Self::key`] or [`Self::path_segments`] for arbitrary keys.
586            pub fn path(&self, dotted_plain_idents: &'static str) -> JSahibONPathRef<T> {
587                let mut inner = self.inner.clone();
588                inner.path = JPath::parse_dotted(dotted_plain_idents);
589                inner
590            }
591
592            /// Return a path ref for a literal object key below the root.
593            pub fn key(&self, key: impl Into<String>) -> JSahibONPathRef<T> {
594                let mut inner = self.inner.clone();
595                inner.path = inner.path.push(key.into());
596                inner
597            }
598
599            /// Return a path ref from literal object-key segments.
600            pub fn path_segments<I, S>(&self, segments: I) -> JSahibONPathRef<T>
601            where
602                I: IntoIterator<Item = S>,
603                S: Into<String>,
604            {
605                let mut inner = self.inner.clone();
606                inner.path = JPath::from_segments(segments);
607                inner
608            }
609
610            /// Predicate that is true when the path resolves to any JSON value.
611            pub fn exists(&self) -> BasicPredicate<T> {
612                self.inner.exists()
613            }
614
615            /// Predicate that is true when the path does not resolve.
616            pub fn missing(&self) -> BasicPredicate<T> {
617                self.inner.missing()
618            }
619
620            /// Predicate that is true when the path resolves to JSON `null`.
621            pub fn is_json_null(&self) -> BasicPredicate<T> {
622                self.inner.is_json_null()
623            }
624
625            /// Predicate that is true when the path resolves to a non-null JSON value.
626            pub fn is_not_json_null(&self) -> BasicPredicate<T> {
627                self.inner.is_not_json_null()
628            }
629
630            /// Predicate that is true when the resolved value matches `kind`.
631            pub fn is_type(&self, kind: JTypeKind) -> BasicPredicate<T> {
632                self.inner.is_type(kind)
633            }
634
635            /// Shorthand for `is_type(JTypeKind::Bool)`.
636            pub fn is_bool(&self) -> BasicPredicate<T> {
637                self.inner.is_bool()
638            }
639
640            /// Shorthand for `is_type(JTypeKind::Number)`.
641            pub fn is_number(&self) -> BasicPredicate<T> {
642                self.inner.is_number()
643            }
644
645            /// Shorthand for `is_type(JTypeKind::String)`.
646            pub fn is_string(&self) -> BasicPredicate<T> {
647                self.inner.is_string()
648            }
649
650            /// Shorthand for `is_type(JTypeKind::Array)`.
651            pub fn is_array(&self) -> BasicPredicate<T> {
652                self.inner.is_array()
653            }
654
655            /// Shorthand for `is_type(JTypeKind::Object)`.
656            pub fn is_object(&self) -> BasicPredicate<T> {
657                self.inner.is_object()
658            }
659
660            /// Predicate that is true when the resolved object contains `key`.
661            pub fn has_key(&self, key: impl Into<String>) -> BasicPredicate<T> {
662                self.inner.has_key(key)
663            }
664
665            /// Predicate that is true when the resolved object contains any key.
666            pub fn has_any_key<I, S>(&self, keys: I) -> BasicPredicate<T>
667            where
668                I: IntoIterator<Item = S>,
669                S: Into<String>,
670            {
671                self.inner.has_any_key(keys)
672            }
673
674            /// Predicate that is true when the resolved object contains all keys.
675            pub fn has_all_keys<I, S>(&self, keys: I) -> BasicPredicate<T>
676            where
677                I: IntoIterator<Item = S>,
678                S: Into<String>,
679            {
680                self.inner.has_all_keys(keys)
681            }
682
683            /// Begin a typed scalar comparison against the resolved value.
684            pub fn value<V: JScalar>(&self) -> JSahibONValueRef<T, V> {
685                self.inner.value()
686            }
687
688            /// Predicate that is true when the resolved JSON value equals `value`.
689            pub fn eq_json(&self, value: JSahibON) -> BasicPredicate<T> {
690                self.inner.eq_json(value)
691            }
692
693            /// Predicate that is true when the resolved JSON value differs from `value`.
694            pub fn neq_json(&self, value: JSahibON) -> BasicPredicate<T> {
695                self.inner.neq_json(value)
696            }
697
698            /// Predicate that is true when the resolved array contains `element`.
699            pub fn array_contains(&self, element: JSahibON) -> BasicPredicate<T> {
700                self.inner.array_contains(element)
701            }
702
703            /// Predicate that is true when the resolved array length equals `len`.
704            pub fn array_len_eq(&self, len: usize) -> BasicPredicate<T> {
705                self.inner.array_len_eq(len)
706            }
707
708            /// Predicate that is true when the resolved array length is greater than `len`.
709            pub fn array_len_gt(&self, len: usize) -> BasicPredicate<T> {
710                self.inner.array_len_gt(len)
711            }
712
713            /// Predicate that is true when the resolved array length is at least `len`.
714            pub fn array_len_gte(&self, len: usize) -> BasicPredicate<T> {
715                self.inner.array_len_gte(len)
716            }
717
718            /// Predicate that is true when the resolved array length is less than `len`.
719            pub fn array_len_lt(&self, len: usize) -> BasicPredicate<T> {
720                self.inner.array_len_lt(len)
721            }
722
723            /// Predicate that is true when the resolved array length is at most `len`.
724            pub fn array_len_lte(&self, len: usize) -> BasicPredicate<T> {
725                self.inner.array_len_lte(len)
726            }
727        }
728    };
729}
730
731delegate_json_ref_methods!(JSahibONFieldRef);
732delegate_json_ref_methods!(JSahibONOptionFieldRef);
733
734impl<T: 'static> JSahibONPathRef<T> {
735    /// Push an additional literal object key onto this path.
736    ///
737    /// The key is taken verbatim (never parsed) so dots, hyphens, digits,
738    /// empty strings, and non-ASCII text are addressed correctly.
739    pub fn key(self, key: impl Into<String>) -> Self {
740        Self {
741            path: self.path.push(key.into()),
742            ..self
743        }
744    }
745
746    /// Append additional literal segments onto this path.
747    pub fn path_segments<I, S>(self, segments: I) -> Self
748    where
749        I: IntoIterator<Item = S>,
750        S: Into<String>,
751    {
752        Self {
753            path: self.path.extend(segments),
754            ..self
755        }
756    }
757
758    /// Predicate that is true when the path resolves to a value (any JSON
759    /// kind, including `null`).
760    pub fn exists(&self) -> BasicPredicate<T> {
761        self.predicate(JSahibONPredicateBody::Exists {
762            path: self.path.clone(),
763        })
764    }
765
766    /// Predicate that is true when the path does not resolve.
767    pub fn missing(&self) -> BasicPredicate<T> {
768        self.predicate(JSahibONPredicateBody::Missing {
769            path: self.path.clone(),
770        })
771    }
772
773    /// Predicate that is true only when the resolved value exists and is
774    /// JSON `null`.
775    pub fn is_json_null(&self) -> BasicPredicate<T> {
776        self.predicate(JSahibONPredicateBody::IsJsonNull {
777            path: self.path.clone(),
778        })
779    }
780
781    /// Predicate that is true only when the resolved value exists and is
782    /// not JSON `null`.
783    pub fn is_not_json_null(&self) -> BasicPredicate<T> {
784        self.predicate(JSahibONPredicateBody::IsNotJsonNull {
785            path: self.path.clone(),
786        })
787    }
788
789    /// Predicate that is true only when the resolved value matches `kind`.
790    pub fn is_type(&self, kind: JTypeKind) -> BasicPredicate<T> {
791        self.predicate(JSahibONPredicateBody::Type {
792            path: self.path.clone(),
793            kind,
794        })
795    }
796
797    /// Shorthand for `is_type(JTypeKind::Bool)`.
798    pub fn is_bool(&self) -> BasicPredicate<T> {
799        self.is_type(JTypeKind::Bool)
800    }
801
802    /// Shorthand for `is_type(JTypeKind::Number)`. Matches any of the three
803    /// numeric carriers (`I64`, `U64`, `F64`).
804    pub fn is_number(&self) -> BasicPredicate<T> {
805        self.is_type(JTypeKind::Number)
806    }
807
808    /// Shorthand for `is_type(JTypeKind::String)`.
809    pub fn is_string(&self) -> BasicPredicate<T> {
810        self.is_type(JTypeKind::String)
811    }
812
813    /// Shorthand for `is_type(JTypeKind::Array)`.
814    pub fn is_array(&self) -> BasicPredicate<T> {
815        self.is_type(JTypeKind::Array)
816    }
817
818    /// Shorthand for `is_type(JTypeKind::Object)`.
819    pub fn is_object(&self) -> BasicPredicate<T> {
820        self.is_type(JTypeKind::Object)
821    }
822
823    /// Predicate that is true when the resolved value is an object
824    /// containing `key`.
825    pub fn has_key(&self, key: impl Into<String>) -> BasicPredicate<T> {
826        self.predicate(JSahibONPredicateBody::HasKey {
827            path: self.path.clone(),
828            key: Arc::new(key.into()),
829        })
830    }
831
832    /// Predicate that is true when the resolved value is an object
833    /// containing at least one of `keys`.
834    pub fn has_any_key<I, S>(&self, keys: I) -> BasicPredicate<T>
835    where
836        I: IntoIterator<Item = S>,
837        S: Into<String>,
838    {
839        self.predicate(JSahibONPredicateBody::HasAnyKey {
840            path: self.path.clone(),
841            keys: keys.into_iter().map(Into::into).collect::<Vec<_>>().into(),
842        })
843    }
844
845    /// Predicate that is true when the resolved value is an object
846    /// containing every key in `keys`.
847    pub fn has_all_keys<I, S>(&self, keys: I) -> BasicPredicate<T>
848    where
849        I: IntoIterator<Item = S>,
850        S: Into<String>,
851    {
852        self.predicate(JSahibONPredicateBody::HasAllKeys {
853            path: self.path.clone(),
854            keys: keys.into_iter().map(Into::into).collect::<Vec<_>>().into(),
855        })
856    }
857
858    /// Begin a typed scalar comparison against the resolved value.
859    ///
860    /// Numeric scalar kinds (`i64`, `u64`, `f64`) accept any of the three
861    /// JSON numeric carriers and compare in the portable numeric domain.
862    /// `String` and `bool` are exact-kind matches and have no implicit
863    /// coercions.
864    pub fn value<V: JScalar>(&self) -> JSahibONValueRef<T, V> {
865        JSahibONValueRef {
866            inner: self.clone(),
867            _marker: PhantomData,
868        }
869    }
870
871    /// Predicate that is true when the resolved JSON value structurally
872    /// equals `value` under [`JSahibON`]'s manual equality (objects are
873    /// order-insensitive; numbers softened across `I64`/`U64`/`F64`).
874    pub fn eq_json(&self, value: JSahibON) -> BasicPredicate<T> {
875        self.predicate(JSahibONPredicateBody::JsonEq {
876            path: self.path.clone(),
877            value,
878        })
879    }
880
881    /// Predicate that is true when the resolved JSON value differs from
882    /// `value` under [`JSahibON`] manual equality.
883    pub fn neq_json(&self, value: JSahibON) -> BasicPredicate<T> {
884        self.predicate(JSahibONPredicateBody::JsonNeq {
885            path: self.path.clone(),
886            value,
887        })
888    }
889
890    /// Predicate that is true when the resolved value is an array containing
891    /// `element` under [`JSahibON`] manual equality.
892    pub fn array_contains(&self, element: JSahibON) -> BasicPredicate<T> {
893        self.predicate(JSahibONPredicateBody::ArrayContains {
894            path: self.path.clone(),
895            element,
896        })
897    }
898
899    /// Predicate that is true when the resolved array's length equals `len`.
900    pub fn array_len_eq(&self, len: usize) -> BasicPredicate<T> {
901        self.array_len(JCompareOp::Eq, len)
902    }
903
904    /// Predicate that is true when the resolved array's length is greater
905    /// than `len`.
906    pub fn array_len_gt(&self, len: usize) -> BasicPredicate<T> {
907        self.array_len(JCompareOp::Gt, len)
908    }
909
910    /// Predicate that is true when the resolved array's length is greater
911    /// than or equal to `len`.
912    pub fn array_len_gte(&self, len: usize) -> BasicPredicate<T> {
913        self.array_len(JCompareOp::Gte, len)
914    }
915
916    /// Predicate that is true when the resolved array's length is less
917    /// than `len`.
918    pub fn array_len_lt(&self, len: usize) -> BasicPredicate<T> {
919        self.array_len(JCompareOp::Lt, len)
920    }
921
922    /// Predicate that is true when the resolved array's length is less
923    /// than or equal to `len`.
924    pub fn array_len_lte(&self, len: usize) -> BasicPredicate<T> {
925        self.array_len(JCompareOp::Lte, len)
926    }
927
928    fn array_len(&self, op: JCompareOp, len: usize) -> BasicPredicate<T> {
929        let len = u64::try_from(len).expect("array length predicate exceeds u64");
930        self.predicate(JSahibONPredicateBody::ArrayLen {
931            path: self.path.clone(),
932            op,
933            len,
934        })
935    }
936
937    fn predicate(&self, body: JSahibONPredicateBody) -> BasicPredicate<T> {
938        let root = self.root;
939        let body: Arc<JSahibONPredicateBody> = Arc::new(body);
940        let body_for_eval = body.clone();
941        let value: Arc<dyn Any + Send + Sync> = body;
942        BasicPredicate::Field(FieldPredicate::new(
943            self.field_name,
944            LookupOp::Json,
945            value,
946            move |entry| evaluate_jsahibon_predicate(root.resolve(entry), body_for_eval.as_ref()),
947        ))
948    }
949}
950
951impl<T: 'static, V: JScalar> JSahibONValueRef<T, V> {
952    /// `value == operand`. Type-mismatched resolved values evaluate to
953    /// `false`.
954    ///
955    /// # Panics
956    ///
957    /// When `V = f64`, panics if `value` is `NaN`, `+Infinity`, or
958    /// `-Infinity` per [`JScalar::into_scalar_value`].
959    pub fn eq(&self, value: V) -> BasicPredicate<T> {
960        self.compare(JCompareOp::Eq, value)
961    }
962
963    /// `value != operand`. Type-mismatched resolved values evaluate to
964    /// `false`.
965    ///
966    /// # Panics
967    ///
968    /// When `V = f64`, panics if `value` is non-finite.
969    pub fn neq(&self, value: V) -> BasicPredicate<T> {
970        self.compare(JCompareOp::Neq, value)
971    }
972
973    /// `value IN (values…)`. Resolution failure or scalar-kind mismatch
974    /// evaluates to `false`. An empty `values` slice with a present
975    /// matching scalar evaluates to `false`.
976    ///
977    /// # Panics
978    ///
979    /// When `V = f64`, panics if any element of `values` is non-finite.
980    pub fn in_(&self, values: Vec<V>) -> BasicPredicate<T> {
981        self.in_predicate(values, JInPolarity::In)
982    }
983
984    /// `value NOT IN (values…)`. Resolution failure or scalar-kind mismatch
985    /// evaluates to `false`. An empty `values` slice with a present
986    /// matching scalar evaluates to `true`.
987    ///
988    /// # Panics
989    ///
990    /// When `V = f64`, panics if any element of `values` is non-finite.
991    pub fn not_in(&self, values: Vec<V>) -> BasicPredicate<T> {
992        self.in_predicate(values, JInPolarity::NotIn)
993    }
994
995    fn compare(&self, op: JCompareOp, value: V) -> BasicPredicate<T> {
996        self.inner.predicate(JSahibONPredicateBody::ScalarCompare {
997            path: self.inner.path.clone(),
998            op,
999            scalar_kind: V::KIND,
1000            operand: value.into_scalar_value(),
1001        })
1002    }
1003
1004    fn in_predicate(&self, values: Vec<V>, polarity: JInPolarity) -> BasicPredicate<T> {
1005        self.inner.predicate(JSahibONPredicateBody::ScalarIn {
1006            path: self.inner.path.clone(),
1007            scalar_kind: V::KIND,
1008            operands: values
1009                .into_iter()
1010                .map(JScalar::into_scalar_value)
1011                .collect::<Vec<_>>()
1012                .into(),
1013            polarity,
1014        })
1015    }
1016}
1017
1018impl<T: 'static, V: JOrderedScalar> JSahibONValueRef<T, V> {
1019    /// `value > operand`.
1020    ///
1021    /// # Panics
1022    ///
1023    /// When `V = f64`, panics if `value` is non-finite.
1024    pub fn gt(&self, value: V) -> BasicPredicate<T> {
1025        self.compare(JCompareOp::Gt, value)
1026    }
1027
1028    /// `value >= operand`.
1029    ///
1030    /// # Panics
1031    ///
1032    /// When `V = f64`, panics if `value` is non-finite.
1033    pub fn gte(&self, value: V) -> BasicPredicate<T> {
1034        self.compare(JCompareOp::Gte, value)
1035    }
1036
1037    /// `value < operand`.
1038    ///
1039    /// # Panics
1040    ///
1041    /// When `V = f64`, panics if `value` is non-finite.
1042    pub fn lt(&self, value: V) -> BasicPredicate<T> {
1043        self.compare(JCompareOp::Lt, value)
1044    }
1045
1046    /// `value <= operand`.
1047    ///
1048    /// # Panics
1049    ///
1050    /// When `V = f64`, panics if `value` is non-finite.
1051    pub fn lte(&self, value: V) -> BasicPredicate<T> {
1052        self.compare(JCompareOp::Lte, value)
1053    }
1054
1055    /// `low <= value <= high` (inclusive on both ends).
1056    ///
1057    /// # Panics
1058    ///
1059    /// When `V = f64`, panics if either `low` or `high` is non-finite.
1060    pub fn between(&self, low: V, high: V) -> BasicPredicate<T> {
1061        self.inner.predicate(JSahibONPredicateBody::ScalarBetween {
1062            path: self.inner.path.clone(),
1063            scalar_kind: V::KIND,
1064            low: low.into_scalar_value(),
1065            high: high.into_scalar_value(),
1066        })
1067    }
1068}
1069
1070/// Evaluate a [`JSahibONPredicateBody`] against an in-memory JSON value.
1071///
1072/// `root` is the entry's root JSON value (`None` represents an absent
1073/// `Option<JSahibON>` field; `Some(&value)` represents a present value).
1074/// Returns the boolean result per the truth rules documented on
1075/// [`JSahibONPredicateBody`].
1076///
1077/// Downstream crates may reuse this evaluator instead of reimplementing
1078/// the truth rules — Sassi's `Punnu` evaluator already calls into it via
1079/// the predicate closure captured at construction.
1080pub fn evaluate_jsahibon_predicate(root: Option<&JSahibON>, body: &JSahibONPredicateBody) -> bool {
1081    match body {
1082        JSahibONPredicateBody::Exists { path } => resolve_path(root, path).is_some(),
1083        JSahibONPredicateBody::Missing { path } => resolve_path(root, path).is_none(),
1084        JSahibONPredicateBody::IsJsonNull { path } => {
1085            matches!(resolve_path(root, path), Some(JSahibON::Null))
1086        }
1087        JSahibONPredicateBody::IsNotJsonNull { path } => {
1088            resolve_path(root, path).is_some_and(|value| !matches!(value, JSahibON::Null))
1089        }
1090        JSahibONPredicateBody::Type { path, kind } => {
1091            resolve_path(root, path).is_some_and(|value| matches_type(value, *kind))
1092        }
1093        JSahibONPredicateBody::HasKey { path, key } => {
1094            object_at(root, path).is_some_and(|object| object.get(key.as_str()).is_some())
1095        }
1096        JSahibONPredicateBody::HasAnyKey { path, keys } => object_at(root, path)
1097            .is_some_and(|object| keys.iter().any(|key| object.get(key.as_str()).is_some())),
1098        JSahibONPredicateBody::HasAllKeys { path, keys } => object_at(root, path)
1099            .is_some_and(|object| keys.iter().all(|key| object.get(key.as_str()).is_some())),
1100        JSahibONPredicateBody::ScalarCompare {
1101            path,
1102            op,
1103            scalar_kind,
1104            operand,
1105        } => scalar_at(root, path, *scalar_kind)
1106            .is_some_and(|left| compare_scalar(&left, *op, operand)),
1107        JSahibONPredicateBody::ScalarIn {
1108            path,
1109            scalar_kind,
1110            operands,
1111            polarity,
1112        } => scalar_at(root, path, *scalar_kind).is_some_and(|left| {
1113            let contains = operands.iter().any(|right| &left == right);
1114            match polarity {
1115                JInPolarity::In => contains,
1116                JInPolarity::NotIn => !contains,
1117            }
1118        }),
1119        JSahibONPredicateBody::ScalarBetween {
1120            path,
1121            scalar_kind,
1122            low,
1123            high,
1124        } => scalar_at(root, path, *scalar_kind).is_some_and(|left| {
1125            compare_scalar_order(&left, low).is_some_and(|ordering| ordering != Ordering::Less)
1126                && compare_scalar_order(&left, high)
1127                    .is_some_and(|ordering| ordering != Ordering::Greater)
1128        }),
1129        JSahibONPredicateBody::JsonEq { path, value } => {
1130            resolve_path(root, path).is_some_and(|left| left == value)
1131        }
1132        JSahibONPredicateBody::JsonNeq { path, value } => {
1133            resolve_path(root, path).is_some_and(|left| left != value)
1134        }
1135        JSahibONPredicateBody::ArrayContains { path, element } => match resolve_path(root, path) {
1136            Some(JSahibON::Array(values)) => values.iter().any(|value| value == element),
1137            _ => false,
1138        },
1139        JSahibONPredicateBody::ArrayLen { path, op, len } => match resolve_path(root, path) {
1140            Some(JSahibON::Array(values)) => {
1141                compare_u64(u64::try_from(values.len()).unwrap_or(u64::MAX), *op, *len)
1142            }
1143            _ => false,
1144        },
1145    }
1146}
1147
1148fn matches_type(value: &JSahibON, kind: JTypeKind) -> bool {
1149    matches!(
1150        (value, kind),
1151        (JSahibON::Null, JTypeKind::Null)
1152            | (JSahibON::Bool(_), JTypeKind::Bool)
1153            | (
1154                JSahibON::I64(_) | JSahibON::U64(_) | JSahibON::F64(_),
1155                JTypeKind::Number
1156            )
1157            | (JSahibON::String(_), JTypeKind::String)
1158            | (JSahibON::Array(_), JTypeKind::Array)
1159            | (JSahibON::Object(_), JTypeKind::Object)
1160    )
1161}
1162
1163fn resolve_path<'a>(root: Option<&'a JSahibON>, path: &JPath) -> Option<&'a JSahibON> {
1164    let mut current = root?;
1165    for segment in path.segments() {
1166        let JSahibON::Object(object) = current else {
1167            return None;
1168        };
1169        current = object.get(segment)?;
1170    }
1171    Some(current)
1172}
1173
1174fn object_at<'a>(root: Option<&'a JSahibON>, path: &JPath) -> Option<&'a JObject> {
1175    match resolve_path(root, path) {
1176        Some(JSahibON::Object(object)) => Some(object),
1177        _ => None,
1178    }
1179}
1180
1181fn scalar_at(root: Option<&JSahibON>, path: &JPath, kind: JScalarKind) -> Option<JScalarValue> {
1182    let value = resolve_path(root, path)?;
1183    match (kind, value) {
1184        (JScalarKind::I64 | JScalarKind::U64 | JScalarKind::F64, JSahibON::I64(value)) => {
1185            Some(JScalarValue::I64(*value))
1186        }
1187        (JScalarKind::I64 | JScalarKind::U64 | JScalarKind::F64, JSahibON::U64(value)) => {
1188            Some(JScalarValue::U64(*value))
1189        }
1190        (JScalarKind::I64 | JScalarKind::U64 | JScalarKind::F64, JSahibON::F64(value)) => {
1191            Some(JScalarValue::F64(*value))
1192        }
1193        (JScalarKind::String, JSahibON::String(value)) => Some(JScalarValue::String(value.clone())),
1194        (JScalarKind::Bool, JSahibON::Bool(value)) => Some(JScalarValue::Bool(*value)),
1195        _ => None,
1196    }
1197}
1198
1199fn compare_scalar(left: &JScalarValue, op: JCompareOp, right: &JScalarValue) -> bool {
1200    match op {
1201        JCompareOp::Eq => left == right,
1202        JCompareOp::Neq => left != right,
1203        JCompareOp::Gt => {
1204            compare_scalar_order(left, right).is_some_and(|ordering| ordering == Ordering::Greater)
1205        }
1206        JCompareOp::Gte => {
1207            compare_scalar_order(left, right).is_some_and(|ordering| ordering != Ordering::Less)
1208        }
1209        JCompareOp::Lt => {
1210            compare_scalar_order(left, right).is_some_and(|ordering| ordering == Ordering::Less)
1211        }
1212        JCompareOp::Lte => {
1213            compare_scalar_order(left, right).is_some_and(|ordering| ordering != Ordering::Greater)
1214        }
1215    }
1216}
1217
1218fn compare_scalar_order(left: &JScalarValue, right: &JScalarValue) -> Option<Ordering> {
1219    compare_jsahibon_numbers(&scalar_to_jsahibon(left), &scalar_to_jsahibon(right))
1220}
1221
1222fn compare_u64(left: u64, op: JCompareOp, right: u64) -> bool {
1223    match op {
1224        JCompareOp::Eq => left == right,
1225        JCompareOp::Neq => left != right,
1226        JCompareOp::Gt => left > right,
1227        JCompareOp::Gte => left >= right,
1228        JCompareOp::Lt => left < right,
1229        JCompareOp::Lte => left <= right,
1230    }
1231}
1232
1233fn scalar_to_jsahibon(value: &JScalarValue) -> JSahibON {
1234    match value {
1235        JScalarValue::I64(value) => JSahibON::I64(*value),
1236        JScalarValue::U64(value) => JSahibON::U64(*value),
1237        JScalarValue::F64(value) => JSahibON::F64(*value),
1238        JScalarValue::String(value) => JSahibON::String(value.clone()),
1239        JScalarValue::Bool(value) => JSahibON::Bool(*value),
1240    }
1241}