#![forbid(unsafe_code)]
#![doc(html_root_url = "https://docs.rs/decamelize-keys/0.1.0")]
use decamelize::decamelize_with;
use serde_json::{Map, Value};
#[cfg(doctest)]
#[doc = include_str!("../README.md")]
struct ReadmeDoctests;
#[derive(Debug, Clone)]
pub struct Options {
separator: String,
deep: bool,
exclude: Vec<String>,
}
impl Default for Options {
fn default() -> Self {
Self {
separator: "_".to_string(),
deep: false,
exclude: Vec::new(),
}
}
}
impl Options {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn separator(mut self, separator: impl Into<String>) -> Self {
self.separator = separator.into();
self
}
#[must_use]
pub fn deep(mut self, value: bool) -> Self {
self.deep = value;
self
}
#[must_use]
pub fn exclude<I, S>(mut self, keys: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.exclude = keys.into_iter().map(Into::into).collect();
self
}
}
#[must_use]
pub fn decamelize_keys(value: &Value) -> Value {
decamelize_keys_with(value, &Options::new())
}
#[must_use]
pub fn decamelize_keys_with(value: &Value, options: &Options) -> Value {
transform(value, options)
}
fn is_recursable(value: &Value) -> bool {
value.is_object() || value.is_array()
}
fn transform(value: &Value, options: &Options) -> Value {
match value {
Value::Array(array) => Value::Array(
array
.iter()
.map(|item| {
if is_recursable(item) {
transform(item, options)
} else {
item.clone()
}
})
.collect(),
),
Value::Object(map) => {
let mut result = Map::new();
for (key, val) in map {
let new_value = if options.deep && is_recursable(val) {
transform(val, options)
} else {
val.clone()
};
let new_key = if options.exclude.iter().any(|excluded| excluded == key) {
key.clone()
} else {
decamelize_with(key, &options.separator, false)
};
result.insert(new_key, new_value);
}
Value::Object(result)
}
other => other.clone(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn shallow() {
assert_eq!(
decamelize_keys(&json!({ "fooBar": 1, "BazQux": 2 })),
json!({ "foo_bar": 1, "baz_qux": 2 })
);
assert_eq!(
decamelize_keys(&json!({ "fooBar": { "nestedKey": 1 } })),
json!({ "foo_bar": { "nestedKey": 1 } })
);
}
#[test]
fn deep_and_arrays() {
assert_eq!(
decamelize_keys_with(
&json!({ "fooBar": { "nestedKey": 1 } }),
&Options::new().deep(true)
),
json!({ "foo_bar": { "nested_key": 1 } })
);
assert_eq!(
decamelize_keys(&json!([{ "aB": 1 }, { "cD": 2 }])),
json!([{ "a_b": 1 }, { "c_d": 2 }])
);
assert_eq!(
decamelize_keys_with(
&json!({ "fooBar": [{ "xY": 1 }] }),
&Options::new().deep(true)
),
json!({ "foo_bar": [{ "x_y": 1 }] })
);
}
#[test]
fn separator_and_exclude() {
assert_eq!(
decamelize_keys_with(&json!({ "fooBar": 1 }), &Options::new().separator("-")),
json!({ "foo-bar": 1 })
);
assert_eq!(
decamelize_keys_with(
&json!({ "fooBar": 1, "XMLHttp": 2 }),
&Options::new().separator(" ")
),
json!({ "foo bar": 1, "xml http": 2 })
);
assert_eq!(
decamelize_keys_with(&json!({ "fooBar": 1 }), &Options::new().exclude(["fooBar"])),
json!({ "fooBar": 1 })
);
}
#[test]
fn numeric_keys_and_scalars() {
assert_eq!(
decamelize_keys(&json!({ "0": 1, "42": 2, "fooBar": 3 })),
json!({ "0": 1, "42": 2, "foo_bar": 3 })
);
assert_eq!(decamelize_keys(&json!("scalar")), json!("scalar"));
assert_eq!(decamelize_keys(&json!(42)), json!(42));
}
}