Skip to main content

datalogic_rs/
parsed_data.rs

1//! [`ParsedData`] — a self-contained parsed JSON document.
2//!
3//! Parsing the data JSON dominates the string-in/string-out contract
4//! (70-90% of a parse-eval-serialize round trip in the boundary
5//! measurements), and every string-shaped entry point re-parses the
6//! same payload on every call. `ParsedData` factors that cost out:
7//! parse once, then evaluate any number of rules against the resident
8//! tree through the zero-cost `&DataValue` passthrough of
9//! [`crate::EvalInput`].
10//!
11//! Internally this is the same self-referential shape as the compiler's
12//! pre-built literals (`node/prelit.rs`): a [`self_cell`] owning a
13//! [`Bump`] arena and the [`DataValue`] tree parsed into it. The input
14//! text is copied into the arena before parsing, so the tree borrows
15//! only from memory the cell owns and the handle is fully
16//! self-contained.
17//!
18//! `ParsedData` is `Send` (move it across threads freely) but not
19//! `Sync` — [`Bump`] is `!Sync`. A binding that wants to share one
20//! handle across threads must layer its own guarantee on top (the tree
21//! is never mutated after construction, so read-only sharing is sound
22//! when the wrapper enforces it).
23
24use bumpalo::Bump;
25use self_cell::self_cell;
26
27use crate::Result;
28use crate::arena::DataValue;
29
30self_cell!(
31    /// Owns the bump arena and the `DataValue` tree parsed into it.
32    struct ParsedCell {
33        owner: Bump,
34        #[covariant]
35        dependent: DataValue,
36    }
37);
38
39/// A parsed JSON document that owns its backing storage.
40///
41/// Accepted by every arena-lifetime evaluation entry point
42/// ([`crate::Engine::evaluate`], [`crate::Session::eval`] /
43/// [`crate::Session::eval_borrowed`], …) via [`crate::EvalInput`] at
44/// zero per-call conversion cost — the tree is already arena-resident.
45///
46/// # Example
47///
48/// ```rust
49/// use datalogic_rs::{Engine, ParsedData};
50/// use datalogic_rs::bumpalo::Bump;
51///
52/// let engine = Engine::new();
53/// let data = ParsedData::from_json(r#"{"user": {"age": 34}}"#).unwrap();
54///
55/// // Evaluate many rules against the one parsed payload.
56/// let adult = engine.compile(r#"{">=": [{"var": "user.age"}, 18]}"#).unwrap();
57/// let senior = engine.compile(r#"{">=": [{"var": "user.age"}, 65]}"#).unwrap();
58///
59/// let arena = Bump::new();
60/// assert_eq!(engine.evaluate(&adult, &data, &arena).unwrap().as_bool(), Some(true));
61/// assert_eq!(engine.evaluate(&senior, &data, &arena).unwrap().as_bool(), Some(false));
62/// ```
63pub struct ParsedData(ParsedCell);
64
65impl ParsedData {
66    /// Parse `json` into a self-contained document.
67    ///
68    /// Returns the engine's usual `ParseError` on malformed input.
69    pub fn from_json(json: &str) -> Result<Self> {
70        // Copy the input into the arena first: the parser's zero-copy
71        // strings then borrow from arena-owned bytes, which is what
72        // keeps the cell self-contained. Capacity heuristic: input copy
73        // plus tree nodes typically land within ~2x the text size.
74        let cell =
75            ParsedCell::try_new(Bump::with_capacity(json.len().saturating_mul(2)), |arena| {
76                let stable: &str = arena.alloc_str(json);
77                DataValue::from_str(stable, arena).map_err(crate::Error::from)
78            })?;
79        Ok(Self(cell))
80    }
81
82    /// Borrow the parsed tree.
83    ///
84    /// The returned reference is valid for as long as the handle lives;
85    /// it satisfies the `&DataValue` input shape of the evaluation
86    /// entry points directly.
87    pub fn value(&self) -> &DataValue<'_> {
88        self.0.borrow_dependent()
89    }
90
91    /// Bytes currently held by the backing arena (input copy + tree).
92    pub fn allocated_bytes(&self) -> usize {
93        self.0.borrow_owner().allocated_bytes()
94    }
95}
96
97impl std::fmt::Debug for ParsedData {
98    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
99        f.debug_tuple("ParsedData").field(self.value()).finish()
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106    use crate::Engine;
107
108    #[test]
109    fn parses_and_exposes_value() {
110        let data = ParsedData::from_json(r#"{"a": [1, 2, 3], "b": "text"}"#).unwrap();
111        let v = data.value();
112        assert!(v.is_object());
113        assert_eq!(v.to_string(), r#"{"a":[1,2,3],"b":"text"}"#);
114        assert!(data.allocated_bytes() > 0);
115    }
116
117    #[test]
118    fn malformed_json_is_a_parse_error() {
119        let err = ParsedData::from_json("{ not json").unwrap_err();
120        assert_eq!(err.tag(), "ParseError");
121    }
122
123    #[test]
124    fn evaluates_through_engine_and_session() {
125        let engine = Engine::new();
126        let rule = engine.compile(r#"{"+": [{"var": "x"}, 1]}"#).unwrap();
127        let data = ParsedData::from_json(r#"{"x": 41}"#).unwrap();
128
129        // Engine::evaluate (caller arena) — repeated calls, one parse.
130        let arena = bumpalo::Bump::new();
131        for _ in 0..3 {
132            let out = engine.evaluate(&rule, &data, &arena).unwrap();
133            assert_eq!(out.as_i64(), Some(42));
134        }
135
136        // Session::eval_borrowed — same handle, session-owned arena.
137        let mut session = engine.session();
138        let out = session.eval_borrowed(&rule, &data).unwrap();
139        assert_eq!(out.as_i64(), Some(42));
140    }
141
142    #[test]
143    fn outlives_short_lived_eval_arenas() {
144        let engine = Engine::new();
145        let rule = engine.compile(r#"{"var": "name"}"#).unwrap();
146        let data = ParsedData::from_json(r#"{"name": "Ada"}"#).unwrap();
147        for _ in 0..2 {
148            let arena = bumpalo::Bump::new();
149            let out = engine.evaluate(&rule, &data, &arena).unwrap();
150            assert_eq!(out.to_string(), r#""Ada""#);
151            // arena drops here; `data` stays valid.
152        }
153        assert_eq!(data.value().to_string(), r#"{"name":"Ada"}"#);
154    }
155
156    #[test]
157    fn handle_is_send() {
158        fn assert_send<T: Send>() {}
159        assert_send::<ParsedData>();
160    }
161}