Skip to main content

quillmark_core/
value.rs

1//! Value type for unified representation of TOML/YAML/JSON values.
2//!
3//! This module provides [`QuillValue`], a newtype wrapper around `serde_json::Value`
4//! that centralizes all value conversions across the Quillmark system.
5
6use serde::{Deserialize, Serialize};
7use std::ops::Deref;
8
9/// Unified value type backed by `serde_json::Value`.
10///
11/// This type is used throughout Quillmark to represent metadata, fields, and other
12/// dynamic values. It provides conversion methods for TOML and YAML.
13#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
14pub struct QuillValue(serde_json::Value);
15
16/// `true` when `value` nests deeper than `max_depth` container levels.
17///
18/// Every path that stores a value into a `Document` โ€” markdown parse,
19/// DTO/wire deserialization, the typed mutators, the binding converters โ€”
20/// bounds nesting at the spec ยง8 limit
21/// ([`crate::document::limits::MAX_YAML_DEPTH`]), which makes the recursive
22/// consumers (emit, plate-JSON serialization, DTO conversion) bounded by
23/// construction. The walk is iterative (explicit stack), so the check
24/// itself cannot overflow on adversarially deep input โ€” the very condition
25/// it exists to detect.
26pub fn json_depth_exceeds(value: &serde_json::Value, max_depth: usize) -> bool {
27    use serde_json::Value;
28    // (value, depth) pairs; depth counts container levels entered.
29    let mut stack: Vec<(&Value, usize)> = vec![(value, 0)];
30    while let Some((v, depth)) = stack.pop() {
31        match v {
32            Value::Array(items) => {
33                if depth + 1 > max_depth && !items.is_empty() {
34                    return true;
35                }
36                stack.extend(items.iter().map(|c| (c, depth + 1)));
37            }
38            Value::Object(map) => {
39                if depth + 1 > max_depth && !map.is_empty() {
40                    return true;
41                }
42                stack.extend(map.values().map(|c| (c, depth + 1)));
43            }
44            _ => {}
45        }
46    }
47    false
48}
49
50impl QuillValue {
51    /// Create a QuillValue from a YAML string
52    pub fn from_yaml_str(yaml_str: &str) -> Result<Self, serde_saphyr::Error> {
53        let json_val: serde_json::Value = serde_saphyr::from_str(yaml_str)?;
54        Ok(QuillValue(json_val))
55    }
56
57    /// Get a reference to the underlying JSON value
58    pub fn as_json(&self) -> &serde_json::Value {
59        &self.0
60    }
61
62    /// Convert into the underlying JSON value
63    pub fn into_json(self) -> serde_json::Value {
64        self.0
65    }
66
67    /// Create a QuillValue directly from a JSON value
68    pub fn from_json(json_val: serde_json::Value) -> Self {
69        QuillValue(json_val)
70    }
71
72    /// String value.
73    pub fn string(s: impl Into<String>) -> Self {
74        QuillValue(serde_json::Value::String(s.into()))
75    }
76
77    /// Integer value.
78    pub fn integer(n: i64) -> Self {
79        QuillValue(serde_json::Value::Number(n.into()))
80    }
81
82    /// Boolean value.
83    pub fn bool(b: bool) -> Self {
84        QuillValue(serde_json::Value::Bool(b))
85    }
86
87    /// Null value.
88    pub fn null() -> Self {
89        QuillValue(serde_json::Value::Null)
90    }
91}
92
93impl Deref for QuillValue {
94    type Target = serde_json::Value;
95
96    fn deref(&self) -> &Self::Target {
97        &self.0
98    }
99}
100
101// Implement common delegating methods for convenience
102impl QuillValue {
103    /// Check if the value is null
104    pub fn is_null(&self) -> bool {
105        self.0.is_null()
106    }
107
108    /// Get the value as a string reference
109    pub fn as_str(&self) -> Option<&str> {
110        self.0.as_str()
111    }
112
113    /// Get the value as a boolean
114    pub fn as_bool(&self) -> Option<bool> {
115        self.0.as_bool()
116    }
117
118    /// Get the value as an i64
119    pub fn as_i64(&self) -> Option<i64> {
120        self.0.as_i64()
121    }
122
123    /// Get the value as a u64
124    pub fn as_u64(&self) -> Option<u64> {
125        self.0.as_u64()
126    }
127
128    /// Get the value as an f64
129    pub fn as_f64(&self) -> Option<f64> {
130        self.0.as_f64()
131    }
132
133    /// Get the value as an array reference
134    pub fn as_array(&self) -> Option<&Vec<serde_json::Value>> {
135        self.0.as_array()
136    }
137
138    /// Get the value as an object reference
139    pub fn as_object(&self) -> Option<&serde_json::Map<String, serde_json::Value>> {
140        self.0.as_object()
141    }
142
143    /// Get a field from an object by key
144    pub fn get(&self, key: &str) -> Option<QuillValue> {
145        self.0.get(key).map(|v| QuillValue(v.clone()))
146    }
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152
153    #[test]
154    fn test_from_yaml_value() {
155        let yaml_str = r#"
156            package:
157              name: test
158              version: 1.0.0
159        "#;
160        let json_val: serde_json::Value = serde_saphyr::from_str(yaml_str).unwrap();
161        let quill_val = QuillValue::from_json(json_val);
162
163        assert!(quill_val.as_object().is_some());
164        assert_eq!(
165            quill_val
166                .get("package")
167                .unwrap()
168                .get("name")
169                .unwrap()
170                .as_str(),
171            Some("test")
172        );
173    }
174
175    #[test]
176    fn test_from_yaml_str() {
177        let yaml_str = r#"
178            title: Test Document
179            author: John Doe
180            count: 42
181        "#;
182        let quill_val = QuillValue::from_yaml_str(yaml_str).unwrap();
183
184        assert_eq!(
185            quill_val.get("title").as_ref().and_then(|v| v.as_str()),
186            Some("Test Document")
187        );
188        assert_eq!(
189            quill_val.get("author").as_ref().and_then(|v| v.as_str()),
190            Some("John Doe")
191        );
192        assert_eq!(
193            quill_val.get("count").as_ref().and_then(|v| v.as_i64()),
194            Some(42)
195        );
196    }
197
198    #[test]
199    fn test_delegating_methods() {
200        let quill_val = QuillValue::from_json(serde_json::json!({
201            "name": "test",
202            "count": 42,
203            "active": true,
204            "items": [1, 2, 3]
205        }));
206
207        assert_eq!(
208            quill_val.get("name").as_ref().and_then(|v| v.as_str()),
209            Some("test")
210        );
211        assert_eq!(
212            quill_val.get("count").as_ref().and_then(|v| v.as_i64()),
213            Some(42)
214        );
215        assert_eq!(
216            quill_val.get("active").as_ref().and_then(|v| v.as_bool()),
217            Some(true)
218        );
219        assert!(quill_val
220            .get("items")
221            .as_ref()
222            .and_then(|v| v.as_array())
223            .is_some());
224    }
225
226    #[test]
227    fn test_yaml_custom_tags_ignored_at_value_level() {
228        // At the raw `QuillValue::from_yaml_str` layer, custom YAML tags
229        // (including `!fill`) pass through serde_saphyr which drops the
230        // tag and returns the underlying scalar.  The tag is recovered at
231        // the `Document` layer by `document::prescan`: see
232        // `document::tests::lossiness_tests::custom_tags_lose_tag_but_keep_value`.
233        let yaml_str = "memo_from: !fill 2d lt example";
234        let quill_val = QuillValue::from_yaml_str(yaml_str).unwrap();
235
236        assert_eq!(
237            quill_val.get("memo_from").as_ref().and_then(|v| v.as_str()),
238            Some("2d lt example")
239        );
240    }
241}
242