use crate::base::{Error, Result, VALIDATION_ERROR, in_order};
use datavalue::{NumberValue, OwnedDataValue};
use serde::Deserialize;
use std::borrow::Cow;
pub(super) fn validation_error(message: impl std::fmt::Display) -> Error {
Error::service(VALIDATION_ERROR, format!("Validation error: {message}"))
}
pub(super) fn xml_to_value(xml: &str) -> Result<OwnedDataValue> {
#[derive(Deserialize)]
struct Read(#[serde(deserialize_with = "in_order")] OwnedDataValue);
quick_xml::de::from_str::<Read>(xml)
.map(|Read(value)| value)
.map_err(|e| validation_error(format_args!("Failed to parse XML: {e}")))
}
pub(super) fn value_to_xml(value: &OwnedDataValue, root_element: &str) -> String {
let root_element = sanitize_name(root_element);
let root_element = &*root_element;
let mut out = String::new();
tag(&mut out, "<", root_element);
content(value, &mut out);
tag(&mut out, "</", root_element);
out
}
fn tag(out: &mut String, open: &str, name: &str) {
out.push_str(open);
out.push_str(name);
out.push('>');
}
fn content(value: &OwnedDataValue, out: &mut String) {
match value {
OwnedDataValue::Object(pairs) => {
for (key, val) in pairs {
let key = sanitize_name(key);
tag(out, "<", &key);
content(val, out);
tag(out, "</", &key);
}
}
OwnedDataValue::Array(items) => {
for item in items {
out.push_str("<item>");
content(item, out);
out.push_str("</item>");
}
}
_ => scalar(value, out),
}
}
fn scalar(value: &OwnedDataValue, out: &mut String) {
use std::fmt::Write as _;
match value {
OwnedDataValue::Null => {}
OwnedDataValue::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
OwnedDataValue::Number(NumberValue::Integer(i)) => {
let _ = write!(out, "{i}");
}
OwnedDataValue::Number(NumberValue::Float(f)) => {
if let Some(n) = serde_json::Number::from_f64(*f) {
let _ = write!(out, "{n}");
}
}
OwnedDataValue::String(s) => escape(s, out),
OwnedDataValue::Object(_) | OwnedDataValue::Array(_) => {}
#[allow(
unreachable_patterns,
reason = "the catch-all is live only with datavalue's `datetime` or `tensor` \
feature, which feature unification can turn on from another crate"
)]
other => foreign(other, out),
}
}
#[cold]
fn foreign(value: &OwnedDataValue, out: &mut String) {
let text = serde_json::to_string(value).unwrap_or_default();
match crate::base::exact(&mut serde_json::Deserializer::from_str(&text)) {
Ok(OwnedDataValue::String(s)) => escape(&s, out),
Ok(other) => content(&other, out),
Err(_) => {}
}
}
fn escape(s: &str, out: &mut String) {
for c in s.chars() {
match c {
'&' => out.push_str("&"),
'<' => out.push_str("<"),
'>' => out.push_str(">"),
'"' => out.push_str("""),
'\'' => out.push_str("'"),
c => out.push(c),
}
}
}
fn sanitize_name(name: &str) -> Cow<'_, str> {
let mut chars = name.chars();
let valid = chars
.next()
.is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
&& chars.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.');
if valid {
return Cow::Borrowed(name);
}
let mut result = String::new();
for (i, c) in name.chars().enumerate() {
if i == 0 {
if c.is_ascii_alphabetic() || c == '_' {
result.push(c);
} else {
result.push('_');
if c.is_ascii_alphanumeric() {
result.push(c);
}
}
} else if c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.' {
result.push(c);
} else {
result.push('_');
}
}
if result.is_empty() {
result = "_element".to_string();
}
Cow::Owned(result)
}
#[cfg(test)]
mod tests {
use super::*;
fn doc(root: &str) -> String {
let value = OwnedDataValue::from_json(r#"{"a":1}"#).expect("JSON");
value_to_xml(&value, root)
}
#[test]
fn a_root_element_is_made_an_xml_name_as_a_key_is() {
assert_eq!(doc("envelope"), "<envelope><a>1</a></envelope>");
assert_eq!(
doc("x><admin>true</admin><x"),
"<x__admin_true__admin__x><a>1</a></x__admin_true__admin__x>"
);
assert_eq!(doc(""), "<_element><a>1</a></_element>");
assert_eq!(doc("a b"), "<a_b><a>1</a></a_b>");
assert_eq!(doc("1st"), "<_1st><a>1</a></_1st>");
}
#[test]
fn a_valid_name_is_borrowed_and_any_other_rewritten() {
for name in ["root", "_x", "a-b.c_9", "Z"] {
assert!(
matches!(sanitize_name(name), Cow::Borrowed(n) if n == name),
"{name}"
);
}
for (name, made) in [
("", "_element"),
("9", "_9"),
("-a", "_a"),
("a/b", "a_b"),
("é", "_"),
] {
assert_eq!(sanitize_name(name), made, "{name}");
}
}
}