Skip to main content

dkit_core/
value.rs

1use std::fmt;
2
3use indexmap::IndexMap;
4use serde::{Deserialize, Serialize};
5
6/// Unified data model for all supported formats.
7///
8/// Every data format (JSON, CSV, YAML, TOML, XML, etc.) is converted to and
9/// from this common representation. The variants cover all primitive and
10/// composite data types needed for lossless round-trip conversion.
11///
12/// # Examples
13///
14/// ```
15/// use dkit_core::value::Value;
16///
17/// let v = Value::Integer(42);
18/// assert_eq!(v.as_i64(), Some(42));
19/// assert!(v.as_str().is_none());
20/// ```
21#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
22#[non_exhaustive]
23pub enum Value {
24    /// JSON `null` / missing value.
25    Null,
26    /// Boolean value.
27    Bool(bool),
28    /// 64-bit signed integer.
29    Integer(i64),
30    /// 64-bit floating-point number.
31    Float(f64),
32    /// UTF-8 string.
33    String(String),
34    /// Ordered sequence of values.
35    Array(Vec<Value>),
36    /// Ordered map of string keys to values (insertion order preserved).
37    Object(IndexMap<String, Value>),
38}
39
40impl fmt::Display for Value {
41    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42        match self {
43            Value::Null => write!(f, "null"),
44            Value::Bool(b) => write!(f, "{b}"),
45            Value::Integer(n) => write!(f, "{n}"),
46            Value::Float(n) => {
47                if n.fract() == 0.0 && n.is_finite() {
48                    write!(f, "{n:.1}")
49                } else {
50                    write!(f, "{n}")
51                }
52            }
53            Value::String(s) => write!(f, "{s}"),
54            Value::Array(arr) => {
55                write!(f, "[")?;
56                for (i, v) in arr.iter().enumerate() {
57                    if i > 0 {
58                        write!(f, ", ")?;
59                    }
60                    match v {
61                        Value::String(s) => write!(f, "\"{s}\"")?,
62                        _ => write!(f, "{v}")?,
63                    }
64                }
65                write!(f, "]")
66            }
67            Value::Object(map) => {
68                write!(f, "{{")?;
69                for (i, (k, v)) in map.iter().enumerate() {
70                    if i > 0 {
71                        write!(f, ", ")?;
72                    }
73                    match v {
74                        Value::String(s) => write!(f, "\"{k}\": \"{s}\"")?,
75                        _ => write!(f, "\"{k}\": {v}")?,
76                    }
77                }
78                write!(f, "}}")
79            }
80        }
81    }
82}
83
84#[allow(dead_code)]
85impl Value {
86    /// Returns the boolean value if this is a `Bool`, otherwise `None`.
87    pub fn as_bool(&self) -> Option<bool> {
88        match self {
89            Value::Bool(b) => Some(*b),
90            _ => None,
91        }
92    }
93
94    /// Returns the integer value if this is an `Integer`, otherwise `None`.
95    pub fn as_i64(&self) -> Option<i64> {
96        match self {
97            Value::Integer(n) => Some(*n),
98            _ => None,
99        }
100    }
101
102    /// Returns the value as `f64`. Works for both `Float` and `Integer` variants.
103    pub fn as_f64(&self) -> Option<f64> {
104        match self {
105            Value::Float(f) => Some(*f),
106            Value::Integer(n) => Some(*n as f64),
107            _ => None,
108        }
109    }
110
111    /// Returns a string slice if this is a `String`, otherwise `None`.
112    pub fn as_str(&self) -> Option<&str> {
113        match self {
114            Value::String(s) => Some(s),
115            _ => None,
116        }
117    }
118
119    /// Returns a reference to the inner `Vec` if this is an `Array`, otherwise `None`.
120    pub fn as_array(&self) -> Option<&Vec<Value>> {
121        match self {
122            Value::Array(a) => Some(a),
123            _ => None,
124        }
125    }
126
127    /// Returns a reference to the inner `IndexMap` if this is an `Object`, otherwise `None`.
128    pub fn as_object(&self) -> Option<&IndexMap<String, Value>> {
129        match self {
130            Value::Object(o) => Some(o),
131            _ => None,
132        }
133    }
134
135    /// Returns `true` if this value is `Null`.
136    pub fn is_null(&self) -> bool {
137        matches!(self, Value::Null)
138    }
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144
145    #[test]
146    fn test_value_accessors() {
147        assert_eq!(Value::Bool(true).as_bool(), Some(true));
148        assert_eq!(Value::Integer(42).as_i64(), Some(42));
149        assert_eq!(Value::Float(3.14).as_f64(), Some(3.14));
150        assert_eq!(Value::Integer(42).as_f64(), Some(42.0));
151        assert_eq!(Value::String("hello".into()).as_str(), Some("hello"));
152        assert!(Value::Null.is_null());
153        assert!(!Value::Bool(false).is_null());
154    }
155
156    #[test]
157    fn test_value_array() {
158        let arr = Value::Array(vec![Value::Integer(1), Value::Integer(2)]);
159        assert_eq!(arr.as_array().unwrap().len(), 2);
160    }
161
162    #[test]
163    fn test_value_object() {
164        let mut map = IndexMap::new();
165        map.insert("key".to_string(), Value::String("value".into()));
166        let obj = Value::Object(map);
167        assert_eq!(
168            obj.as_object().unwrap().get("key"),
169            Some(&Value::String("value".into()))
170        );
171    }
172
173    #[test]
174    fn test_display_primitives() {
175        assert_eq!(Value::Null.to_string(), "null");
176        assert_eq!(Value::Bool(true).to_string(), "true");
177        assert_eq!(Value::Bool(false).to_string(), "false");
178        assert_eq!(Value::Integer(42).to_string(), "42");
179        assert_eq!(Value::Float(3.14).to_string(), "3.14");
180        assert_eq!(Value::Float(1.0).to_string(), "1.0");
181        assert_eq!(Value::String("hello".into()).to_string(), "hello");
182    }
183
184    #[test]
185    fn test_display_array() {
186        let arr = Value::Array(vec![
187            Value::Integer(1),
188            Value::String("two".into()),
189            Value::Bool(true),
190        ]);
191        assert_eq!(arr.to_string(), r#"[1, "two", true]"#);
192    }
193
194    #[test]
195    fn test_display_empty_array() {
196        assert_eq!(Value::Array(vec![]).to_string(), "[]");
197    }
198
199    #[test]
200    fn test_display_object() {
201        let mut map = IndexMap::new();
202        map.insert("name".to_string(), Value::String("dkit".into()));
203        map.insert("version".to_string(), Value::Integer(1));
204        let obj = Value::Object(map);
205        assert_eq!(obj.to_string(), r#"{"name": "dkit", "version": 1}"#);
206    }
207
208    #[test]
209    fn test_display_empty_object() {
210        assert_eq!(Value::Object(IndexMap::new()).to_string(), "{}");
211    }
212
213    #[test]
214    fn test_accessor_returns_none_for_wrong_type() {
215        let v = Value::Integer(42);
216        assert_eq!(v.as_bool(), None);
217        assert_eq!(v.as_str(), None);
218        assert_eq!(v.as_array(), None);
219        assert_eq!(v.as_object(), None);
220    }
221}