quillmark-core 0.92.1

Core types and functionality for Quillmark
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
//! Value type for unified representation of TOML/YAML/JSON values.
//!
//! [`QuillValue`] is an **annotated value tree**: every node carries a
//! `fill` flag (the in-memory form of the `!must_fill` YAML tag) alongside
//! its data. The tree is the authoritative representation. For the data
//! API (`as_json`, `as_array`, `as_object`, `Deref`) a plain
//! [`serde_json::Value`] projection is materialized lazily and cached; that
//! projection is **fill-free** — it is a derived view of the data, not a
//! second source of truth. Fill never reaches the JSON projection, so
//! rendering and wire layers that consume `as_json()` are unaffected by it.

use indexmap::IndexMap;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde_json::Value as JsonValue;
use std::ops::Deref;
use std::sync::OnceLock;

/// Unified value type: an annotated tree of JSON-shaped data where every
/// node additionally records whether it was tagged `!must_fill`.
///
/// Construction (`from_json`, `from_yaml_str`, the scalar constructors)
/// produces nodes with `fill = false`; the `!must_fill` markers are applied
/// by the document layer. `QuillValue` exposes no data-mutating methods —
/// only `set_fill`/`with_fill`, which do not affect the JSON projection —
/// so the cached projection never goes stale.
pub struct QuillValue {
    node: Node,
    /// Lazily materialized, fill-free [`serde_json::Value`] view of `node`.
    json: OnceLock<JsonValue>,
}

/// One node of the annotated tree: a `fill` flag plus the data.
#[derive(Debug, Clone, PartialEq)]
struct Node {
    fill: bool,
    kind: Kind,
}

#[derive(Debug, Clone, PartialEq)]
enum Kind {
    Null,
    Bool(bool),
    Number(serde_json::Number),
    String(String),
    Array(Vec<Node>),
    Object(IndexMap<String, Node>),
}

/// One step of a path into a value tree: an object key or an array index.
///
/// This is the canonical path-segment type for the whole crate; the document
/// layer aliases it as `CommentPathSegment` for nested-comment paths.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum PathSegment {
    Key(String),
    Index(usize),
}

fn collect_fill_paths(node: &Node, prefix: &mut Vec<PathSegment>, out: &mut Vec<Vec<PathSegment>>) {
    if node.fill {
        out.push(prefix.clone());
    }
    match &node.kind {
        Kind::Array(items) => {
            for (i, child) in items.iter().enumerate() {
                prefix.push(PathSegment::Index(i));
                collect_fill_paths(child, prefix, out);
                prefix.pop();
            }
        }
        Kind::Object(entries) => {
            for (k, child) in entries {
                prefix.push(PathSegment::Key(k.clone()));
                collect_fill_paths(child, prefix, out);
                prefix.pop();
            }
        }
        _ => {}
    }
}

fn node_at_mut<'a>(node: &'a mut Node, path: &[PathSegment]) -> Option<&'a mut Node> {
    let mut cur = node;
    for seg in path {
        cur = match (&mut cur.kind, seg) {
            (Kind::Object(entries), PathSegment::Key(k)) => entries.get_mut(k)?,
            (Kind::Array(items), PathSegment::Index(i)) => items.get_mut(*i)?,
            _ => return None,
        };
    }
    Some(cur)
}

fn node_is_object(node: &Node, path: &[PathSegment]) -> bool {
    fn at<'a>(node: &'a Node, path: &[PathSegment]) -> Option<&'a Node> {
        let mut cur = node;
        for seg in path {
            cur = match (&cur.kind, seg) {
                (Kind::Object(entries), PathSegment::Key(k)) => entries.get(k)?,
                (Kind::Array(items), PathSegment::Index(i)) => items.get(*i)?,
                _ => return None,
            };
        }
        Some(cur)
    }
    matches!(at(node, path).map(|n| &n.kind), Some(Kind::Object(_)))
}

impl Node {
    fn from_json(value: &JsonValue) -> Node {
        let kind = match value {
            JsonValue::Null => Kind::Null,
            JsonValue::Bool(b) => Kind::Bool(*b),
            JsonValue::Number(n) => Kind::Number(n.clone()),
            JsonValue::String(s) => Kind::String(s.clone()),
            JsonValue::Array(items) => Kind::Array(items.iter().map(Node::from_json).collect()),
            JsonValue::Object(map) => Kind::Object(
                map.iter()
                    .map(|(k, v)| (k.clone(), Node::from_json(v)))
                    .collect(),
            ),
        };
        Node { fill: false, kind }
    }

