Skip to main content

quillmark_core/
value.rs

1//! Value type for unified representation of TOML/YAML/JSON values.
2//!
3//! [`QuillValue`] is an **annotated value tree**: every node carries a
4//! `fill` flag (the in-memory form of the `!must_fill` YAML tag) alongside
5//! its data. The tree is the authoritative representation. For the data
6//! API (`as_json`, `as_array`, `as_object`, `Deref`) a plain
7//! [`serde_json::Value`] projection is materialized lazily and cached; that
8//! projection is **fill-free** — it is a derived view of the data, not a
9//! second source of truth. Fill never reaches the JSON projection, so
10//! rendering and wire layers that consume `as_json()` are unaffected by it.
11
12use indexmap::IndexMap;
13use serde::{Deserialize, Deserializer, Serialize, Serializer};
14use serde_json::Value as JsonValue;
15use std::ops::Deref;
16use std::sync::OnceLock;
17
18/// Unified value type: an annotated tree of JSON-shaped data where every
19/// node additionally records whether it was tagged `!must_fill`.
20///
21/// Construction (`from_json`, `from_yaml_str`, the scalar constructors)
22/// produces nodes with `fill = false`; the `!must_fill` markers are applied
23/// by the document layer. `QuillValue` exposes no data-mutating methods —
24/// only `set_fill`/`with_fill`, which do not affect the JSON projection —
25/// so the cached projection never goes stale.
26pub struct QuillValue {
27    node: Node,
28    /// Lazily materialized, fill-free [`serde_json::Value`] view of `node`.
29    json: OnceLock<JsonValue>,
30}
31
32/// One node of the annotated tree: a `fill` flag plus the data.
33#[derive(Debug, Clone, PartialEq)]
34struct Node {
35    fill: bool,
36    kind: Kind,
37}
38
39#[derive(Debug, Clone, PartialEq)]
40enum Kind {
41    Null,
42    Bool(bool),
43    Number(serde_json::Number),
44    String(String),
45    Array(Vec<Node>),
46    Object(IndexMap<String, Node>),
47}
48
49/// One step of a path into a value tree: an object key or an array index.
50///
51/// This is the canonical path-segment type for the whole crate; the document
52/// layer aliases it as `CommentPathSegment` for nested-comment paths.
53#[derive(Debug, Clone, PartialEq, Eq, Hash)]
54pub enum PathSegment {
55    Key(String),
56    Index(usize),
57}
58
59fn collect_fill_paths(node: &Node, prefix: &mut Vec<PathSegment>, out: &mut Vec<Vec<PathSegment>>) {
60    if node.fill {
61        out.push(prefix.clone());
62    }
63    match &node.kind {
64        Kind::Array(items) => {
65            for (i, child) in items.iter().enumerate() {
66                prefix.push(PathSegment::Index(i));
67                collect_fill_paths(child, prefix, out);
68                prefix.pop();
69            }
70        }
71        Kind::Object(entries) => {
72            for (k, child) in entries {
73                prefix.push(PathSegment::Key(k.clone()));
74                collect_fill_paths(child, prefix, out);
75                prefix.pop();
76            }
77        }
78        _ => {}
79    }
80}
81
82fn node_at_mut<'a>(node: &'a mut Node, path: &[PathSegment]) -> Option<&'a mut Node> {
83    let mut cur = node;
84    for seg in path {
85        cur = match (&mut cur.kind, seg) {
86            (Kind::Object(entries), PathSegment::Key(k)) => entries.get_mut(k)?,
87            (Kind::Array(items), PathSegment::Index(i)) => items.get_mut(*i)?,
88            _ => return None,
89        };
90    }
91    Some(cur)
92}
93
94fn node_is_object(node: &Node, path: &[PathSegment]) -> bool {
95    fn at<'a>(node: &'a Node, path: &[PathSegment]) -> Option<&'a Node> {
96        let mut cur = node;
97        for seg in path {
98            cur = match (&cur.kind, seg) {
99                (Kind::Object(entries), PathSegment::Key(k)) => entries.get(k)?,
100                (Kind::Array(items), PathSegment::Index(i)) => items.get(*i)?,
101                _ => return None,
102            };
103        }
104        Some(cur)
105    }
106    matches!(at(node, path).map(|n| &n.kind), Some(Kind::Object(_)))
107}
108
109impl Node {
110    fn from_json(value: &JsonValue) -> Node {
111        let kind = match value {
112            JsonValue::Null => Kind::Null,
113            JsonValue::Bool(b) => Kind::Bool(*b),
114            JsonValue::Number(n) => Kind::Number(n.clone()),
115            JsonValue::String(s) => Kind::String(s.clone()),
116            JsonValue::Array(items) => Kind::Array(items.iter().map(Node::from_json).collect()),
117            JsonValue::Object(map) => Kind::Object(
118                map.iter()
119                    .map(|(k, v)| (k.clone(), Node::from_json(v)))
120                    .collect(),
121            ),
122        };
123        Node { fill: false, kind }
124    }
125
126    fn to_json(&self) -> JsonValue {
127        match &self.kind {
128            Kind::Null => JsonValue::Null,
129            Kind::Bool(b) => JsonValue::Bool(*b),
130            Kind::Number(n) => JsonValue::Number(n.clone()),
131            Kind::String(s) => JsonValue::String(s.clone()),
132            Kind::Array(items) => JsonValue::Array(items.iter().map(Node::to_json).collect()),
133            Kind::Object(entries) => JsonValue::Object(
134                entries
135                    .iter()
136                    .map(|(k, n)| (k.clone(), n.to_json()))
137                    .collect(),
138            ),
139        }
140    }
141}
142
143/// `true` when `value` nests deeper than `max_depth` container levels.
144///
145/// Every path that stores a value into a `Document` — markdown parse,
146/// DTO/wire deserialization, the typed mutators, the binding converters —
147/// bounds nesting at the spec §8 limit
148/// ([`crate::document::limits::MAX_YAML_DEPTH`]), which makes the recursive
149/// consumers (emit, plate-JSON serialization, DTO conversion) bounded by
150/// construction. The walk is iterative (explicit stack), so the check
151/// itself cannot overflow on adversarially deep input — the very condition
152/// it exists to detect.
153pub fn json_depth_exceeds(value: &serde_json::Value, max_depth: usize) -> bool {
154    use serde_json::Value;
155    // (value, depth) pairs; depth counts container levels entered.
156    let mut stack: Vec<(&Value, usize)> = vec![(value, 0)];
157    while let Some((v, depth)) = stack.pop() {
158        match v {
159            Value::Array(items) => {
160                if depth + 1 > max_depth && !items.is_empty() {
161                    return true;
162                }
163                stack.extend(items.iter().map(|c| (c, depth + 1)));
164            }
165            Value::Object(map) => {
166                if depth + 1 > max_depth && !map.is_empty() {
167                    return true;
168                }
169                stack.extend(map.values().map(|c| (c, depth + 1)));
170            }
171            _ => {}
172        }
173    }
174    false
175}
176
177impl QuillValue {
178    fn from_node(node: Node) -> Self {
179        QuillValue {
180            node,
181            json: OnceLock::new(),
182        }
183    }
184
185    /// Create a QuillValue from a YAML string
186    pub fn from_yaml_str(yaml_str: &str) -> Result<Self, serde_saphyr::Error> {
187        let json_val: serde_json::Value = serde_saphyr::from_str(yaml_str)?;
188        Ok(Self::from_json(json_val))
189    }
190
191    /// Get a reference to the value's JSON projection.
192    ///
193    /// The projection is materialized on first use and cached. It carries
194    /// the data only; `!must_fill` markers are not represented in JSON.
195    pub fn as_json(&self) -> &serde_json::Value {
196        self.json.get_or_init(|| self.node.to_json())
197    }
198
199    /// Convert into the underlying JSON value (fill markers are dropped).
200    pub fn into_json(self) -> serde_json::Value {
201        match self.json.into_inner() {
202            Some(json) => json,
203            None => self.node.to_json(),
204        }
205    }
206
207    /// Create a QuillValue from a JSON value, with every node `fill = false`.
208    pub fn from_json(json_val: serde_json::Value) -> Self {
209        let node = Node::from_json(&json_val);
210        let json = OnceLock::new();
211        // Seed the projection with the value we were handed so the common
212        // render path doesn't re-lower it. This trades memory for speed:
213        // until dropped, the data is held twice (the `node` tree plus the
214        // cached JSON). Acceptable for the render-hot path; leaving the cache
215        // empty here would halve memory at the cost of re-lowering on first
216        // `as_json`.
217        let _ = json.set(json_val);
218        QuillValue { node, json }
219    }
220
221    /// String value.
222    pub fn string(s: impl Into<String>) -> Self {
223        Self::from_json(serde_json::Value::String(s.into()))
224    }
225
226    /// Integer value.
227    pub fn integer(n: i64) -> Self {
228        Self::from_json(serde_json::Value::Number(n.into()))
229    }
230
231    /// Boolean value.
232    pub fn bool(b: bool) -> Self {
233        Self::from_json(serde_json::Value::Bool(b))
234    }
235
236    /// Null value.
237    pub fn null() -> Self {
238        Self::from_json(serde_json::Value::Null)
239    }
240
241    /// Whether this value's root node carries the `!must_fill` marker.
242    pub fn fill(&self) -> bool {
243        self.node.fill
244    }
245
246    /// Set the root node's `!must_fill` marker, consuming `self`.
247    pub fn with_fill(mut self, fill: bool) -> Self {
248        self.node.fill = fill;
249        self
250    }
251
252    /// Set the root node's `!must_fill` marker in place.
253    ///
254    /// Does not invalidate the JSON projection: `fill` is never part of it.
255    pub fn set_fill(&mut self, fill: bool) {
256        self.node.fill = fill;
257    }
258
259    /// Paths (relative to this value's root) of every node carrying the
260    /// `!must_fill` marker. The root, if filled, is reported as the empty
261    /// path. The JSON projection carries no fill, so this is the only way to
262    /// observe nested fill markers.
263    pub fn fill_paths(&self) -> Vec<Vec<PathSegment>> {
264        let mut out = Vec::new();
265        let mut prefix = Vec::new();
266        collect_fill_paths(&self.node, &mut prefix, &mut out);
267        out
268    }
269
270    /// Fill paths *nested inside* this value — every [`fill_paths`](Self::fill_paths)
271    /// entry except the empty (root) path. A root fill is carried separately as
272    /// the `fill` flag on the owning field, so the wire / storage DTO record
273    /// only the nested ones here.
274    pub fn nonroot_fill_paths(&self) -> impl Iterator<Item = Vec<PathSegment>> {
275        self.fill_paths().into_iter().filter(|p| !p.is_empty())
276    }
277
278    /// Set `fill = true` on the node at `path` (relative to the root).
279    /// Returns `false` if the path does not resolve to a node.
280    pub fn set_fill_at(&mut self, path: &[PathSegment]) -> bool {
281        match node_at_mut(&mut self.node, path) {
282            Some(n) => {
283                n.fill = true;
284                true
285            }
286            None => false,
287        }
288    }
289
290    /// Whether the node at `path` (relative to the root) is a mapping.
291    /// Used to reject `!must_fill` on object-valued nodes.
292    pub fn is_object_at(&self, path: &[PathSegment]) -> bool {
293        node_is_object(&self.node, path)
294    }
295}
296
297impl Deref for QuillValue {
298    type Target = serde_json::Value;
299
300    fn deref(&self) -> &Self::Target {
301        self.as_json()
302    }
303}
304
305impl PartialEq for QuillValue {
306    /// Two values are equal when their annotated trees (data **and** fill)
307    /// are equal. The cached JSON projection is derived and not compared.
308    fn eq(&self, other: &Self) -> bool {
309        self.node == other.node
310    }
311}
312
313impl Clone for QuillValue {
314    fn clone(&self) -> Self {
315        let json = OnceLock::new();
316        if let Some(cached) = self.json.get() {
317            let _ = json.set(cached.clone());
318        }
319        QuillValue {
320            node: self.node.clone(),
321            json,
322        }
323    }
324}
325
326impl std::fmt::Debug for QuillValue {
327    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
328        if self.node.fill {
329            write!(f, "QuillValue(!must_fill {:?})", self.as_json())
330        } else {
331            write!(f, "QuillValue({:?})", self.as_json())
332        }
333    }
334}
335
336impl Serialize for QuillValue {
337    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
338        self.as_json().serialize(serializer)
339    }
340}
341
342impl<'de> Deserialize<'de> for QuillValue {
343    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
344        let json = serde_json::Value::deserialize(deserializer)?;
345        Ok(QuillValue::from_json(json))
346    }
347}
348
349// Common delegating accessors, projected through the JSON view so existing
350// consumers keep their serde_json-shaped API unchanged.
351impl QuillValue {
352    /// Check if the value is null
353    pub fn is_null(&self) -> bool {
354        self.as_json().is_null()
355    }
356
357    /// Get the value as a string reference
358    pub fn as_str(&self) -> Option<&str> {
359        self.as_json().as_str()
360    }
361
362    /// Get the value as a boolean
363    pub fn as_bool(&self) -> Option<bool> {
364        self.as_json().as_bool()
365    }
366
367    /// Get the value as an i64
368    pub fn as_i64(&self) -> Option<i64> {
369        self.as_json().as_i64()
370    }
371
372    /// Get the value as a u64
373    pub fn as_u64(&self) -> Option<u64> {
374        self.as_json().as_u64()
375    }
376
377    /// Get the value as an f64
378    pub fn as_f64(&self) -> Option<f64> {
379        self.as_json().as_f64()
380    }
381
382    /// Get the value as an array reference
383    pub fn as_array(&self) -> Option<&Vec<serde_json::Value>> {
384        self.as_json().as_array()
385    }
386
387    /// Get the value as an object reference
388    pub fn as_object(&self) -> Option<&serde_json::Map<String, serde_json::Value>> {
389        self.as_json().as_object()
390    }
391
392    /// Get a field from an object by key, preserving the child's fill markers.
393    pub fn get(&self, key: &str) -> Option<QuillValue> {
394        match &self.node.kind {
395            Kind::Object(entries) => entries.get(key).map(|n| QuillValue::from_node(n.clone())),
396            _ => None,
397        }
398    }
399}
400
401#[cfg(test)]
402mod tests {
403    use super::*;
404
405    #[test]
406    fn test_from_yaml_value() {
407        let yaml_str = r#"
408            package:
409              name: test
410              version: 1.0.0
411        "#;
412        let json_val: serde_json::Value = serde_saphyr::from_str(yaml_str).unwrap();
413        let quill_val = QuillValue::from_json(json_val);
414
415        assert!(quill_val.as_object().is_some());
416        assert_eq!(
417            quill_val
418                .get("package")
419                .unwrap()
420                .get("name")
421                .unwrap()
422                .as_str(),
423            Some("test")
424        );
425    }
426
427    #[test]
428    fn test_from_yaml_str() {
429        let yaml_str = r#"
430            title: Test Document
431            author: John Doe
432            count: 42
433        "#;
434        let quill_val = QuillValue::from_yaml_str(yaml_str).unwrap();
435
436        assert_eq!(
437            quill_val.get("title").as_ref().and_then(|v| v.as_str()),
438            Some("Test Document")
439        );
440        assert_eq!(
441            quill_val.get("author").as_ref().and_then(|v| v.as_str()),
442            Some("John Doe")
443        );
444        assert_eq!(
445            quill_val.get("count").as_ref().and_then(|v| v.as_i64()),
446            Some(42)
447        );
448    }
449
450    #[test]
451    fn test_delegating_methods() {
452        let quill_val = QuillValue::from_json(serde_json::json!({
453            "name": "test",
454            "count": 42,
455            "active": true,
456            "items": [1, 2, 3]
457        }));
458
459        assert_eq!(
460            quill_val.get("name").as_ref().and_then(|v| v.as_str()),
461            Some("test")
462        );
463        assert_eq!(
464            quill_val.get("count").as_ref().and_then(|v| v.as_i64()),
465            Some(42)
466        );
467        assert_eq!(
468            quill_val.get("active").as_ref().and_then(|v| v.as_bool()),
469            Some(true)
470        );
471        assert!(quill_val
472            .get("items")
473            .as_ref()
474            .and_then(|v| v.as_array())
475            .is_some());
476    }
477
478    #[test]
479    fn test_yaml_custom_tags_ignored_at_value_level() {
480        // At the raw `QuillValue::from_yaml_str` layer, custom YAML tags
481        // (including `!must_fill`) pass through serde_saphyr which drops the
482        // tag and returns the underlying scalar.  The tag is recovered at
483        // the `Document` layer by `document::prescan`: see
484        // `document::tests::lossiness_tests::custom_tags_lose_tag_but_keep_value`.
485        let yaml_str = "memo_from: !must_fill 2d lt example";
486        let quill_val = QuillValue::from_yaml_str(yaml_str).unwrap();
487
488        assert_eq!(
489            quill_val.get("memo_from").as_ref().and_then(|v| v.as_str()),
490            Some("2d lt example")
491        );
492    }
493
494    #[test]
495    fn json_round_trips_through_the_tree() {
496        // from_json → as_json must be identity, preserving object key order
497        // (serde_json `preserve_order`) and number kinds.
498        let original = serde_json::json!({
499            "z": 1,
500            "a": [true, "x", 3.5, null],
501            "nested": { "k": 42 }
502        });
503        let qv = QuillValue::from_json(original.clone());
504        assert_eq!(qv.as_json(), &original);
505
506        // A value re-lowered from the tree (not the seeded cache) also matches.
507        let relowered = QuillValue::from_node(qv.node.clone()).into_json();
508        assert_eq!(relowered, original);
509    }
510
511    #[test]
512    fn fill_marker_rides_on_the_node_not_the_json() {
513        let qv = QuillValue::string("draft").with_fill(true);
514        assert!(qv.fill());
515        // Projection is fill-free and equal to the plain scalar.
516        assert_eq!(qv.as_json(), &serde_json::json!("draft"));
517        // Equality is fill-sensitive.
518        assert_ne!(qv, QuillValue::string("draft"));
519        assert_eq!(qv, QuillValue::string("draft").with_fill(true));
520    }
521}