Skip to main content

neco_json/
value.rs

1use alloc::string::String;
2use alloc::vec::Vec;
3
4use crate::error::AccessError;
5
6#[derive(Debug, Clone, PartialEq)]
7pub enum JsonValue {
8    Null,
9    Bool(bool),
10    Number(f64),
11    String(String),
12    Array(Vec<JsonValue>),
13    Object(Vec<(String, JsonValue)>),
14}
15
16impl JsonValue {
17    pub fn is_null(&self) -> bool {
18        matches!(self, Self::Null)
19    }
20
21    pub fn is_bool(&self) -> bool {
22        matches!(self, Self::Bool(_))
23    }
24
25    pub fn is_number(&self) -> bool {
26        matches!(self, Self::Number(_))
27    }
28
29    pub fn is_string(&self) -> bool {
30        matches!(self, Self::String(_))
31    }
32
33    pub fn is_array(&self) -> bool {
34        matches!(self, Self::Array(_))
35    }
36
37    pub fn is_object(&self) -> bool {
38        matches!(self, Self::Object(_))
39    }
40
41    pub fn as_bool(&self) -> Option<bool> {
42        match self {
43            Self::Bool(value) => Some(*value),
44            _ => None,
45        }
46    }
47
48    pub fn as_f64(&self) -> Option<f64> {
49        match self {
50            Self::Number(value) => Some(*value),
51            _ => None,
52        }
53    }
54
55    pub fn as_i64(&self) -> Option<i64> {
56        match self {
57            Self::Number(value) => {
58                let n = *value;
59                // Reject if out of range or has a fractional part.
60                if n >= i64::MIN as f64 && n <= i64::MAX as f64 && (n as i64) as f64 == n {
61                    Some(n as i64)
62                } else {
63                    None
64                }
65            }
66            _ => None,
67        }
68    }
69
70    pub fn as_u64(&self) -> Option<u64> {
71        match self {
72            Self::Number(value) => {
73                let n = *value;
74                if n >= 0.0 && n <= u64::MAX as f64 && (n as u64) as f64 == n {
75                    Some(n as u64)
76                } else {
77                    None
78                }
79            }
80            _ => None,
81        }
82    }
83
84    pub fn as_str(&self) -> Option<&str> {
85        match self {
86            Self::String(value) => Some(value.as_str()),
87            _ => None,
88        }
89    }
90
91    pub fn as_array(&self) -> Option<&[JsonValue]> {
92        match self {
93            Self::Array(values) => Some(values.as_slice()),
94            _ => None,
95        }
96    }
97
98    pub fn as_object(&self) -> Option<&[(String, JsonValue)]> {
99        match self {
100            Self::Object(fields) => Some(fields.as_slice()),
101            _ => None,
102        }
103    }
104
105    pub fn get(&self, key: &str) -> Option<&JsonValue> {
106        match self {
107            Self::Object(fields) => fields
108                .iter()
109                .find_map(|(field, value)| (field == key).then_some(value)),
110            _ => None,
111        }
112    }
113
114    pub fn required_str(&self, key: &str) -> Result<&str, AccessError> {
115        self.required_value(key, JsonValue::as_str, "string")
116    }
117
118    pub fn required_bool(&self, key: &str) -> Result<bool, AccessError> {
119        self.required_value(key, JsonValue::as_bool, "bool")
120    }
121
122    pub fn required_f64(&self, key: &str) -> Result<f64, AccessError> {
123        self.required_value(key, JsonValue::as_f64, "number")
124    }
125
126    pub fn required_array(&self, key: &str) -> Result<&[JsonValue], AccessError> {
127        self.required_value(key, JsonValue::as_array, "array")
128    }
129
130    pub fn required_object(&self, key: &str) -> Result<&[(String, JsonValue)], AccessError> {
131        self.required_value(key, JsonValue::as_object, "object")
132    }
133
134    pub fn optional_str(&self, key: &str) -> Result<Option<&str>, AccessError> {
135        self.optional_value(key, JsonValue::as_str, "string")
136    }
137
138    pub fn optional_bool(&self, key: &str) -> Result<Option<bool>, AccessError> {
139        self.optional_value(key, JsonValue::as_bool, "bool")
140    }
141
142    pub fn optional_f64(&self, key: &str) -> Result<Option<f64>, AccessError> {
143        self.optional_value(key, JsonValue::as_f64, "number")
144    }
145
146    pub fn optional_array(&self, key: &str) -> Result<Option<&[JsonValue]>, AccessError> {
147        self.optional_value(key, JsonValue::as_array, "array")
148    }
149
150    fn required_value<'a, T>(
151        &'a self,
152        key: &str,
153        accessor: impl FnOnce(&'a JsonValue) -> Option<T>,
154        expected: &'static str,
155    ) -> Result<T, AccessError> {
156        let value = self.object_field(key)?;
157        accessor(value).ok_or_else(|| AccessError::TypeMismatch {
158            field: key.into(),
159            expected,
160        })
161    }
162
163    fn optional_value<'a, T>(
164        &'a self,
165        key: &str,
166        accessor: impl FnOnce(&'a JsonValue) -> Option<T>,
167        expected: &'static str,
168    ) -> Result<Option<T>, AccessError> {
169        let Some(value) = self.object_field_optional(key)? else {
170            return Ok(None);
171        };
172
173        if value.is_null() {
174            return Ok(None);
175        }
176
177        accessor(value)
178            .map(Some)
179            .ok_or_else(|| AccessError::TypeMismatch {
180                field: key.into(),
181                expected,
182            })
183    }
184
185    fn object_field(&self, key: &str) -> Result<&JsonValue, AccessError> {
186        self.object_field_optional(key)?
187            .ok_or_else(|| AccessError::MissingField(key.into()))
188    }
189
190    fn object_field_optional(&self, key: &str) -> Result<Option<&JsonValue>, AccessError> {
191        match self {
192            Self::Object(_) => Ok(self.get(key)),
193            _ => Err(AccessError::NotAnObject),
194        }
195    }
196}
197
198#[cfg(test)]
199mod tests {
200    use alloc::string::String;
201    use alloc::vec;
202
203    use super::JsonValue;
204    use crate::AccessError;
205
206    fn sample_object() -> JsonValue {
207        JsonValue::Object(vec![
208            ("name".into(), JsonValue::String("neco".into())),
209            ("enabled".into(), JsonValue::Bool(true)),
210            ("score".into(), JsonValue::Number(42.5)),
211            (
212                "items".into(),
213                JsonValue::Array(vec![JsonValue::Bool(false), JsonValue::Null]),
214            ),
215            (
216                "meta".into(),
217                JsonValue::Object(vec![("nested".into(), JsonValue::String("ok".into()))]),
218            ),
219            ("maybe".into(), JsonValue::Null),
220            ("dup".into(), JsonValue::String("first".into())),
221            ("dup".into(), JsonValue::String("second".into())),
222        ])
223    }
224
225    #[test]
226    fn is_methods_match_variants() {
227        assert!(JsonValue::Null.is_null());
228        assert!(JsonValue::Bool(true).is_bool());
229        assert!(JsonValue::Number(1.0).is_number());
230        assert!(JsonValue::String("x".into()).is_string());
231        assert!(JsonValue::Array(vec![]).is_array());
232        assert!(JsonValue::Object(vec![]).is_object());
233    }
234
235    #[test]
236    fn as_methods_return_expected_values() {
237        let value = JsonValue::Bool(true);
238        assert_eq!(value.as_bool(), Some(true));
239        assert_eq!(value.as_f64(), None);
240
241        let value = JsonValue::Number(3.25);
242        assert_eq!(value.as_f64(), Some(3.25));
243        assert_eq!(value.as_str(), None);
244
245        let value = JsonValue::String("cat".into());
246        assert_eq!(value.as_str(), Some("cat"));
247        assert_eq!(value.as_array(), None);
248
249        let array = vec![JsonValue::Null];
250        let value = JsonValue::Array(array.clone());
251        assert_eq!(value.as_array(), Some(array.as_slice()));
252        assert_eq!(value.as_object(), None);
253
254        let object = vec![("k".into(), JsonValue::Bool(false))];
255        let value = JsonValue::Object(object.clone());
256        assert_eq!(value.as_object(), Some(object.as_slice()));
257        assert_eq!(value.as_bool(), None);
258    }
259
260    #[test]
261    fn get_reads_object_field_and_keeps_first_duplicate() {
262        let object = sample_object();
263
264        assert_eq!(object.get("name"), Some(&JsonValue::String("neco".into())));
265        assert_eq!(object.get("dup"), Some(&JsonValue::String("first".into())));
266        assert_eq!(object.get("missing"), None);
267        assert_eq!(JsonValue::Bool(true).get("name"), None);
268    }
269
270    #[test]
271    fn required_accessors_cover_success_and_errors() {
272        let object = sample_object();
273
274        assert_eq!(object.required_str("name"), Ok("neco"));
275        assert_eq!(object.required_bool("enabled"), Ok(true));
276        assert_eq!(object.required_f64("score"), Ok(42.5));
277        assert_eq!(
278            object.required_array("items"),
279            Ok([JsonValue::Bool(false), JsonValue::Null].as_slice())
280        );
281        assert_eq!(
282            object.required_object("meta"),
283            Ok([("nested".into(), JsonValue::String("ok".into()))].as_slice())
284        );
285
286        assert_eq!(
287            object.required_str("missing"),
288            Err(AccessError::MissingField(String::from("missing")))
289        );
290        assert_eq!(
291            object.required_bool("name"),
292            Err(AccessError::TypeMismatch {
293                field: String::from("name"),
294                expected: "bool",
295            })
296        );
297        assert_eq!(
298            JsonValue::Null.required_str("name"),
299            Err(AccessError::NotAnObject)
300        );
301    }
302
303    #[test]
304    fn optional_accessors_cover_success_none_and_errors() {
305        let object = sample_object();
306
307        assert_eq!(object.optional_str("name"), Ok(Some("neco")));
308        assert_eq!(object.optional_bool("enabled"), Ok(Some(true)));
309        assert_eq!(object.optional_f64("score"), Ok(Some(42.5)));
310        assert_eq!(
311            object.optional_array("items"),
312            Ok(Some([JsonValue::Bool(false), JsonValue::Null].as_slice()))
313        );
314
315        assert_eq!(object.optional_str("missing"), Ok(None));
316        assert_eq!(object.optional_str("maybe"), Ok(None));
317        assert_eq!(
318            object.optional_bool("name"),
319            Err(AccessError::TypeMismatch {
320                field: String::from("name"),
321                expected: "bool",
322            })
323        );
324        assert_eq!(
325            JsonValue::Null.optional_str("name"),
326            Err(AccessError::NotAnObject)
327        );
328    }
329}