    fn to_json(&self) -> JsonValue {
        match &self.kind {
            Kind::Null => JsonValue::Null,
            Kind::Bool(b) => JsonValue::Bool(*b),
            Kind::Number(n) => JsonValue::Number(n.clone()),
            Kind::String(s) => JsonValue::String(s.clone()),
            Kind::Array(items) => JsonValue::Array(items.iter().map(Node::to_json).collect()),
            Kind::Object(entries) => JsonValue::Object(
                entries
                    .iter()
                    .map(|(k, n)| (k.clone(), n.to_json()))
                    .collect(),
            ),
        }
    }
}

/// `true` when `value` nests deeper than `max_depth` container levels.
///
/// Every path that stores a value into a `Document` — markdown parse,
/// DTO/wire deserialization, the typed mutators, the binding converters —
/// bounds nesting at the spec §8 limit
/// ([`crate::document::limits::MAX_YAML_DEPTH`]), which makes the recursive
/// consumers (emit, plate-JSON serialization, DTO conversion) bounded by
/// construction. The walk is iterative (explicit stack), so the check
/// itself cannot overflow on adversarially deep input — the very condition
/// it exists to detect.
pub fn json_depth_exceeds(value: &serde_json::Value, max_depth: usize) -> bool {
    use serde_json::Value;
    // (value, depth) pairs; depth counts container levels entered.
    let mut stack: Vec<(&Value, usize)> = vec![(value, 0)];
    while let Some((v, depth)) = stack.pop() {
        match v {
            Value::Array(items) => {
                if depth + 1 > max_depth && !items.is_empty() {
                    return true;
                }
                stack.extend(items.iter().map(|c| (c, depth + 1)));
            }
            Value::Object(map) => {
                if depth + 1 > max_depth && !map.is_empty() {
                    return true;
                }
                stack.extend(map.values().map(|c| (c, depth + 1)));
            }
            _ => {}
        }
    }
    false
}

impl QuillValue {
    fn from_node(node: Node) -> Self {
        QuillValue {
            node,
            json: OnceLock::new(),
        }
    }

    /// Create a QuillValue from a YAML string
    pub fn from_yaml_str(yaml_str: &str) -> Result<Self, serde_saphyr::Error> {
        let json_val: serde_json::Value = serde_saphyr::from_str(yaml_str)?;
        Ok(Self::from_json(json_val))
    }

    /// Get a reference to the value's JSON projection.
    ///
    /// The projection is materialized on first use and cached. It carries
    /// the data only; `!must_fill` markers are not represented in JSON.
    pub fn as_json(&self) -> &serde_json::Value {
        self.json.get_or_init(|| self.node.to_json())
    }

    /// Convert into the underlying JSON value (fill markers are dropped).
    pub fn into_json(self) -> serde_json::Value {
        match self.json.into_inner() {
            Some(json) => json,
            None => self.node.to_json(),
        }
    }

    /// Create a QuillValue from a JSON value, with every node `fill = false`.
    pub fn from_json(json_val: serde_json::Value) -> Self {
        let node = Node::from_json(&json_val);
        let json = OnceLock::new();
        // Seed the projection with the value we were handed so the common
        // render path doesn't re-lower it. This trades memory for speed:
        // until dropped, the data is held twice (the `node` tree plus the
        // cached JSON). Acceptable for the render-hot path; leaving the cache
        // empty here would halve memory at the cost of re-lowering on first
        // `as_json`.
        let _ = json.set(json_val);
        QuillValue { node, json }
    }

    /// String value.
    pub fn string(s: impl Into<String>) -> Self {
        Self::from_json(serde_json::Value::String(s.into()))
    }

    /// Integer value.
    pub fn integer(n: i64) -> Self {
        Self::from_json(serde_json::Value::Number(n.into()))
    }

    /// Boolean value.
    pub fn bool(b: bool) -> Self {
        Self::from_json(serde_json::Value::Bool(b))
    }

