diff --git a/Cargo.toml b/Cargo.toml index 5d22416..8fbe0a7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "quickxml_to_serde" -version = "0.4.2" +version = "0.4.3" authors = ["Alec Troemel ", "Max Voskob "] description = "Convert between XML JSON using quickxml and serde" repository = "https://github.com/AlecTroemel/quickxml_to_serde" diff --git a/README.md b/README.md index 4501454..895ac09 100644 --- a/README.md +++ b/README.md @@ -70,14 +70,14 @@ Sample XML document: ``` Configuration to make attribute `attr1="007"` always come out as a JSON string: ```rust -let conf = Config::new_with_defaults().add_json_type_override("/a/@attr1", JsonType::AlwaysString); +let conf = Config::new_with_defaults().add_json_type_override("/a/@attr1", JsonArray::Infer(JsonType::AlwaysString)); ``` Configuration to make both attributes and the text node of `` always come out as a JSON string: ```rust let conf = Config::new_with_defaults() - .add_json_type_override("/a/@attr1", JsonType::AlwaysString) - .add_json_type_override("/a/b/@attr1", JsonType::AlwaysString) - .add_json_type_override("/a/b", JsonType::AlwaysString); + .add_json_type_override("/a/@attr1", JsonArray::Infer(JsonType::AlwaysString)) + .add_json_type_override("/a/b/@attr1", JsonArray::Infer(JsonType::AlwaysString)) + .add_json_type_override("/a/b", JsonArray::Infer(JsonType::AlwaysString)); ``` #### Boolean @@ -86,10 +86,69 @@ The only two [valid boolean values in JSON](https://json-schema.org/understandin ```rust let conf = Config::new_with_defaults() - .add_json_type_override("/a/b", JsonType::Bool(vec!["True","true","1","yes"])); + .add_json_type_override("/a/b", JsonArray::Infer(JsonType::Bool(vec!["True","true","1","yes"]))); ``` -See embedded docs for `Config` struct and its members for more details. +#### Arrays + +Multiple nodes with the same name are automatically converted into a JSON array. For example, +```xml + + 1 + 2 + +``` +is converted into +```json +{ "a": + { "b": [1,2] } +} +``` +By default, a single element like +```xml + + 1 + +``` +is converted into a scalar value or a map +```json +{ "a": + { "b": 1 } +} +``` + +You can use `add_json_type_override()` with `JsonArray::Always()` to create a JSON array regardless of the number of elements so that `1` becomes `{ "a": { "b": [1] } }`. + +`JsonArray::Always()` and `JsonArray::Infer()` can specify what underlying JSON type should be used, e.g. +* `JsonArray::Infer(JsonType::AlwaysString)` - infer array, convert the values to JSON string +* `JsonArray::Always(JsonType::Infer)` - always wrap the values in a JSON array, infer the value types +* `JsonArray::Always(JsonType::AlwaysString)` - always wrap the values in a JSON array and convert values to JSON string + +```rust +let config = Config::new_with_defaults() + .add_json_type_override("/a/b", JsonArray::Always(JsonType::AlwaysString)); +``` + +Conversion of empty XML nodes like `` depends on `NullValue` setting. For example, +```rust +let config = Config::new_with_custom_values(false, "@", "#text", NullValue::Ignore) + .add_json_type_override("/a/b", JsonArray::Always(JsonType::Infer)); +``` +converts `` to +```json +{"a": null} +``` +and the same `config` with `NullValue::Null` converts it to + +```json +{"a": { "b": [null] }} +``` + +It is not possible to get an empty array like `{"a": { "b": [] }}`. + +---- + +*See embedded docs for `Config` struct and its members for more details.* ## Conversion specifics @@ -169,8 +228,8 @@ is converted into #### Additional info and examples -See `mod tests` inside [lib.rs](src/lib.rs) for more usage examples. +See [tests.rs](src/tests.rs) for more usage examples. ## Edge cases -XML and JSON are not directly compatible for 1:1 conversion without additional hints to the converter. Feel free to post an issue if you come across any incorrect conversion. +XML and JSON are not directly compatible for 1:1 conversion without additional hints to the converter. Please, post an issue if you come across any incorrect conversion. diff --git a/examples/json_types.rs b/examples/json_types.rs index 3d48cdc..3277ac0 100644 --- a/examples/json_types.rs +++ b/examples/json_types.rs @@ -1,6 +1,6 @@ extern crate quickxml_to_serde; #[cfg(feature = "json_types")] -use quickxml_to_serde::{xml_string_to_json, Config, JsonType}; +use quickxml_to_serde::{xml_string_to_json, Config, JsonArray, JsonType}; #[cfg(feature = "json_types")] fn main() { @@ -8,8 +8,8 @@ fn main() { // custom config values for 1 attribute and a text node let conf = Config::new_with_defaults() - .add_json_type_override("/a/b/@attr1", JsonType::AlwaysString) - .add_json_type_override("/a/b", JsonType::AlwaysString); + .add_json_type_override("/a/b/@attr1", JsonArray::Infer(JsonType::AlwaysString)) + .add_json_type_override("/a/b", JsonArray::Infer(JsonType::AlwaysString)); let json = xml_string_to_json(String::from(xml), &conf); println!("{}", json.expect("Malformed XML").to_string()); } diff --git a/src/lib.rs b/src/lib.rs index e0ab212..8c85780 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -35,10 +35,11 @@ //! for some XML nodes using xPath-like notations. Example for enforcing attribute `attr2` from the snippet above //! as JSON String regardless of its contents: //! ``` -//! use quickxml_to_serde::{Config, JsonType}; +//! use quickxml_to_serde::{Config, JsonArray, JsonType}; //! //! #[cfg(feature = "json_types")] -//! let conf = Config::new_with_defaults().add_json_type_override("/a/b/c/@attr2", JsonType::AlwaysString); +//! let conf = Config::new_with_defaults() +//! .add_json_type_override("/a/b/c/@attr2", JsonArray::Infer(JsonType::AlwaysString)); //! ``` //! //! ## Detailed documentation @@ -73,12 +74,27 @@ pub enum NullValue { EmptyObject, } +/// Defines how the values of this Node should be converted into a JSON array with the underlying types. +/// * `Infer` - the nodes are converted into a JSON array only if there are multiple identical elements. +/// E.g. `1` becomes a map `{"a": {"b": 1 }}` and `123` becomes +/// an array `{"a": {"b": [1, 2, 3] }}` +/// * `Always` - the nodes are converted into a JSON array regardless of how many there are. +/// E.g. `1` becomes an array with a single value `{"a": {"b": [1] }}` and +/// `123` also becomes an array `{"a": {"b": [1, 2, 3] }}` +#[derive(Debug)] +pub enum JsonArray { + /// Convert the nodes into a JSON array even if there is only one element + Always(JsonType), + /// Convert the nodes into a JSON array only if there are multiple identical elements + Infer(JsonType), +} + /// Defines which data type to apply in JSON format for consistency of output. /// E.g., the range of XML values for the same node type may be `1234`, `001234`, `AB1234`. /// It is impossible to guess with 100% consistency which data type to apply without seeing /// the entire range of values. Use this enum to tell the converter which data type should /// be applied. -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Clone)] pub enum JsonType { /// Do not try to infer the type and convert the value to JSON string. /// E.g. convert `1234` into `{"a":"1234"}` or `true` into `{"a":"true"}` @@ -123,7 +139,7 @@ pub struct Config { /// - path for `c`: `/a/b/@c` /// - path for `b` text node (007): `/a/b` #[cfg(feature = "json_types")] - pub json_type_overrides: HashMap, + pub json_type_overrides: HashMap, } impl Config { @@ -165,7 +181,7 @@ impl Config { /// - path for `b` text node (007): `/a/b` /// This function will add the leading `/` if it's missing. #[cfg(feature = "json_types")] - pub fn add_json_type_override(self, path: &str, json_type: JsonType) -> Self { + pub fn add_json_type_override(self, path: &str, json_type: JsonArray) -> Self { let mut conf = self; let path = if path.starts_with("/") { path.to_owned() @@ -238,18 +254,13 @@ fn convert_node(el: &Element, config: &Config, path: &String) -> Option { // add the current node to the path #[cfg(feature = "json_types")] let path = [path, "/", el.name()].concat(); + // get the json_type for this node - #[cfg(feature = "json_types")] - let json_type = config - .json_type_overrides - .get(&path) - .unwrap_or(&JsonType::Infer); - #[cfg(not(feature = "json_types"))] - let json_type = &JsonType::Infer; + let (_, json_type_value) = get_json_type(config, &path); // is it an element with text? if el.text().trim() != "" { - // does it have attributes? + // process node's attributes, if present if el.attrs().count() > 0 { Some(Value::Object( el.attrs() @@ -259,18 +270,19 @@ fn convert_node(el: &Element, config: &Config, path: &String) -> Option { let path = [path.clone(), "/@".to_owned(), k.to_owned()].concat(); // get the json_type for this node #[cfg(feature = "json_types")] - let json_type = config - .json_type_overrides - .get(&path) - .unwrap_or(&JsonType::Infer); + 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), + 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), + parse_text( + &el.text()[..], + config.leading_zero_as_string, + &json_type_value, + ), )]) .collect(), )) @@ -278,7 +290,7 @@ fn convert_node(el: &Element, config: &Config, path: &String) -> Option { Some(parse_text( &el.text()[..], config.leading_zero_as_string, - json_type, + &json_type_value, )) } } else { @@ -291,13 +303,10 @@ fn convert_node(el: &Element, config: &Config, path: &String) -> Option { let path = [path.clone(), "/@".to_owned(), k.to_owned()].concat(); // get the json_type for this node #[cfg(feature = "json_types")] - let json_type = config - .json_type_overrides - .get(&path) - .unwrap_or(&JsonType::Infer); + 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), + parse_text(&v, config.leading_zero_as_string, &json_type_value), ); } @@ -307,18 +316,30 @@ fn convert_node(el: &Element, config: &Config, path: &String) -> Option { Some(val) => { let name = &child.name().to_string(); - if data.contains_key(name) { + #[cfg(feature = "json_types")] + let path = [path.clone(), "/".to_owned(), name.clone()].concat(); + let (json_type_array, _) = get_json_type(config, &path); + // does it have to be an array? + if json_type_array || data.contains_key(name) { + // was this property converted to an array earlier? if data.get(name).unwrap_or(&Value::Null).is_array() { + // add the new value to an existing array data.get_mut(name) .unwrap() .as_array_mut() .unwrap() .push(val); } else { - let temp = data.remove(name).unwrap(); - data.insert(name.clone(), Value::Array(vec![temp, val])); + // convert the property to an array with the existing and the new values + let new_val = match data.remove(name) { + None => vec![val], + Some(temp) => vec![temp, val], + }; + data.insert(name.clone(), Value::Array(new_val)); } } else { + // this is the first time this property is encountered and it doesn't + // have to be an array, so add it as-is data.insert(name.clone(), val); } } @@ -354,3 +375,26 @@ pub fn xml_string_to_json(xml: String, config: &Config) -> Result let root = Element::from_str(xml.as_str())?; Ok(xml_to_map(&root, config)) } + +/// Returns a tuple for Array and Value enforcements for the current node or +/// `(false, JsonArray::Infer(JsonType::Infer)` if the current path is not found +/// in the list of paths with custom config. +#[cfg(feature = "json_types")] +#[inline] +fn get_json_type(config: &Config, path: &String) -> (bool, JsonType) { + match config + .json_type_overrides + .get(path) + .unwrap_or(&JsonArray::Infer(JsonType::Infer)) + { + JsonArray::Infer(v) => (false, v.clone()), + JsonArray::Always(v) => (true, v.clone()), + } +} + +/// Always returns `(false, JsonArray::Infer(JsonType::Infer)` if `json_types` feature is not enabled. +#[cfg(not(feature = "json_types"))] +#[inline] +fn get_json_type(_config: &Config, _path: &String) -> (bool, JsonType) { + (false, JsonType::Infer) +} diff --git a/src/tests.rs b/src/tests.rs index 52a4a52..1bea2db 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -89,13 +89,13 @@ fn test_mixed_nodes() { #[test] fn test_add_json_type_override() { // check if it adds the leading slash - let config = - Config::new_with_defaults().add_json_type_override("a/@attr1", JsonType::AlwaysString); + let config = Config::new_with_defaults() + .add_json_type_override("a/@attr1", JsonArray::Infer(JsonType::AlwaysString)); assert!(config.json_type_overrides.get("/a/@attr1").is_some()); // check if it doesn't add any extra slashes - let config = - Config::new_with_defaults().add_json_type_override("/a/@attr1", JsonType::AlwaysString); + let config = Config::new_with_defaults() + .add_json_type_override("/a/@attr1", JsonArray::Infer(JsonType::AlwaysString)); assert!(config.json_type_overrides.get("/a/@attr1").is_some()); } @@ -130,8 +130,8 @@ fn test_json_type_overrides() { } } }); - let conf = - Config::new_with_defaults().add_json_type_override("/a/@attr1", JsonType::AlwaysString); + let conf = Config::new_with_defaults() + .add_json_type_override("/a/@attr1", JsonArray::Infer(JsonType::AlwaysString)); let result = xml_string_to_json(String::from(xml), &conf); assert_eq!(expected, result.unwrap()); @@ -147,9 +147,12 @@ fn test_json_type_overrides() { } }); let conf = Config::new_with_defaults() - .add_json_type_override("/a/@attr1", JsonType::AlwaysString) - .add_json_type_override("/a/b/@attr1", JsonType::AlwaysString) - .add_json_type_override("/a/b/@attr2", JsonType::Bool(vec!["True"])); + .add_json_type_override("/a/@attr1", JsonArray::Infer(JsonType::AlwaysString)) + .add_json_type_override("/a/b/@attr1", JsonArray::Infer(JsonType::AlwaysString)) + .add_json_type_override( + "/a/b/@attr2", + JsonArray::Infer(JsonType::Bool(vec!["True"])), + ); let result = xml_string_to_json(String::from(xml), &conf); assert_eq!(expected, result.unwrap()); @@ -165,13 +168,113 @@ fn test_json_type_overrides() { } }); let conf = Config::new_with_defaults() - .add_json_type_override("/a/@attr1", JsonType::AlwaysString) - .add_json_type_override("/a/b/@attr1", JsonType::AlwaysString) - .add_json_type_override("/a/b", JsonType::AlwaysString); + .add_json_type_override("/a/@attr1", JsonArray::Infer(JsonType::AlwaysString)) + .add_json_type_override("/a/b/@attr1", JsonArray::Infer(JsonType::AlwaysString)) + .add_json_type_override("/a/b", JsonArray::Infer(JsonType::AlwaysString)); let result = xml_string_to_json(String::from(xml), &conf); assert_eq!(expected, result.unwrap()); } +#[cfg(feature = "json_types")] +#[test] +fn test_enforce_array() { + // test an array with default config values + let xml = r#"12"#; + let expected = json!({ + "a": { + "@attr1":"att1", + "b": [{ "@c":"att", "#text":1 }, { "@c":"att", "#text":2 }] + } + }); + let config = Config::new_with_defaults(); + let result = xml_string_to_json(String::from(xml), &config); + assert_eq!(expected, result.unwrap()); + + // test a non-array with default config values + let xml = r#"1"#; + let expected = json!({ + "a": { + "@attr1":"att1", + "b": { "@c":"att", "#text":1 } + } + }); + let result = xml_string_to_json(String::from(xml), &config); + assert_eq!(expected, result.unwrap()); + + // test a non-array with array enforcement (as object) + let xml = r#"1"#; + let expected = json!({ + "a": { + "@attr1":"att1", + "b": [{ "@c":"att", "#text":1 }] + } + }); + let config = Config::new_with_defaults() + .add_json_type_override("/a/b", JsonArray::Always(JsonType::Infer)); + let result = xml_string_to_json(String::from(xml), &config); + assert_eq!(expected, result.unwrap()); + + // test a non-array with array enforcement (as value) + let xml = r#"1"#; + let expected = json!({ + "a": { + "b": [1] + } + }); + let config = Config::new_with_defaults() + .add_json_type_override("/a/b", JsonArray::Always(JsonType::Infer)); + let result = xml_string_to_json(String::from(xml), &config); + assert_eq!(expected, result.unwrap()); + + // test an array with array enforcement (as value) + let xml = r#"12"#; + let expected = json!({ + "a": { + "b": [1,2] + } + }); + let config = Config::new_with_defaults() + .add_json_type_override("/a/b", JsonArray::Always(JsonType::Infer)); + let result = xml_string_to_json(String::from(xml), &config); + assert_eq!(expected, result.unwrap()); + + // test a non-array with array enforcement + type enforcement (as value) + let xml = r#"1"#; + let expected = json!({ + "a": { + "b": ["1"] + } + }); + let config = Config::new_with_defaults() + .add_json_type_override("/a/b", JsonArray::Always(JsonType::AlwaysString)); + let result = xml_string_to_json(String::from(xml), &config); + assert_eq!(expected, result.unwrap()); + + // test an array with array enforcement + type enforcement (as value) + let xml = r#"12"#; + let expected = json!({ + "a": { + "b": ["1","2"] + } + }); + let config = Config::new_with_defaults() + .add_json_type_override("/a/b", JsonArray::Always(JsonType::AlwaysString)); + let result = xml_string_to_json(String::from(xml), &config); + assert_eq!(expected, result.unwrap()); + + // test an array with array enforcement + null values + let xml = r#""#; + let expected = json!({ + "a": { + "b": [null] + } + }); + let config = Config::new_with_custom_values(false, "@", "#text", NullValue::Null) + .add_json_type_override("/a/b", JsonArray::Always(JsonType::Infer)); + let result = xml_string_to_json(String::from(xml), &config); + assert_eq!(expected, result.unwrap()); +} + #[test] fn test_malformed_xml() { let xml = r#"some text"#;