#![allow(clippy::items_after_test_module)]
#![allow(clippy::single_match)]
#![allow(clippy::single_char_pattern)]
#![allow(clippy::needless_borrow)]
#![allow(clippy::ptr_arg)]
extern crate minidom;
extern crate serde_json;
#[cfg(feature = "regex_path")]
extern crate regex;
use minidom::{Element, Error};
use serde_json::{Map, Number, Value};
#[cfg(feature = "json_types")]
use std::collections::HashMap;
use std::str::FromStr;
#[cfg(feature = "regex_path")]
use regex::Regex;
#[cfg(test)]
mod tests;
#[derive(Debug)]
pub enum NullValue {
Ignore,
Null,
EmptyObject,
EmptyString,
}
#[derive(Debug)]
pub enum JsonArray {
Always(JsonType),
Infer(JsonType),
}
#[derive(Debug)]
pub enum PathMatcher {
Absolute(String),
#[cfg(feature = "regex_path")]
Regex(Regex),
}
impl From<&str> for PathMatcher {
fn from(value: &str) -> Self {
let path_with_leading_slash = if value.starts_with("/") {
value.into()
} else {
["/", value].concat()
};
PathMatcher::Absolute(path_with_leading_slash)
}
}
#[cfg(feature = "regex_path")]
impl From<Regex> for PathMatcher {
fn from(value: Regex) -> Self {
PathMatcher::Regex(value)
}
}
#[derive(Debug, PartialEq, Clone)]
pub enum JsonType {
AlwaysString,
Bool(Vec<&'static str>),
Infer,
}
#[derive(Debug)]
pub struct Config {
pub leading_zero_as_string: bool,
pub xml_attr_prefix: String,
pub xml_text_node_prop_name: String,
pub empty_element_handling: NullValue,
#[cfg(feature = "json_types")]
pub json_type_overrides: HashMap<String, JsonArray>,
#[cfg(feature = "regex_path")]
pub json_regex_type_overrides: Vec<(Regex, JsonArray)>,
}
impl Config {
pub fn new_with_defaults() -> Self {
Config {
leading_zero_as_string: false,
xml_attr_prefix: "@".to_owned(),
xml_text_node_prop_name: "#text".to_owned(),
empty_element_handling: NullValue::EmptyObject,
#[cfg(feature = "json_types")]
json_type_overrides: HashMap::new(),
#[cfg(feature = "regex_path")]
json_regex_type_overrides: Vec::new(),
}
}
pub fn new_with_custom_values(
leading_zero_as_string: bool,
xml_attr_prefix: &str,
xml_text_node_prop_name: &str,
empty_element_handling: NullValue,
) -> Self {
Config {
leading_zero_as_string,
xml_attr_prefix: xml_attr_prefix.to_owned(),
xml_text_node_prop_name: xml_text_node_prop_name.to_owned(),
empty_element_handling,
#[cfg(feature = "json_types")]
json_type_overrides: HashMap::new(),
#[cfg(feature = "regex_path")]
json_regex_type_overrides: Vec::new(),
}
}
#[cfg(feature = "json_types")]
pub fn add_json_type_override<P>(self, path: P, json_type: JsonArray) -> Self
where
P: Into<PathMatcher>,
{
let mut conf = self;
match path.into() {
PathMatcher::Absolute(path) => {
conf.json_type_overrides.insert(path, json_type);
}
#[cfg(feature = "regex_path")]
PathMatcher::Regex(regex) => {
conf.json_regex_type_overrides.push((regex, json_type));
}
}
conf
}
}
impl Default for Config {
fn default() -> Self {
Config::new_with_defaults()
}
}
fn parse_text(text: &str, leading_zero_as_string: bool, json_type: &JsonType) -> Value {
let text = text.trim();
if json_type == &JsonType::AlwaysString {
return Value::String(text.into());
}
#[cfg(feature = "json_types")]
if let JsonType::Bool(true_values) = json_type {
if true_values.contains(&text) {
return Value::Bool(true);
} else {
return Value::Bool(false);
}
}
if let Ok(v) = text.parse::<u64>() {
if leading_zero_as_string && text.starts_with("0") && (v != 0 || text.len() > 1) {
return Value::String(text.into());
}
return Value::Number(Number::from(v));
}
if let Ok(v) = text.parse::<f64>() {
if text.starts_with("0") && !text.starts_with("0.") {
return Value::String(text.into());
}
if let Some(val) = Number::from_f64(v) {
return Value::Number(val);
}
}
if let Ok(v) = text.parse::<bool>() {
return Value::Bool(v);
}
Value::String(text.into())
}
fn convert_node(el: &Element, config: &Config, path: &String) -> Option<Value> {
#[cfg(feature = "json_types")]
let path = [path, "/", el.name()].concat();
let (_, json_type_value) = get_json_type(config, &path);
if el.text().trim() != "" {
if el.attrs().count() > 0 {
Some(Value::Object(
el.attrs()
.map(|(k, v)| {
#[cfg(feature = "json_types")]
let path = [path.clone(), "/@".to_owned(), k.to_owned()].concat();
#[cfg(feature = "json_types")]
let (_, json_type_value) = get_json_type(config, &path);
(
[config.xml_attr_prefix.clone(), k.to_owned()].concat(),
parse_text(&v, config.leading_zero_as_string, &json_type_value),
)
})
.chain(vec![(
config.xml_text_node_prop_name.clone(),
parse_text(
&el.text()[..],
config.leading_zero_as_string,
&json_type_value,
),
)])
.collect(),
))
} else {
Some(parse_text(
&el.text()[..],
config.leading_zero_as_string,
&json_type_value,
))
}
} else {
let mut data = Map::new();
for (k, v) in el.attrs() {
#[cfg(feature = "json_types")]
let path = [path.clone(), "/@".to_owned(), k.to_owned()].concat();
#[cfg(feature = "json_types")]
let (_, json_type_value) = get_json_type(config, &path);
data.insert(
[config.xml_attr_prefix.clone(), k.to_owned()].concat(),
parse_text(&v, config.leading_zero_as_string, &json_type_value),
);
}
for child in el.children() {
match convert_node(child, config, &path) {
Some(val) => {
let name = &child.name().to_string();
#[cfg(feature = "json_types")]
let path = [path.clone(), "/".to_owned(), name.clone()].concat();
let (json_type_array, _) = get_json_type(config, &path);
if json_type_array || data.contains_key(name) {
if data.get(name).unwrap_or(&Value::Null).is_array() {
data.get_mut(name)
.unwrap()
.as_array_mut()
.unwrap()
.push(val);
} else {
let new_val = match data.remove(name) {
None => vec![val],
Some(temp) => vec![temp, val],
};
data.insert(name.clone(), Value::Array(new_val));
}
} else {
data.insert(name.clone(), val);
}
}
_ => (),
}
}
if !data.is_empty() {
return Some(Value::Object(data));
}
match config.empty_element_handling {
NullValue::Null => Some(Value::Null),
NullValue::EmptyObject => Some(Value::Object(data)),
NullValue::EmptyString => Some(Value::String(String::new())),
NullValue::Ignore => None,
}
}
}
fn xml_to_map(e: &Element, config: &Config) -> Value {
let mut data = Map::new();
data.insert(
e.name().to_string(),
convert_node(&e, &config, &String::new()).unwrap_or(Value::Null),
);
Value::Object(data)
}
pub fn xml_str_to_json(xml: &str, config: &Config) -> Result<Value, Error> {
let root = Element::from_str(xml)?;
Ok(xml_to_map(&root, config))
}
pub fn xml_string_to_json(xml: String, config: &Config) -> Result<Value, Error> {
xml_str_to_json(xml.as_str(), config)
}
pub fn map_xml_to_json_per_child(
xml: String,
config: &Config,
) -> Result<Vec<(String, Value)>, Error> {
let root = Element::from_str(xml.as_str())?;
let iterator = root.children().map(|child| {
let mut xml = Vec::new();
child.write_to(&mut xml).expect("successfully write to vec");
let child_xml_str = String::from_utf8(xml).unwrap();
let child_json = xml_to_map(&child, config);
(child_xml_str, child_json)
});
Ok(iterator.collect())
}
#[cfg(feature = "json_types")]
#[inline]
fn get_json_type_with_absolute_path<'conf>(
config: &'conf Config,
path: &String,
) -> (bool, &'conf JsonType) {
match config
.json_type_overrides
.get(path)
.unwrap_or(&JsonArray::Infer(JsonType::Infer))
{
JsonArray::Infer(v) => (false, v),
JsonArray::Always(v) => (true, v),
}
}
#[cfg(feature = "json_types")]
#[cfg(not(feature = "regex_path"))]
#[inline]
fn get_json_type<'conf>(config: &'conf Config, path: &String) -> (bool, &'conf JsonType) {
get_json_type_with_absolute_path(config, path)
}
#[cfg(feature = "json_types")]
#[cfg(feature = "regex_path")]
#[inline]
fn get_json_type<'conf>(config: &'conf Config, path: &String) -> (bool, &'conf JsonType) {
for (regex, json_array) in &config.json_regex_type_overrides {
if regex.is_match(path) {
return match json_array {
JsonArray::Infer(v) => (false, v),
JsonArray::Always(v) => (true, v),
};
}
}
get_json_type_with_absolute_path(config, path)
}
#[cfg(not(feature = "json_types"))]
#[inline]
fn get_json_type<'conf>(_config: &'conf Config, _path: &String) -> (bool, &'conf JsonType) {
(false, &JsonType::Infer)
}