1use serde_json::{Map, Value};
4use std::collections::HashMap;
5
6pub fn to_camel_case(s: &str) -> String {
9 let mut out = String::with_capacity(s.len());
10 let mut capitalize_next = false;
11 for c in s.chars() {
12 if c == '_' {
13 capitalize_next = true;
14 } else if capitalize_next {
15 out.extend(c.to_uppercase());
16 capitalize_next = false;
17 } else {
18 out.push(c);
19 }
20 }
21 out
22}
23
24pub fn to_snake_case(s: &str) -> String {
27 let mut out = String::with_capacity(s.len() + 4);
28 for (i, c) in s.chars().enumerate() {
29 if c.is_uppercase() {
30 if i > 0 {
31 out.push('_');
32 }
33 out.extend(c.to_lowercase());
34 } else {
35 out.push(c);
36 }
37 }
38 out
39}
40
41pub fn object_keys_to_camel_case(obj: &mut Map<String, Value>) {
44 let keys: Vec<String> = obj.keys().cloned().collect();
45 for k in keys {
46 let camel = to_camel_case(&k);
47 if camel != k {
48 if let Some(v) = obj.remove(&k) {
49 obj.insert(camel, v);
50 }
51 }
52 }
53}
54
55pub fn object_keys_to_snake_case(obj: &mut Map<String, Value>) {
58 let keys: Vec<String> = obj.keys().cloned().collect();
59 for k in keys {
60 let snake = to_snake_case(&k);
61 if snake != k {
62 if let Some(v) = obj.remove(&k) {
63 obj.insert(snake, v);
64 }
65 }
66 }
67}
68
69pub fn value_keys_to_camel_case(value: &mut Value) {
71 if let Value::Object(ref mut map) = value {
72 object_keys_to_camel_case(map);
73 }
74}
75
76pub fn value_keys_to_camel_case_recursive(value: &mut Value) {
78 match value {
79 Value::Object(map) => {
80 object_keys_to_camel_case(map);
81 for (_, v) in map.iter_mut() {
82 value_keys_to_camel_case_recursive(v);
83 }
84 }
85 Value::Array(arr) => {
86 for v in arr.iter_mut() {
87 value_keys_to_camel_case_recursive(v);
88 }
89 }
90 _ => {}
91 }
92}
93
94pub fn value_keys_to_snake_case(value: &mut Value) {
96 if let Value::Object(ref mut map) = value {
97 object_keys_to_snake_case(map);
98 }
99}
100
101pub fn hashmap_keys_to_snake_case(map: &HashMap<String, Value>) -> HashMap<String, Value> {
103 map.iter()
104 .map(|(k, v)| (to_snake_case(k), v.clone()))
105 .collect()
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111 use serde_json::json;
112
113 #[test]
116 fn camel_single_underscore() {
117 assert_eq!(to_camel_case("user_id"), "userId");
118 }
119
120 #[test]
121 fn camel_multiple_underscores() {
122 assert_eq!(to_camel_case("created_at_time"), "createdAtTime");
123 }
124
125 #[test]
126 fn camel_no_underscore() {
127 assert_eq!(to_camel_case("name"), "name");
128 }
129
130 #[test]
131 fn camel_leading_underscore() {
132 assert_eq!(to_camel_case("_private"), "Private");
133 }
134
135 #[test]
136 fn camel_empty() {
137 assert_eq!(to_camel_case(""), "");
138 }
139
140 #[test]
141 fn camel_trailing_underscore() {
142 assert_eq!(to_camel_case("foo_"), "foo");
144 }
145
146 #[test]
149 fn snake_simple() {
150 assert_eq!(to_snake_case("userId"), "user_id");
151 }
152
153 #[test]
154 fn snake_multiple_caps() {
155 assert_eq!(to_snake_case("createdAt"), "created_at");
156 }
157
158 #[test]
159 fn snake_already_snake() {
160 assert_eq!(to_snake_case("user_id"), "user_id");
161 }
162
163 #[test]
164 fn snake_leading_cap() {
165 assert_eq!(to_snake_case("UserName"), "user_name");
167 }
168
169 #[test]
170 fn snake_empty() {
171 assert_eq!(to_snake_case(""), "");
172 }
173
174 #[test]
177 fn round_trip_snake_to_camel_to_snake() {
178 let original = "order_line_item";
179 assert_eq!(to_snake_case(&to_camel_case(original)), original);
180 }
181
182 #[test]
185 fn object_keys_camel_converts_keys() {
186 let mut map = json!({"user_id": 1, "created_at": "2024"})
187 .as_object()
188 .unwrap()
189 .clone();
190 object_keys_to_camel_case(&mut map);
191 assert!(map.contains_key("userId"));
192 assert!(map.contains_key("createdAt"));
193 assert!(!map.contains_key("user_id"));
194 }
195
196 #[test]
197 fn object_keys_snake_converts_keys() {
198 let mut map = json!({"userId": 1, "createdAt": "2024"})
199 .as_object()
200 .unwrap()
201 .clone();
202 object_keys_to_snake_case(&mut map);
203 assert!(map.contains_key("user_id"));
204 assert!(map.contains_key("created_at"));
205 assert!(!map.contains_key("userId"));
206 }
207
208 #[test]
211 fn recursive_camel_nested_object() {
212 let mut v = json!({
213 "user_id": 1,
214 "address": { "zip_code": "12345" }
215 });
216 value_keys_to_camel_case_recursive(&mut v);
217 assert!(v.get("userId").is_some());
218 assert!(v.get("address").unwrap().get("zipCode").is_some());
219 }
220
221 #[test]
222 fn recursive_camel_array_of_objects() {
223 let mut v = json!([
224 { "first_name": "Alice" },
225 { "first_name": "Bob" }
226 ]);
227 value_keys_to_camel_case_recursive(&mut v);
228 let arr = v.as_array().unwrap();
229 assert!(arr[0].get("firstName").is_some());
230 assert!(arr[1].get("firstName").is_some());
231 }
232
233 #[test]
234 fn recursive_camel_scalar_is_noop() {
235 let mut v = json!(42);
236 value_keys_to_camel_case_recursive(&mut v);
237 assert_eq!(v, json!(42));
238 }
239
240 #[test]
243 fn hashmap_snake_keys() {
244 let map: HashMap<String, Value> = [
245 ("firstName".to_string(), json!("Alice")),
246 ("lastName".to_string(), json!("Smith")),
247 ]
248 .into_iter()
249 .collect();
250 let result = hashmap_keys_to_snake_case(&map);
251 assert!(result.contains_key("first_name"));
252 assert!(result.contains_key("last_name"));
253 assert_eq!(result["first_name"], json!("Alice"));
254 }
255}