Skip to main content

rustrails_support/
json_ext.rs

1use serde::{Serialize, de::DeserializeOwned};
2use serde_json::Value;
3
4/// Errors returned while decoding JSON.
5#[derive(Debug, thiserror::Error)]
6pub enum JsonExtError {
7    /// The JSON payload could not be parsed or deserialized.
8    #[error("json parse error: {0}")]
9    Parse(#[from] serde_json::Error),
10}
11
12/// Converts values into `serde_json::Value`.
13pub trait ToJson {
14    /// Converts `self` into JSON.
15    fn to_json(&self) -> Value;
16}
17
18impl<T> ToJson for T
19where
20    T: Serialize,
21{
22    fn to_json(&self) -> Value {
23        serde_json::to_value(self).unwrap_or(Value::Null)
24    }
25}
26
27/// Deserializes a JSON string into `T`.
28pub fn from_json<T>(json: &str) -> Result<T, JsonExtError>
29where
30    T: DeserializeOwned,
31{
32    Ok(serde_json::from_str(json)?)
33}
34
35/// Renders a JSON value with pretty indentation.
36#[must_use]
37pub fn json_pretty(value: &Value) -> String {
38    serde_json::to_string_pretty(value).unwrap_or_else(|_| String::from("null"))
39}
40
41/// Deep-merges two JSON values, preferring values from `b`.
42#[must_use]
43pub fn json_merge(a: &Value, b: &Value) -> Value {
44    match (a, b) {
45        (Value::Object(a), Value::Object(b)) => {
46            let mut merged = a.clone();
47            for (key, value) in b {
48                let next = merged
49                    .get(key)
50                    .map(|existing| json_merge(existing, value))
51                    .unwrap_or_else(|| value.clone());
52                merged.insert(key.clone(), next);
53            }
54            Value::Object(merged)
55        }
56        (_, b) => b.clone(),
57    }
58}
59
60#[cfg(test)]
61mod tests {
62    use super::{ToJson, from_json, json_merge, json_pretty};
63    use serde::{Deserialize, Serialize};
64    use serde_json::json;
65
66    #[derive(Debug, Deserialize, PartialEq, Eq, Serialize)]
67    struct User {
68        id: u32,
69        name: String,
70    }
71
72    #[test]
73    fn to_json_converts_serializable_values() {
74        let user = User {
75            id: 1,
76            name: String::from("Rails"),
77        };
78
79        assert_eq!(user.to_json(), json!({"id": 1, "name": "Rails"}));
80    }
81
82    #[test]
83    fn from_json_decodes_typed_values() {
84        let user: User =
85            from_json("{\"id\":1,\"name\":\"Rails\"}").expect("json should deserialize");
86
87        assert_eq!(
88            user,
89            User {
90                id: 1,
91                name: String::from("Rails")
92            }
93        );
94    }
95
96    #[test]
97    fn from_json_returns_a_typed_error_for_invalid_json() {
98        let error = from_json::<User>("{").expect_err("invalid json should fail");
99        assert!(error.to_string().starts_with("json parse error:"));
100    }
101
102    #[test]
103    fn json_pretty_formats_values_with_indentation() {
104        let pretty = json_pretty(&json!({"id": 1, "name": "Rails"}));
105        assert!(pretty.contains('\n'));
106        assert!(pretty.contains("  \"id\": 1"));
107    }
108
109    #[test]
110    fn json_merge_prefers_values_from_the_right_hand_side() {
111        let merged = json_merge(&json!({"name": "Rails"}), &json!({"name": "RustRails"}));
112
113        assert_eq!(merged, json!({"name": "RustRails"}));
114    }
115
116    #[test]
117    fn json_merge_recursively_merges_objects() {
118        let merged = json_merge(
119            &json!({"db": {"host": "localhost", "pool": 5}}),
120            &json!({"db": {"pool": 10, "port": 5432}}),
121        );
122
123        assert_eq!(
124            merged,
125            json!({"db": {"host": "localhost", "pool": 10, "port": 5432}})
126        );
127    }
128
129    #[test]
130    fn json_merge_replaces_non_object_values() {
131        let merged = json_merge(&json!("left"), &json!([1, 2, 3]));
132
133        assert_eq!(merged, json!([1, 2, 3]));
134    }
135}