    /// Null value.
    pub fn null() -> Self {
        Self::from_json(serde_json::Value::Null)
    }

    /// Whether this value's root node carries the `!must_fill` marker.
    pub fn fill(&self) -> bool {
        self.node.fill
    }

    /// Set the root node's `!must_fill` marker, consuming `self`.
    pub fn with_fill(mut self, fill: bool) -> Self {
        self.node.fill = fill;
        self
    }

    /// Set the root node's `!must_fill` marker in place.
    ///
    /// Does not invalidate the JSON projection: `fill` is never part of it.
    pub fn set_fill(&mut self, fill: bool) {
        self.node.fill = fill;
    }

    /// Paths (relative to this value's root) of every node carrying the
    /// `!must_fill` marker. The root, if filled, is reported as the empty
    /// path. The JSON projection carries no fill, so this is the only way to
    /// observe nested fill markers.
    pub fn fill_paths(&self) -> Vec<Vec<PathSegment>> {
        let mut out = Vec::new();
        let mut prefix = Vec::new();
        collect_fill_paths(&self.node, &mut prefix, &mut out);
        out
    }

    /// Fill paths *nested inside* this value — every [`fill_paths`](Self::fill_paths)
    /// entry except the empty (root) path. A root fill is carried separately as
    /// the `fill` flag on the owning field, so the wire / storage DTO record
    /// only the nested ones here.
    pub fn nonroot_fill_paths(&self) -> impl Iterator<Item = Vec<PathSegment>> {
        self.fill_paths().into_iter().filter(|p| !p.is_empty())
    }

    /// Set `fill = true` on the node at `path` (relative to the root).
    /// Returns `false` if the path does not resolve to a node.
    pub fn set_fill_at(&mut self, path: &[PathSegment]) -> bool {
        match node_at_mut(&mut self.node, path) {
            Some(n) => {
                n.fill = true;
                true
            }
            None => false,
        }
    }

    /// Whether the node at `path` (relative to the root) is a mapping.
    /// Used to reject `!must_fill` on object-valued nodes.
    pub fn is_object_at(&self, path: &[PathSegment]) -> bool {
        node_is_object(&self.node, path)
    }
}

impl Deref for QuillValue {
    type Target = serde_json::Value;

    fn deref(&self) -> &Self::Target {
        self.as_json()
    }
}

impl PartialEq for QuillValue {
    /// Two values are equal when their annotated trees (data **and** fill)
    /// are equal. The cached JSON projection is derived and not compared.
    fn eq(&self, other: &Self) -> bool {
        self.node == other.node
    }
}

impl Clone for QuillValue {
    fn clone(&self) -> Self {
        let json = OnceLock::new();
        if let Some(cached) = self.json.get() {
            let _ = json.set(cached.clone());
        }
        QuillValue {
            node: self.node.clone(),
            json,
        }
    }
}

impl std::fmt::Debug for QuillValue {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        if self.node.fill {
            write!(f, "QuillValue(!must_fill {:?})", self.as_json())
        } else {
            write!(f, "QuillValue({:?})", self.as_json())
        }
    }
}

impl Serialize for QuillValue {
    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        self.as_json().serialize(serializer)
    }
}

impl<'de> Deserialize<'de> for QuillValue {
    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
        let json = serde_json::Value::deserialize(deserializer)?;
        Ok(QuillValue::from_json(json))
    }
}

// Common delegating accessors, projected through the JSON view so existing
// consumers keep their serde_json-shaped API unchanged.
impl QuillValue {
    /// Check if the value is null
    pub fn is_null(&self) -> bool {
        self.as_json().is_null()
    }

    /// Get the value as a string reference
    pub fn as_str(&self) -> Option<&str> {
        self.as_json().as_str()
    }

    /// Get the value as a boolean
    pub fn as_bool(&self) -> Option<bool> {
        self.as_json().as_bool()
    }

    /// Get the value as an i64
    pub fn as_i64(&self) -> Option<i64> {
        self.as_json().as_i64()
    }

    /// Get the value as a u64
    pub fn as_u64(&self) -> Option<u64> {
        self.as_json().as_u64()
    }

    /// Get the value as an f64
    pub fn as_f64(&self) -> Option<f64> {
        self.as_json().as_f64()
    }

