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