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
16impl QuillValue {
17    // from_yaml removed as we use serde_json::Value directly
18
19    /// Create a QuillValue from a YAML string
20    pub fn from_yaml_str(yaml_str: &str) -> Result<Self, serde_saphyr::Error> {
21        let json_val: serde_json::Value = serde_saphyr::from_str(yaml_str)?;
22        Ok(QuillValue(json_val))
23    }
24
25    /// Get a reference to the underlying JSON value
26    pub fn as_json(&self) -> &serde_json::Value {
27        &self.0
28    }
29
30    /// Convert into the underlying JSON value
31    pub fn into_json(self) -> serde_json::Value {
32        self.0
33    }
34
35    /// Create a QuillValue directly from a JSON value
36    pub fn from_json(json_val: serde_json::Value) -> Self {
37        QuillValue(json_val)
38    }
39
40    /// String value.
41    pub fn string(s: impl Into<String>) -> Self {
42        QuillValue(serde_json::Value::String(s.into()))
43    }
44
45    /// Integer value.
46    pub fn integer(n: i64) -> Self {
47        QuillValue(serde_json::Value::Number(n.into()))
48    }
49
50    /// Boolean value.
51    pub fn bool(b: bool) -> Self {
52        QuillValue(serde_json::Value::Bool(b))
53    }
54
55    /// Null value.
56    pub fn null() -> Self {
57        QuillValue(serde_json::Value::Null)
58    }
59}
60
61impl Deref for QuillValue {
62    type Target = serde_json::Value;
63
64    fn deref(&self) -> &Self::Target {
65        &self.0
66    }
67}
68
69// Implement common delegating methods for convenience
70impl QuillValue {
71    /// Check if the value is null
72    pub fn is_null(&self) -> bool {
73        self.0.is_null()
74    }
75
76    /// Get the value as a string reference
77    pub fn as_str(&self) -> Option<&str> {
78        self.0.as_str()
79    }
80
81    /// Get the value as a boolean
82    pub fn as_bool(&self) -> Option<bool> {
83        self.0.as_bool()
84    }
85
86    /// Get the value as an i64
87    pub fn as_i64(&self) -> Option<i64> {
88        self.0.as_i64()
89    }
90
91    /// Get the value as a u64
92    pub fn as_u64(&self) -> Option<u64> {
93        self.0.as_u64()
94    }
95
96    /// Get the value as an f64
97    pub fn as_f64(&self) -> Option<f64> {
98        self.0.as_f64()
99    }
100
101    /// Get the value as an array reference
102    pub fn as_array(&self) -> Option<&Vec<serde_json::Value>> {
103        self.0.as_array()
104    }
105
106    /// Get the value as an object reference
107    pub fn as_object(&self) -> Option<&serde_json::Map<String, serde_json::Value>> {
108        self.0.as_object()
109    }
110
111    /// Get a field from an object by key
112    pub fn get(&self, key: &str) -> Option<QuillValue> {
113        self.0.get(key).map(|v| QuillValue(v.clone()))
114    }
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120
121    #[test]
122    fn test_from_yaml_value() {
123        let yaml_str = r#"
124            package:
125              name: test
126              version: 1.0.0
127        "#;
128        let json_val: serde_json::Value = serde_saphyr::from_str(yaml_str).unwrap();
129        let quill_val = QuillValue::from_json(json_val);
130
131        assert!(quill_val.as_object().is_some());
132        assert_eq!(
133            quill_val
134                .get("package")
135                .unwrap()
136                .get("name")
137                .unwrap()
138                .as_str(),
139            Some("test")
140        );
141    }
142
143    #[test]
144    fn test_from_yaml_str() {
145        let yaml_str = r#"
146            title: Test Document
147            author: John Doe
148            count: 42
149        "#;
150        let quill_val = QuillValue::from_yaml_str(yaml_str).unwrap();
151
152        assert_eq!(
153            quill_val.get("title").as_ref().and_then(|v| v.as_str()),
154            Some("Test Document")
155        );
156        assert_eq!(
157            quill_val.get("author").as_ref().and_then(|v| v.as_str()),
158            Some("John Doe")
159        );
160        assert_eq!(
161            quill_val.get("count").as_ref().and_then(|v| v.as_i64()),
162            Some(42)
163        );
164    }
165
166    #[test]
167    fn test_as_json() {
168        let json_val = serde_json::json!({"key": "value"});
169        let quill_val = QuillValue::from_json(json_val.clone());
170
171        assert_eq!(quill_val.as_json(), &json_val);
172    }
173
174    #[test]
175    fn test_into_json() {
176        let json_val = serde_json::json!({"key": "value"});
177        let quill_val = QuillValue::from_json(json_val.clone());
178
179        assert_eq!(quill_val.into_json(), json_val);
180    }
181
182    #[test]
183    fn test_delegating_methods() {
184        let quill_val = QuillValue::from_json(serde_json::json!({
185            "name": "test",
186            "count": 42,
187            "active": true,
188            "items": [1, 2, 3]
189        }));
190
191        assert_eq!(
192            quill_val.get("name").as_ref().and_then(|v| v.as_str()),
193            Some("test")
194        );
195        assert_eq!(
196            quill_val.get("count").as_ref().and_then(|v| v.as_i64()),
197            Some(42)
198        );
199        assert_eq!(
200            quill_val.get("active").as_ref().and_then(|v| v.as_bool()),
201            Some(true)
202        );
203        assert!(quill_val
204            .get("items")
205            .as_ref()
206            .and_then(|v| v.as_array())
207            .is_some());
208    }
209
210    #[test]
211    fn test_yaml_with_tags() {
212        // Note: serde_saphyr handles tags differently - this tests basic parsing
213        let yaml_str = r#"
214            value: 42
215        "#;
216        let quill_val = QuillValue::from_yaml_str(yaml_str).unwrap();
217
218        // Values should be converted to their underlying value
219        assert!(quill_val.as_object().is_some());
220    }
221
222    #[test]
223    fn test_null_value() {
224        let quill_val = QuillValue::from_json(serde_json::Value::Null);
225        assert!(quill_val.is_null());
226    }
227
228    #[test]
229    fn test_yaml_custom_tags_ignored_at_value_level() {
230        // At the raw `QuillValue::from_yaml_str` layer, custom YAML tags
231        // (including `!fill`) pass through serde_saphyr which drops the
232        // tag and returns the underlying scalar.  The tag is recovered at
233        // the `Document` layer by `document::prescan`: see
234        // `document::tests::lossiness_tests::custom_tags_lose_tag_but_keep_value`.
235        let yaml_str = "memo_from: !fill 2d lt example";
236        let quill_val = QuillValue::from_yaml_str(yaml_str).unwrap();
237
238        assert_eq!(
239            quill_val.get("memo_from").as_ref().and_then(|v| v.as_str()),
240            Some("2d lt example")
241        );
242    }
243}