nimbus/
json.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5use serde_json::{Map, Value};
6use std::collections::HashMap;
7
8#[cfg(feature = "stateful")]
9pub type JsonObject = Map<String, Value>;
10
11#[cfg(feature = "stateful")]
12pub type PrefValue = Value;
13
14/// Replace any instance of [from] with [to] in any string within the [serde_json::Value].
15///
16/// This recursively descends into the object, looking at string values and keys.
17#[allow(dead_code)]
18pub(crate) fn replace_str(value: &mut Value, from: &str, to: &str) {
19    let replacer = create_str_replacer(from, to);
20    replace_str_with(value, &replacer);
21}
22
23/// Replace any instance of [from] with [to] in any string within the [serde_json::Value::Map].
24///
25/// This recursively descends into the object, looking at string values and keys.
26pub(crate) fn replace_str_in_map(map: &mut Map<String, Value>, from: &str, to: &str) {
27    let replacer = create_str_replacer(from, to);
28    replace_str_in_map_with(map, &replacer);
29}
30
31fn replace_str_with<F>(value: &mut Value, replacer: &F)
32where
33    F: Fn(&str) -> Option<String> + ?Sized,
34{
35    match value {
36        Value::String(s) => {
37            if let Some(r) = replacer(s) {
38                *s = r;
39            }
40        }
41
42        Value::Array(list) => {
43            for item in list.iter_mut() {
44                replace_str_with(item, replacer);
45            }
46        }
47
48        Value::Object(map) => {
49            replace_str_in_map_with(map, replacer);
50        }
51
52        _ => (),
53    };
54}
55
56pub(crate) fn replace_str_in_map_with<F>(map: &mut Map<String, Value>, replacer: &F)
57where
58    F: Fn(&str) -> Option<String> + ?Sized,
59{
60    // Replace values in place.
61    for v in map.values_mut() {
62        replace_str_with(v, replacer);
63    }
64
65    // Replacing keys in place is a little trickier.
66    let mut changes = HashMap::new();
67    for k in map.keys() {
68        if let Some(new) = replacer(k) {
69            changes.insert(k.to_owned(), new);
70        }
71    }
72
73    for (k, new) in changes {
74        let v = map.remove(&k).unwrap();
75        _ = map.insert(new, v);
76    }
77}
78
79fn create_str_replacer<'a>(from: &'a str, to: &'a str) -> impl Fn(&str) -> Option<String> + 'a {
80    move |s: &str| -> Option<String> {
81        if s.contains(from) {
82            Some(s.replace(from, to))
83        } else {
84            None
85        }
86    }
87}
88
89#[cfg(test)]
90mod unit_tests {
91    use super::*;
92    use serde_json::json;
93
94    #[test]
95    fn test_replace_str() {
96        let mut value = json!("{test}");
97        replace_str(&mut value, "{test}", "success");
98        assert_eq!(value, json!("success"));
99
100        let mut value = json!("{test}-postfix");
101        replace_str(&mut value, "{test}", "success");
102        assert_eq!(value, json!("success-postfix"));
103
104        let mut value = json!("prefix-{test}");
105        replace_str(&mut value, "{test}", "success");
106        assert_eq!(value, json!("prefix-success"));
107
108        let mut value = json!("prefix-{test}-postfix");
109        replace_str(&mut value, "{test}", "success");
110        assert_eq!(value, json!("prefix-success-postfix"));
111
112        let mut value = json!("prefix-{test}-multi-{test}-postfix");
113        replace_str(&mut value, "{test}", "success");
114        assert_eq!(value, json!("prefix-success-multi-success-postfix"));
115    }
116
117    #[test]
118    fn test_replace_str_in_array() {
119        let mut value = json!(["alice", "bob", "{placeholder}", "daphne"]);
120        replace_str(&mut value, "{placeholder}", "charlie");
121        assert_eq!(value, json!(["alice", "bob", "charlie", "daphne"]));
122    }
123
124    #[test]
125    fn test_replace_str_in_map() {
126        let mut value = json!({
127            "key": "{test}",
128            "not": true,
129            "or": 2,
130        });
131        replace_str(&mut value, "{test}", "success");
132        assert_eq!(
133            value,
134            json!({
135                "key": "success",
136                "not": true,
137                "or": 2,
138            })
139        );
140    }
141
142    #[test]
143    fn test_replace_str_in_map_keys() {
144        let mut value = json!({
145            "{test}-en-US": "{test}",
146            "not": true,
147            "or": 2,
148        });
149        replace_str(&mut value, "{test}", "success");
150        assert_eq!(
151            value,
152            json!({
153                "success-en-US": "success",
154                "not": true,
155                "or": 2,
156            })
157        );
158    }
159
160    #[test]
161    fn test_replace_str_mixed() {
162        let mut value = json!({
163            "messages": {
164                "{test}-en-US": {
165                    "test": "{test}"
166                },
167                "{test}{test}": {
168                    "test": "{test}{test}"
169                }
170            }
171        });
172        replace_str(&mut value, "{test}", "success");
173        assert_eq!(
174            value,
175            json!({
176                "messages": {
177                    "success-en-US": {
178                        "test": "success"
179                    },
180                    "successsuccess": {
181                        "test": "successsuccess"
182                    }
183                }
184            })
185        );
186    }
187}