ctoolbox/utilities/
serde_value.rs

1//! Utilities for working with `serde_json::Value` objects, including insertion,
2//! extraction, and conversion operations.
3
4use anyhow::{Result, anyhow};
5use serde::Serialize;
6use serde_json::{Value, to_value};
7
8use crate::formats::base64::standard_base64_to_bytes;
9use crate::json;
10
11/// Inserts a key-value pair into a JSON object. If the input is an object, it adds
12/// the key directly. If not, it wraps the input in a new object under "original"
13/// and adds the new key. Returns the modified `Value`.
14pub fn insert_key<T: Serialize>(input: T, key: &str, value: Value) -> Value {
15    let mut val = to_value(input).expect("Could not serialize input");
16    if let Value::Object(map) = &mut val {
17        map.insert(key.to_string(), value);
18        val
19    } else {
20        // If it's not an object, wrap it in a new object with the new key
21        let mut map = serde_json::Map::new();
22        map.insert(key.to_string(), value);
23        map.insert("original".to_string(), val);
24        Value::Object(map)
25    }
26}
27
28/// Extracts bytes from a `Value`. Supports arrays of u64 (converted to u8) or
29/// base64-encoded strings. Returns `None` for unsupported types. Differs from
30/// `get_as_bytes` by operating directly on the `Value` rather than extracting
31/// from an object key.
32pub fn get_bytes(val: &Value) -> Option<Vec<u8>> {
33    let value_bytes: Vec<u8> = match val {
34        Value::Array(arr) => arr
35            .iter()
36            .map(|v| {
37                u8::try_from(v.as_u64().expect("Invalid u64 value"))
38                    .expect("Invalid u8 value")
39            })
40            .collect(),
41        Value::String(s) => {
42            standard_base64_to_bytes(s.to_string()).unwrap_or_default()
43        }
44        _ => return None,
45    };
46    Some(value_bytes)
47}
48
49/// Retrieves the value associated with a key from a JSON object. Returns `None`
50/// if the `Value` is not an object or the key doesn't exist. Differs from
51/// type-specific getters like `get_as_string` by returning the raw `Value`.
52pub fn get_key(val: &Value, key: &str) -> Option<Value> {
53    if let Value::Object(map) = val {
54        map.get(key).cloned()
55    } else {
56        None
57    }
58}
59
60/// Retrieve a key and convert it to a String. Returns `None` if the key
61/// doesn't exist or is not a string.
62pub fn get_string(val: &Value, key: &str) -> Option<String> {
63    let val = get_key(val, key)?;
64    val.as_str().map(std::string::ToString::to_string)
65}
66
67/// Retrieve a key and convert it to a JSON string. Returns `None` if the key
68/// doesn't exist.
69pub fn get_as_json_string(val: &Value, key: &str) -> Option<String> {
70    let val = get_key(val, key)?;
71    Some(json!(val))
72}
73
74/// Converts a `Value` to a plain string representation. Supports String, Number,
75/// Bool, and Null types. Returns an error for unsupported types like Array or
76/// Object. Differs from `to_html_form_string` by providing a generic string
77/// conversion without HTML-specific handling.
78pub fn to_plain_string(val: &Value) -> Result<String> {
79    match &val {
80        serde_json::Value::String(s) => Ok(s.clone()),
81        serde_json::Value::Number(n) => Ok(n.to_string()),
82        serde_json::Value::Bool(b) => Ok(b.to_string()),
83        serde_json::Value::Null => Ok(String::new()),
84        _ => Err(anyhow!("Unsupported value type for {val:?}")),
85    }
86}
87
88/// Converts a `Value` to a string suitable for HTML form fields. Handles Bool
89/// as "on" for true or None for false, and supports String, Number, and Null.
90/// Returns an error for unsupported types. Differs from `to_plain_string` by
91/// applying HTML form conventions (e.g., bool handling).
92pub fn to_html_form_string(val: &Value) -> Result<Option<String>> {
93    match &val {
94        serde_json::Value::String(s) => Ok(Some(s.clone())),
95        serde_json::Value::Number(n) => Ok(Some(n.to_string())),
96        serde_json::Value::Bool(b) => {
97            if *b {
98                Ok(Some("on".to_string()))
99            } else {
100                Ok(None)
101            }
102        }
103        serde_json::Value::Null => Ok(Some(String::new())),
104        _ => Err(anyhow!("Unsupported value type for {val:?}")),
105    }
106}
107
108/// Retrieves bytes from a JSON object under a specific key. Combines `get_key`
109/// and `get_bytes` to extract and convert the value. Returns `None` if the key
110/// doesn't exist or conversion fails.
111pub fn get_as_bytes(val: &Value, key: &str) -> Option<Vec<u8>> {
112    if let Value::Object(map) = val {
113        if let Some(v) = map.get(key) {
114            get_bytes(v)
115        } else {
116            None
117        }
118    } else {
119        None
120    }
121}
122
123/// Retrieves the u64 value associated with a key from a JSON object.
124/// Returns `None` if the `Value` is not an object, the key doesn't exist, or
125/// the value is not a u64.
126pub fn get_as_u64(val: &Value, key: &str) -> Option<u64> {
127    if let Value::Object(map) = val {
128        map.get(key).and_then(serde_json::Value::as_u64)
129    } else {
130        None
131    }
132}
133
134/// Parses a JSON string into a `serde_json::Value`. Returns an error if parsing
135/// fails. This is a convenience function for deserializing JSON strings into
136/// `Value` objects.
137pub fn from_json_string(s: &str) -> Result<Value> {
138    let v: Value = serde_json::from_str(s)?;
139    Ok(v)
140}
141
142/// Retrieves the value associated with a key from a JSON string representing
143/// an object. Combines `from_json_string` and `get_key` to parse and extract
144/// the value. Returns an error if parsing fails.
145pub fn get_key_from_json_string(s: &str, key: &str) -> Result<Option<Value>> {
146    let v: Value = from_json_string(s)?;
147    Ok(get_key(&v, key))
148}
149
150/// Retrieves the string value associated with a key from a JSON string
151/// representing an object. Combines `from_json_string` and `get_as_json_string`
152/// to parse and extract the string. Returns an error if parsing fails.
153pub fn get_as_json_string_from_json_string(
154    s: &str,
155    key: &str,
156) -> Result<Option<String>> {
157    let v: Value = from_json_string(s)?;
158    if let Some(val) = get_as_json_string(&v, key) {
159        Ok(Some(val.to_string()))
160    } else {
161        Ok(None)
162    }
163}
164
165#[cfg(test)]
166mod tests {
167    //! Tests for serde_values helpers.
168
169    use crate::formats::base64::bytes_to_standard_base64;
170    use crate::json_value;
171
172    use super::*;
173    use serde::{Deserialize, Serialize};
174
175    /// Simple struct for testing.
176    #[derive(Serialize, Deserialize, Debug, PartialEq)]
177    struct TestStruct {
178        id: u32,
179        name: String,
180    }
181
182    /// Test creating a value from a primitive and inserting a key.
183    #[crate::ctb_test]
184    fn test_insert_key_primitive() {
185        let val = 42u64;
186        let v = json_value!(val);
187        let v2 = insert_key(v, "extra", Value::String("hello".to_string()));
188        assert!(get_key(&v2, "extra").is_some());
189        assert_eq!(get_as_u64(&v2, "val"), None); // original key not present
190    }
191
192    /// Test creating a value from a struct and extracting fields.
193    #[crate::ctb_test]
194    fn test_struct_value_and_getters() {
195        let s = TestStruct {
196            id: 7,
197            name: "abc".to_string(),
198        };
199        let v = json_value!(s);
200        assert_eq!(get_as_u64(&v, "id"), Some(7));
201        assert_eq!(get_string(&v, "name"), Some("abc".to_string()));
202    }
203
204    /// Test inserting a key into a struct value.
205    #[crate::ctb_test]
206    fn test_insert_key_struct() {
207        let s = TestStruct {
208            id: 1,
209            name: "xyz".to_string(),
210        };
211        let v = json_value!(s);
212        let v2 = insert_key(v, "extra", Value::Bool(true));
213        assert_eq!(get_key(&v2, "extra"), Some(Value::Bool(true)));
214        assert_eq!(get_string(&v2, "name"), Some("xyz".to_string()));
215    }
216
217    /// Test extracting bytes from an array and a base64 string.
218    #[crate::ctb_test]
219    fn test_get_bytes_array_and_base64() {
220        let arr = vec![1u8, 2, 3, 4];
221        let v = json_value!(arr);
222        let bytes = get_bytes(&v).unwrap();
223        assert_eq!(bytes, vec![1, 2, 3, 4]);
224
225        let base64_str = bytes_to_standard_base64(&[5u8, 6, 7]);
226        let v2 = Value::String(base64_str.clone());
227        let bytes2 = get_bytes(&v2).unwrap();
228        assert_eq!(bytes2, vec![5, 6, 7]);
229    }
230
231    /// Test `get_as_bytes` from an object.
232    #[crate::ctb_test]
233    fn test_get_as_bytes_from_object() {
234        let mut obj = serde_json::Map::new();
235        obj.insert("data".to_string(), json_value!(vec![9u8, 8]));
236        let v = Value::Object(obj);
237        let bytes = get_as_bytes(&v, "data").unwrap();
238        assert_eq!(bytes, vec![9, 8]);
239    }
240
241    /// Test `get_key` directly.
242    #[crate::ctb_test]
243    fn test_get_key() {
244        let mut obj = serde_json::Map::new();
245        obj.insert("key1".to_string(), Value::String("value".to_string()));
246        let v = Value::Object(obj);
247        assert_eq!(
248            get_key(&v, "key1"),
249            Some(Value::String("value".to_string()))
250        );
251        assert_eq!(get_key(&v, "missing"), None);
252        assert_eq!(get_key(&Value::Null, "key1"), None);
253    }
254
255    /// Test `to_plain_string`.
256    #[crate::ctb_test]
257    fn test_to_plain_string() {
258        assert_eq!(
259            to_plain_string(&Value::String("test".to_string())).unwrap(),
260            "test"
261        );
262        assert_eq!(to_plain_string(&Value::Number(42.into())).unwrap(), "42");
263        assert_eq!(to_plain_string(&Value::Bool(true)).unwrap(), "true");
264        assert_eq!(to_plain_string(&Value::Null).unwrap(), "");
265        assert!(to_plain_string(&Value::Array(vec![])).is_err());
266    }
267
268    /// Test `to_html_form_string`.
269    #[crate::ctb_test]
270    fn test_to_html_form_string() {
271        assert_eq!(
272            to_html_form_string(&Value::String("test".to_string())).unwrap(),
273            Some("test".to_string())
274        );
275        assert_eq!(
276            to_html_form_string(&Value::Number(42.into())).unwrap(),
277            Some("42".to_string())
278        );
279        assert_eq!(
280            to_html_form_string(&Value::Bool(true)).unwrap(),
281            Some("on".to_string())
282        );
283        assert_eq!(to_html_form_string(&Value::Bool(false)).unwrap(), None);
284        assert_eq!(
285            to_html_form_string(&Value::Null).unwrap(),
286            Some("".to_string())
287        );
288        assert!(to_html_form_string(&Value::Array(vec![])).is_err());
289    }
290
291    /// Test `get_as_u64`.
292    #[crate::ctb_test]
293    fn test_get_as_u64() {
294        let mut obj = serde_json::Map::new();
295        obj.insert("num".to_string(), Value::Number(123.into()));
296        let v = Value::Object(obj);
297        assert_eq!(get_as_u64(&v, "num"), Some(123));
298        assert_eq!(get_as_u64(&v, "missing"), None);
299        assert_eq!(get_as_u64(&Value::Null, "num"), None);
300    }
301
302    #[crate::ctb_test]
303    fn test_from_json_string() {
304        let json = r#"{"num": 123}"#;
305        let v = from_json_string(json).unwrap();
306        assert_eq!(get_as_u64(&v, "num"), Some(123));
307        assert_eq!(get_as_u64(&v, "missing"), None);
308        assert_eq!(get_as_u64(&Value::Null, "num"), None);
309    }
310}