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}