rustrails_support/
json_ext.rs1use serde::{Serialize, de::DeserializeOwned};
2use serde_json::Value;
3
4#[derive(Debug, thiserror::Error)]
6pub enum JsonExtError {
7 #[error("json parse error: {0}")]
9 Parse(#[from] serde_json::Error),
10}
11
12pub trait ToJson {
14 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
27pub fn from_json<T>(json: &str) -> Result<T, JsonExtError>
29where
30 T: DeserializeOwned,
31{
32 Ok(serde_json::from_str(json)?)
33}
34
35#[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#[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}