1#![forbid(unsafe_code)]
33#![doc(html_root_url = "https://docs.rs/map-obj/0.1.0")]
34
35use serde_json::{Map, Value};
36
37#[cfg(doctest)]
39#[doc = include_str!("../README.md")]
40struct ReadmeDoctests;
41
42#[derive(Debug, Clone, PartialEq)]
44pub enum MapEntry {
45 Skip,
47 Keep {
51 key: String,
53 value: Value,
55 recurse: bool,
57 },
58}
59
60impl MapEntry {
61 #[must_use]
63 pub fn keep(key: impl Into<String>, value: Value) -> Self {
64 Self::Keep {
65 key: key.into(),
66 value,
67 recurse: true,
68 }
69 }
70
71 #[must_use]
73 pub fn keep_without_recursing(key: impl Into<String>, value: Value) -> Self {
74 Self::Keep {
75 key: key.into(),
76 value,
77 recurse: false,
78 }
79 }
80}
81
82fn is_container(value: &Value) -> bool {
84 value.is_object() || value.is_array()
85}
86
87pub fn map_obj<F>(object: &Value, deep: bool, mut mapper: F) -> Value
96where
97 F: FnMut(&str, &Value) -> MapEntry,
98{
99 match object {
100 Value::Object(map) => Value::Object(map_object(map, deep, &mut mapper)),
101 other => other.clone(),
102 }
103}
104
105fn map_object<F>(map: &Map<String, Value>, deep: bool, mapper: &mut F) -> Map<String, Value>
106where
107 F: FnMut(&str, &Value) -> MapEntry,
108{
109 let mut target = Map::new();
110 for (key, value) in map {
111 let MapEntry::Keep {
112 key: new_key,
113 value: new_value,
114 recurse,
115 } = mapper(key, value)
116 else {
117 continue;
118 };
119
120 if new_key == "__proto__" {
122 continue;
123 }
124
125 let new_value = if deep && recurse && is_container(&new_value) {
126 map_container(&new_value, deep, mapper)
127 } else {
128 new_value
129 };
130
131 target.insert(new_key, new_value);
132 }
133 target
134}
135
136fn map_array<F>(array: &[Value], deep: bool, mapper: &mut F) -> Vec<Value>
138where
139 F: FnMut(&str, &Value) -> MapEntry,
140{
141 array
142 .iter()
143 .map(|item| {
144 if is_container(item) {
145 map_container(item, deep, mapper)
146 } else {
147 item.clone()
148 }
149 })
150 .collect()
151}
152
153fn map_container<F>(value: &Value, deep: bool, mapper: &mut F) -> Value
154where
155 F: FnMut(&str, &Value) -> MapEntry,
156{
157 match value {
158 Value::Object(map) => Value::Object(map_object(map, deep, mapper)),
159 Value::Array(array) => Value::Array(map_array(array, deep, mapper)),
160 other => other.clone(),
161 }
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167 use serde_json::json;
168
169 fn append_underscore(value: &Value, deep: bool) -> Value {
170 map_obj(value, deep, |key, value| {
171 MapEntry::keep(format!("{key}_"), value.clone())
172 })
173 }
174
175 #[test]
176 fn shallow_keys() {
177 assert_eq!(
178 append_underscore(&json!({ "a": 1, "b": 2 }), false),
179 json!({ "a_": 1, "b_": 2 })
180 );
181 assert_eq!(
183 append_underscore(&json!({ "a": { "b": 1 } }), false),
184 json!({ "a_": { "b": 1 } })
185 );
186 }
187
188 #[test]
189 fn deep_and_arrays() {
190 assert_eq!(
191 append_underscore(&json!({ "a": { "b": 1 } }), true),
192 json!({ "a_": { "b_": 1 } })
193 );
194 assert_eq!(
195 append_underscore(&json!({ "a": [{ "b": 1 }] }), true),
196 json!({ "a_": [{ "b_": 1 }] })
197 );
198 }
199
200 #[test]
201 fn skip() {
202 let result = map_obj(&json!({ "a": 1, "_x": 2 }), false, |key, value| {
203 if key.starts_with('_') {
204 MapEntry::Skip
205 } else {
206 MapEntry::keep(key, value.clone())
207 }
208 });
209 assert_eq!(result, json!({ "a": 1 }));
210 }
211
212 #[test]
213 fn should_not_recurse() {
214 let result = map_obj(
215 &json!({ "a": { "b": 1 }, "stop": { "c": 2 } }),
216 true,
217 |key, value| {
218 if key == "stop" {
219 MapEntry::keep_without_recursing(key, value.clone())
220 } else {
221 MapEntry::keep(key, value.clone())
222 }
223 },
224 );
225 assert_eq!(result, json!({ "a": { "b": 1 }, "stop": { "c": 2 } }));
226 }
228
229 #[test]
230 fn drops_proto() {
231 let result = map_obj(&json!({ "evil": 1, "ok": 2 }), false, |key, value| {
232 if key == "evil" {
233 MapEntry::keep("__proto__", value.clone())
234 } else {
235 MapEntry::keep(key, value.clone())
236 }
237 });
238 assert_eq!(result, json!({ "ok": 2 }));
239 }
240
241 #[test]
242 fn transform_values() {
243 let result = map_obj(&json!({ "a": 1, "b": 2 }), false, |key, value| {
244 let n = value.as_i64().unwrap_or(0) * 10;
245 MapEntry::keep(key, json!(n))
246 });
247 assert_eq!(result, json!({ "a": 10, "b": 20 }));
248 }
249
250 #[test]
251 fn non_object_passthrough() {
252 assert_eq!(
253 map_obj(&json!(42), true, |k, v| MapEntry::keep(k, v.clone())),
254 json!(42)
255 );
256 }
257}