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 minijinja::value::Value as MjValue;
7use serde::{Deserialize, Serialize};
8use std::collections::BTreeMap;
9use std::ops::Deref;
10
11/// Unified value type backed by `serde_json::Value`.
12///
13/// This type is used throughout Quillmark to represent metadata, fields, and other
14/// dynamic values. It provides conversion methods for TOML, YAML, and MiniJinja.
15#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
16pub struct QuillValue(serde_json::Value);
17
18impl QuillValue {
19    /// Create a QuillValue from a TOML value
20    pub fn from_toml(toml_val: &toml::Value) -> Result<Self, serde_json::Error> {
21        let json_val = serde_json::to_value(toml_val)?;
22        Ok(QuillValue(json_val))
23    }
24
25    /// Create a QuillValue from a YAML string
26    pub fn from_yaml_str(yaml_str: &str) -> Result<Self, serde_saphyr::Error> {
27        let json_val: serde_json::Value = serde_saphyr::from_str(yaml_str)?;
28        Ok(QuillValue(json_val))
29    }
30
31    /// Convert to a MiniJinja value for templating
32    pub fn to_minijinja(&self) -> Result<MjValue, String> {
33        json_to_minijinja(&self.0)
34    }
35
36    /// Get a reference to the underlying JSON value
37    pub fn as_json(&self) -> &serde_json::Value {
38        &self.0
39    }
40
41    /// Convert into the underlying JSON value
42    pub fn into_json(self) -> serde_json::Value {
43        self.0
44    }
45
46    /// Create a QuillValue directly from a JSON value
47    pub fn from_json(json_val: serde_json::Value) -> Self {
48        QuillValue(json_val)
49    }
50}
51
52impl Deref for QuillValue {
53    type Target = serde_json::Value;
54
55    fn deref(&self) -> &Self::Target {
56        &self.0
57    }
58}
59
60/// Convert a JSON value to a MiniJinja value
61fn json_to_minijinja(value: &serde_json::Value) -> Result<MjValue, String> {
62    use serde_json::Value as JsonValue;
63
64    let result = match value {
65        JsonValue::Null => MjValue::from(()),
66        JsonValue::Bool(b) => MjValue::from(*b),
67        JsonValue::Number(n) => {
68            if let Some(i) = n.as_i64() {
69                MjValue::from(i)
70            } else if let Some(u) = n.as_u64() {
71                MjValue::from(u)
72            } else if let Some(f) = n.as_f64() {
73                MjValue::from(f)
74            } else {
75                return Err("Invalid number in JSON".to_string());
76            }
77        }
78        JsonValue::String(s) => MjValue::from(s.clone()),
79        JsonValue::Array(arr) => {
80            let mut vec = Vec::new();
81            for item in arr {
82                vec.push(json_to_minijinja(item)?);
83            }
84            MjValue::from(vec)
85        }
86        JsonValue::Object(map) => {
87            let mut obj = BTreeMap::new();
88            for (k, v) in map {
89                obj.insert(k.clone(), json_to_minijinja(v)?);
90            }
91            MjValue::from_object(obj)
92        }
93    };
94
95    Ok(result)
96}
97
98// Implement common delegating methods for convenience
99impl QuillValue {
100    /// Check if the value is null
101    pub fn is_null(&self) -> bool {
102        self.0.is_null()
103    }
104
105    /// Get the value as a string reference
106    pub fn as_str(&self) -> Option<&str> {
107        self.0.as_str()
108    }
109
110    /// Get the value as a boolean
111    pub fn as_bool(&self) -> Option<bool> {
112        self.0.as_bool()
113    }
114
115    /// Get the value as an i64
116    pub fn as_i64(&self) -> Option<i64> {
117        self.0.as_i64()
118    }
119
120    /// Get the value as a u64
121    pub fn as_u64(&self) -> Option<u64> {
122        self.0.as_u64()
123    }
124
125    /// Get the value as an f64
126    pub fn as_f64(&self) -> Option<f64> {
127        self.0.as_f64()
128    }
129
130    /// Get the value as an array reference
131    pub fn as_array(&self) -> Option<&Vec<serde_json::Value>> {
132        self.0.as_array()
133    }
134
135    /// Get the value as an array reference (alias for as_array, for YAML compatibility)
136    pub fn as_sequence(&self) -> Option<&Vec<serde_json::Value>> {
137        self.0.as_array()
138    }
139
140    /// Get the value as an object reference
141    pub fn as_object(&self) -> Option<&serde_json::Map<String, serde_json::Value>> {
142        self.0.as_object()
143    }
144
145    /// Get the value as an object reference (alias for as_object, for YAML compatibility)
146    pub fn as_mapping(&self) -> Option<&serde_json::Map<String, serde_json::Value>> {
147        self.0.as_object()
148    }
149
150    /// Get a field from an object by key
151    pub fn get(&self, key: &str) -> Option<QuillValue> {
152        self.0.get(key).map(|v| QuillValue(v.clone()))
153    }
154}
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159
160    #[test]
161    fn test_from_toml() {
162        let toml_str = r#"
163            [package]
164            name = "test"
165            version = "1.0.0"
166        "#;
167        let toml_val: toml::Value = toml::from_str(toml_str).unwrap();
168        let quill_val = QuillValue::from_toml(&toml_val).unwrap();
169
170        assert!(quill_val.as_object().is_some());
171    }
172
173    #[test]
174    fn test_from_yaml_str() {
175        let yaml_str = r#"
176            title: Test Document
177            author: John Doe
178            count: 42
179        "#;
180        let quill_val = QuillValue::from_yaml_str(yaml_str).unwrap();
181
182        assert_eq!(
183            quill_val.get("title").as_ref().and_then(|v| v.as_str()),
184            Some("Test Document")
185        );
186        assert_eq!(
187            quill_val.get("author").as_ref().and_then(|v| v.as_str()),
188            Some("John Doe")
189        );
190        assert_eq!(
191            quill_val.get("count").as_ref().and_then(|v| v.as_i64()),
192            Some(42)
193        );
194    }
195
196    #[test]
197    fn test_to_minijinja() {
198        let json_val = serde_json::json!({
199            "title": "Test",
200            "count": 42,
201            "active": true,
202            "items": [1, 2, 3]
203        });
204        let quill_val = QuillValue::from_json(json_val);
205        let mj_val = quill_val.to_minijinja().unwrap();
206
207        // Verify it's convertible to MiniJinja value
208        assert!(mj_val.as_object().is_some());
209    }
210
211    #[test]
212    fn test_as_json() {
213        let json_val = serde_json::json!({"key": "value"});
214        let quill_val = QuillValue::from_json(json_val.clone());
215
216        assert_eq!(quill_val.as_json(), &json_val);
217    }
218
219    #[test]
220    fn test_into_json() {
221        let json_val = serde_json::json!({"key": "value"});
222        let quill_val = QuillValue::from_json(json_val.clone());
223
224        assert_eq!(quill_val.into_json(), json_val);
225    }
226
227    #[test]
228    fn test_delegating_methods() {
229        let quill_val = QuillValue::from_json(serde_json::json!({
230            "name": "test",
231            "count": 42,
232            "active": true,
233            "items": [1, 2, 3]
234        }));
235
236        assert_eq!(
237            quill_val.get("name").as_ref().and_then(|v| v.as_str()),
238            Some("test")
239        );
240        assert_eq!(
241            quill_val.get("count").as_ref().and_then(|v| v.as_i64()),
242            Some(42)
243        );
244        assert_eq!(
245            quill_val.get("active").as_ref().and_then(|v| v.as_bool()),
246            Some(true)
247        );
248        assert!(quill_val
249            .get("items")
250            .as_ref()
251            .and_then(|v| v.as_array())
252            .is_some());
253    }
254
255    #[test]
256    fn test_yaml_with_tags() {
257        // Note: serde_saphyr handles tags differently - this tests basic parsing
258        let yaml_str = r#"
259            value: 42
260        "#;
261        let quill_val = QuillValue::from_yaml_str(yaml_str).unwrap();
262
263        // Values should be converted to their underlying value
264        assert!(quill_val.as_object().is_some());
265    }
266
267    #[test]
268    fn test_null_value() {
269        let quill_val = QuillValue::from_json(serde_json::Value::Null);
270        assert!(quill_val.is_null());
271    }
272
273    #[test]
274    fn test_yaml_custom_tags_ignored() {
275        // User-defined YAML tags should be accepted and ignored
276        // The value should be parsed as if the tag were not present
277        let yaml_str = "memo_from: !fill 2d lt example";
278        let quill_val = QuillValue::from_yaml_str(yaml_str).unwrap();
279
280        // The tag !fill should be ignored, value parsed as string
281        assert_eq!(
282            quill_val.get("memo_from").as_ref().and_then(|v| v.as_str()),
283            Some("2d lt example")
284        );
285    }
286}