1#![forbid(unsafe_code)]
30#![doc(html_root_url = "https://docs.rs/decamelize-keys/0.1.0")]
31
32use decamelize::decamelize_with;
33use serde_json::{Map, Value};
34
35#[cfg(doctest)]
37#[doc = include_str!("../README.md")]
38struct ReadmeDoctests;
39
40#[derive(Debug, Clone)]
42pub struct Options {
43 separator: String,
44 deep: bool,
45 exclude: Vec<String>,
46}
47
48impl Default for Options {
49 fn default() -> Self {
50 Self {
51 separator: "_".to_string(),
52 deep: false,
53 exclude: Vec::new(),
54 }
55 }
56}
57
58impl Options {
59 #[must_use]
61 pub fn new() -> Self {
62 Self::default()
63 }
64
65 #[must_use]
67 pub fn separator(mut self, separator: impl Into<String>) -> Self {
68 self.separator = separator.into();
69 self
70 }
71
72 #[must_use]
74 pub fn deep(mut self, value: bool) -> Self {
75 self.deep = value;
76 self
77 }
78
79 #[must_use]
81 pub fn exclude<I, S>(mut self, keys: I) -> Self
82 where
83 I: IntoIterator<Item = S>,
84 S: Into<String>,
85 {
86 self.exclude = keys.into_iter().map(Into::into).collect();
87 self
88 }
89}
90
91#[must_use]
99pub fn decamelize_keys(value: &Value) -> Value {
100 decamelize_keys_with(value, &Options::new())
101}
102
103#[must_use]
105pub fn decamelize_keys_with(value: &Value, options: &Options) -> Value {
106 transform(value, options)
107}
108
109fn is_recursable(value: &Value) -> bool {
111 value.is_object() || value.is_array()
112}
113
114fn transform(value: &Value, options: &Options) -> Value {
115 match value {
116 Value::Array(array) => Value::Array(
117 array
118 .iter()
119 .map(|item| {
120 if is_recursable(item) {
121 transform(item, options)
122 } else {
123 item.clone()
124 }
125 })
126 .collect(),
127 ),
128 Value::Object(map) => {
129 let mut result = Map::new();
130 for (key, val) in map {
131 let new_value = if options.deep && is_recursable(val) {
132 transform(val, options)
133 } else {
134 val.clone()
135 };
136 let new_key = if options.exclude.iter().any(|excluded| excluded == key) {
137 key.clone()
138 } else {
139 decamelize_with(key, &options.separator, false)
140 };
141 result.insert(new_key, new_value);
142 }
143 Value::Object(result)
144 }
145 other => other.clone(),
146 }
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152 use serde_json::json;
153
154 #[test]
155 fn shallow() {
156 assert_eq!(
157 decamelize_keys(&json!({ "fooBar": 1, "BazQux": 2 })),
158 json!({ "foo_bar": 1, "baz_qux": 2 })
159 );
160 assert_eq!(
162 decamelize_keys(&json!({ "fooBar": { "nestedKey": 1 } })),
163 json!({ "foo_bar": { "nestedKey": 1 } })
164 );
165 }
166
167 #[test]
168 fn deep_and_arrays() {
169 assert_eq!(
170 decamelize_keys_with(
171 &json!({ "fooBar": { "nestedKey": 1 } }),
172 &Options::new().deep(true)
173 ),
174 json!({ "foo_bar": { "nested_key": 1 } })
175 );
176 assert_eq!(
177 decamelize_keys(&json!([{ "aB": 1 }, { "cD": 2 }])),
178 json!([{ "a_b": 1 }, { "c_d": 2 }])
179 );
180 assert_eq!(
181 decamelize_keys_with(
182 &json!({ "fooBar": [{ "xY": 1 }] }),
183 &Options::new().deep(true)
184 ),
185 json!({ "foo_bar": [{ "x_y": 1 }] })
186 );
187 }
188
189 #[test]
190 fn separator_and_exclude() {
191 assert_eq!(
192 decamelize_keys_with(&json!({ "fooBar": 1 }), &Options::new().separator("-")),
193 json!({ "foo-bar": 1 })
194 );
195 assert_eq!(
196 decamelize_keys_with(
197 &json!({ "fooBar": 1, "XMLHttp": 2 }),
198 &Options::new().separator(" ")
199 ),
200 json!({ "foo bar": 1, "xml http": 2 })
201 );
202 assert_eq!(
203 decamelize_keys_with(&json!({ "fooBar": 1 }), &Options::new().exclude(["fooBar"])),
204 json!({ "fooBar": 1 })
205 );
206 }
207
208 #[test]
209 fn numeric_keys_and_scalars() {
210 assert_eq!(
211 decamelize_keys(&json!({ "0": 1, "42": 2, "fooBar": 3 })),
212 json!({ "0": 1, "42": 2, "foo_bar": 3 })
213 );
214 assert_eq!(decamelize_keys(&json!("scalar")), json!("scalar"));
215 assert_eq!(decamelize_keys(&json!(42)), json!(42));
216 }
217}