    /// Get the value as an array reference
    pub fn as_array(&self) -> Option<&Vec<serde_json::Value>> {
        self.as_json().as_array()
    }

    /// Get the value as an object reference
    pub fn as_object(&self) -> Option<&serde_json::Map<String, serde_json::Value>> {
        self.as_json().as_object()
    }

    /// Get a field from an object by key, preserving the child's fill markers.
    pub fn get(&self, key: &str) -> Option<QuillValue> {
        match &self.node.kind {
            Kind::Object(entries) => entries.get(key).map(|n| QuillValue::from_node(n.clone())),
            _ => None,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_from_yaml_value() {
        let yaml_str = r#"
            package:
              name: test
              version: 1.0.0
        "#;
        let json_val: serde_json::Value = serde_saphyr::from_str(yaml_str).unwrap();
        let quill_val = QuillValue::from_json(json_val);

        assert!(quill_val.as_object().is_some());
        assert_eq!(
            quill_val
                .get("package")
                .unwrap()
                .get("name")
                .unwrap()
                .as_str(),
            Some("test")
        );
    }

    #[test]
    fn test_from_yaml_str() {
        let yaml_str = r#"
            title: Test Document
            author: John Doe
            count: 42
        "#;
        let quill_val = QuillValue::from_yaml_str(yaml_str).unwrap();

        assert_eq!(
            quill_val.get("title").as_ref().and_then(|v| v.as_str()),
            Some("Test Document")
        );
        assert_eq!(
            quill_val.get("author").as_ref().and_then(|v| v.as_str()),
            Some("John Doe")
        );
        assert_eq!(
            quill_val.get("count").as_ref().and_then(|v| v.as_i64()),
            Some(42)
        );
    }

    #[test]
    fn test_delegating_methods() {
        let quill_val = QuillValue::from_json(serde_json::json!({
            "name": "test",
            "count": 42,
            "active": true,
            "items": [1, 2, 3]
        }));

        assert_eq!(
            quill_val.get("name").as_ref().and_then(|v| v.as_str()),
            Some("test")
        );
        assert_eq!(
            quill_val.get("count").as_ref().and_then(|v| v.as_i64()),
            Some(42)
        );
        assert_eq!(
            quill_val.get("active").as_ref().and_then(|v| v.as_bool()),
            Some(true)
        );
        assert!(quill_val
            .get("items")
            .as_ref()
            .and_then(|v| v.as_array())
            .is_some());
    }

    #[test]
    fn test_yaml_custom_tags_ignored_at_value_level() {
        // At the raw `QuillValue::from_yaml_str` layer, custom YAML tags
        // (including `!must_fill`) pass through serde_saphyr which drops the
        // tag and returns the underlying scalar.  The tag is recovered at
        // the `Document` layer by `document::prescan`: see
        // `document::tests::lossiness_tests::custom_tags_lose_tag_but_keep_value`.
        let yaml_str = "memo_from: !must_fill 2d lt example";
        let quill_val = QuillValue::from_yaml_str(yaml_str).unwrap();

        assert_eq!(
            quill_val.get("memo_from").as_ref().and_then(|v| v.as_str()),
            Some("2d lt example")
        );
    }

    #[test]
    fn json_round_trips_through_the_tree() {
        // from_json → as_json must be identity, preserving object key order
        // (serde_json `preserve_order`) and number kinds.
        let original = serde_json::json!({
            "z": 1,
            "a": [true, "x", 3.5, null],
            "nested": { "k": 42 }
        });
        let qv = QuillValue::from_json(original.clone());
        assert_eq!(qv.as_json(), &original);

        // A value re-lowered from the tree (not the seeded cache) also matches.
        let relowered = QuillValue::from_node(qv.node.clone()).into_json();
        assert_eq!(relowered, original);
    }

    #[test]
    fn fill_marker_rides_on_the_node_not_the_json() {
        let qv = QuillValue::string("draft").with_fill(true);
        assert!(qv.fill());
        // Projection is fill-free and equal to the plain scalar.
        assert_eq!(qv.as_json(), &serde_json::json!("draft"));
        // Equality is fill-sensitive.
        assert_ne!(qv, QuillValue::string("draft"));
        assert_eq!(qv, QuillValue::string("draft").with_fill(true));
    }
}