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
41impl Deref for QuillValue {
42    type Target = serde_json::Value;
43
44    fn deref(&self) -> &Self::Target {
45        &self.0
46    }
47}
48
49// Implement common delegating methods for convenience
50impl QuillValue {
51    /// Check if the value is null
52    pub fn is_null(&self) -> bool {
53        self.0.is_null()
54    }
55
56    /// Get the value as a string reference
57    pub fn as_str(&self) -> Option<&str> {
58        self.0.as_str()
59    }
60
61    /// Get the value as a boolean
62    pub fn as_bool(&self) -> Option<bool> {
63        self.0.as_bool()
64    }
65
66    /// Get the value as an i64
67    pub fn as_i64(&self) -> Option<i64> {
68        self.0.as_i64()
69    }
70
71    /// Get the value as a u64
72    pub fn as_u64(&self) -> Option<u64> {
73        self.0.as_u64()
74    }
75
76    /// Get the value as an f64
77    pub fn as_f64(&self) -> Option<f64> {
78        self.0.as_f64()
79    }
80
81    /// Get the value as an array reference
82    pub fn as_array(&self) -> Option<&Vec<serde_json::Value>> {
83        self.0.as_array()
84    }
85
86    /// Get the value as an array reference (alias for as_array, for YAML compatibility)
87    pub fn as_sequence(&self) -> Option<&Vec<serde_json::Value>> {
88        self.0.as_array()
89    }
90
91    /// Get the value as an object reference
92    pub fn as_object(&self) -> Option<&serde_json::Map<String, serde_json::Value>> {
93        self.0.as_object()
94    }
95
96    /// Get a field from an object by key
97    pub fn get(&self, key: &str) -> Option<QuillValue> {
98        self.0.get(key).map(|v| QuillValue(v.clone()))
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105
106    #[test]
107    fn test_from_yaml_value() {
108        let yaml_str = r#"
109            package:
110              name: test
111              version: 1.0.0
112        "#;
113        let json_val: serde_json::Value = serde_saphyr::from_str(yaml_str).unwrap();
114        let quill_val = QuillValue::from_json(json_val);
115
116        assert!(quill_val.as_object().is_some());
117        assert_eq!(
118            quill_val
119                .get("package")
120                .unwrap()
121                .get("name")
122                .unwrap()
123                .as_str(),
124            Some("test")
125        );
126    }
127
128    #[test]
129    fn test_from_yaml_str() {
130        let yaml_str = r#"
131            title: Test Document
132            author: John Doe
133            count: 42
134        "#;
135        let quill_val = QuillValue::from_yaml_str(yaml_str).unwrap();
136
137        assert_eq!(
138            quill_val.get("title").as_ref().and_then(|v| v.as_str()),
139            Some("Test Document")
140        );
141        assert_eq!(
142            quill_val.get("author").as_ref().and_then(|v| v.as_str()),
143            Some("John Doe")
144        );
145        assert_eq!(
146            quill_val.get("count").as_ref().and_then(|v| v.as_i64()),
147            Some(42)
148        );
149    }
150
151    #[test]
152    fn test_as_json() {
153        let json_val = serde_json::json!({"key": "value"});
154        let quill_val = QuillValue::from_json(json_val.clone());
155
156        assert_eq!(quill_val.as_json(), &json_val);
157    }
158
159    #[test]
160    fn test_into_json() {
161        let json_val = serde_json::json!({"key": "value"});
162        let quill_val = QuillValue::from_json(json_val.clone());
163
164        assert_eq!(quill_val.into_json(), json_val);
165    }
166
167    #[test]
168    fn test_delegating_methods() {
169        let quill_val = QuillValue::from_json(serde_json::json!({
170            "name": "test",
171            "count": 42,
172            "active": true,
173            "items": [1, 2, 3]
174        }));
175
176        assert_eq!(
177            quill_val.get("name").as_ref().and_then(|v| v.as_str()),
178            Some("test")
179        );
180        assert_eq!(
181            quill_val.get("count").as_ref().and_then(|v| v.as_i64()),
182            Some(42)
183        );
184        assert_eq!(
185            quill_val.get("active").as_ref().and_then(|v| v.as_bool()),
186            Some(true)
187        );
188        assert!(quill_val
189            .get("items")
190            .as_ref()
191            .and_then(|v| v.as_array())
192            .is_some());
193    }
194
195    #[test]
196    fn test_yaml_with_tags() {
197        // Note: serde_saphyr handles tags differently - this tests basic parsing
198        let yaml_str = r#"
199            value: 42
200        "#;
201        let quill_val = QuillValue::from_yaml_str(yaml_str).unwrap();
202
203        // Values should be converted to their underlying value
204        assert!(quill_val.as_object().is_some());
205    }
206
207    #[test]
208    fn test_null_value() {
209        let quill_val = QuillValue::from_json(serde_json::Value::Null);
210        assert!(quill_val.is_null());
211    }
212
213    #[test]
214    fn test_yaml_custom_tags_ignored() {
215        // User-defined YAML tags should be accepted and ignored
216        // The value should be parsed as if the tag were not present
217        let yaml_str = "memo_from: !fill 2d lt example";
218        let quill_val = QuillValue::from_yaml_str(yaml_str).unwrap();
219
220        // The tag !fill should be ignored, value parsed as string
221        assert_eq!(
222            quill_val.get("memo_from").as_ref().and_then(|v| v.as_str()),
223            Some("2d lt example")
224        );
225    }
226}