#![expect(
clippy::disallowed_types,
reason = "canonical JSON / Simplified Formats operate on the wire value by definition \
(ITS-JSON; ITS-REST Simplified Formats) — serde_json::Value IS the subject matter \
(#1694)"
)]
use quick_xml::Reader;
use quick_xml::Writer;
use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event};
pub use quick_xml::events::BytesStart as XmlStart;
pub const XSI_NS: &str = "http://www.w3.org/2001/XMLSchema-instance";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Namespace {
V1,
V2,
}
impl Namespace {
#[must_use]
pub const fn uri(self) -> &'static str {
match self {
Namespace::V1 => "http://schemas.openehr.org/v1",
Namespace::V2 => "http://schemas.openehr.org/v2",
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum XmlError {
#[error("xml write error: {0}")]
Write(#[from] quick_xml::Error),
#[error("xml io error: {0}")]
Io(#[from] std::io::Error),
#[error("xml output was not valid utf-8: {0}")]
Utf8(#[from] std::string::FromUtf8Error),
#[error("xml parse error: {0}")]
Parse(String),
}
pub struct XmlWriter {
w: Writer<Vec<u8>>,
pending_root_ns: Option<Namespace>,
}
impl std::fmt::Debug for XmlWriter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("XmlWriter")
.field("pending_root_ns", &self.pending_root_ns)
.finish_non_exhaustive()
}
}
impl XmlWriter {
#[must_use]
pub fn new() -> Self {
Self {
w: Writer::new(Vec::new()),
pending_root_ns: None,
}
}
pub fn set_root_namespace(&mut self, ns: Namespace) {
self.pending_root_ns = Some(ns);
}
pub fn write_start(&mut self, mut e: BytesStart<'_>) -> Result<(), XmlError> {
if let Some(ns) = self.pending_root_ns.take() {
e.push_attribute(("xmlns", ns.uri()));
e.push_attribute(("xmlns:xsi", XSI_NS));
}
self.w.write_event(Event::Start(e))?;
Ok(())
}
pub fn write_end(&mut self, tag: &str) -> Result<(), XmlError> {
self.w.write_event(Event::End(BytesEnd::new(tag)))?;
Ok(())
}
pub fn write_text_element(&mut self, tag: &str, text: &str) -> Result<(), XmlError> {
let mut start = BytesStart::new(tag);
if let Some(ns) = self.pending_root_ns.take() {
start.push_attribute(("xmlns", ns.uri()));
start.push_attribute(("xmlns:xsi", XSI_NS));
}
self.w.write_event(Event::Start(start))?;
self.w.write_event(Event::Text(BytesText::new(text)))?;
self.write_end(tag)
}
pub fn write_kv_element(&mut self, tag: &str, id: &str, text: &str) -> Result<(), XmlError> {
let mut start = BytesStart::new(tag);
start.push_attribute(("id", id));
self.w.write_event(Event::Start(start))?;
self.w.write_event(Event::Text(BytesText::new(text)))?;
self.write_end(tag)
}
pub fn into_string(self) -> Result<String, XmlError> {
Ok(String::from_utf8(self.w.into_inner())?)
}
}
impl Default for XmlWriter {
fn default() -> Self {
Self::new()
}
}
pub fn to_xml<T: ToXml + ?Sized>(
value: &T,
root_tag: &str,
ns: Namespace,
) -> Result<String, XmlError> {
let mut w = XmlWriter::new();
w.set_root_namespace(ns);
value.write_xml(&mut w, root_tag, None)?;
w.into_string()
}
pub fn to_xml_declared<T: ToXml + ?Sized>(
value: &T,
root_tag: &str,
declared: &str,
ns: Namespace,
) -> Result<String, XmlError> {
let mut w = XmlWriter::new();
w.set_root_namespace(ns);
value.write_xml(&mut w, root_tag, Some(declared))?;
w.into_string()
}
pub trait ToXml {
fn xml_type_name(&self) -> &'static str {
""
}
fn write_xml(
&self,
w: &mut XmlWriter,
tag: &str,
declared: Option<&str>,
) -> Result<(), XmlError>;
}
impl ToXml for String {
fn write_xml(&self, w: &mut XmlWriter, tag: &str, _d: Option<&str>) -> Result<(), XmlError> {
w.write_text_element(tag, self)
}
}
impl ToXml for str {
fn write_xml(&self, w: &mut XmlWriter, tag: &str, _d: Option<&str>) -> Result<(), XmlError> {
w.write_text_element(tag, self)
}
}
macro_rules! impl_to_xml_display {
($($t:ty),*) => {$(
impl ToXml for $t {
fn write_xml(&self, w: &mut XmlWriter, tag: &str, _d: Option<&str>)
-> Result<(), XmlError>
{
w.write_text_element(tag, &self.to_string())
}
}
)*};
}
impl_to_xml_display!(bool, i32, i64, u8, char, f32);
fn xsd_double_lexical(v: f64) -> String {
if v.is_nan() {
return "NaN".to_owned();
}
if v.is_infinite() {
return if v.is_sign_negative() { "-INF" } else { "INF" }.to_owned();
}
if v.fract() == 0.0 {
format!("{v:.1}")
} else {
v.to_string()
}
}
impl ToXml for f64 {
fn write_xml(&self, w: &mut XmlWriter, tag: &str, _d: Option<&str>) -> Result<(), XmlError> {
w.write_text_element(tag, &xsd_double_lexical(*self))
}
}
#[cfg(test)]
mod real_lexical_tests {
use super::xsd_double_lexical;
#[test]
fn real_values_take_the_xsd_double_lexical_form() {
assert_eq!(xsd_double_lexical(120.0), "120.0");
assert_eq!(xsd_double_lexical(0.0), "0.0");
assert_eq!(xsd_double_lexical(-0.0), "-0.0");
assert_eq!(xsd_double_lexical(-7.0), "-7.0");
assert_eq!(xsd_double_lexical(5.66), "5.66");
assert_eq!(
xsd_double_lexical(32.299_869_242_485_19),
"32.29986924248519"
);
assert_eq!(xsd_double_lexical(f64::INFINITY), "INF");
assert_eq!(xsd_double_lexical(f64::NEG_INFINITY), "-INF");
assert_eq!(xsd_double_lexical(f64::NAN), "NaN");
assert_ne!(xsd_double_lexical(f64::INFINITY), f64::INFINITY.to_string());
}
#[test]
fn every_emitted_lexeme_parses_back() {
for v in [
120.0_f64,
-0.0,
5.66,
32.299_869_242_485_19,
1e21,
1e-7,
f64::MAX,
f64::MIN_POSITIVE,
f64::INFINITY,
f64::NEG_INFINITY,
] {
let text = xsd_double_lexical(v);
let back: f64 = text
.parse()
.unwrap_or_else(|e| panic!("{text:?} does not parse back: {e}"));
assert_eq!(back.to_bits(), v.to_bits(), "{text:?}");
}
assert!(
xsd_double_lexical(f64::NAN)
.parse::<f64>()
.is_ok_and(f64::is_nan)
);
}
}
impl<T: ToXml> ToXml for Box<T> {
fn xml_type_name(&self) -> &'static str {
(**self).xml_type_name()
}
fn write_xml(&self, w: &mut XmlWriter, tag: &str, d: Option<&str>) -> Result<(), XmlError> {
(**self).write_xml(w, tag, d)
}
}
impl ToXml for uuid::Uuid {
fn write_xml(&self, w: &mut XmlWriter, tag: &str, _d: Option<&str>) -> Result<(), XmlError> {
w.write_text_element(tag, &self.to_string())
}
}
impl ToXml for serde_json::Value {
fn write_xml(&self, w: &mut XmlWriter, tag: &str, _d: Option<&str>) -> Result<(), XmlError> {
w.write_text_element(tag, &self.to_string())
}
}
#[derive(Debug, Clone)]
pub struct StartTag {
pub name: String,
pub attrs: Vec<(String, String)>,
}
impl StartTag {
#[must_use]
pub fn attr(&self, key: &str) -> Option<&str> {
self.attrs
.iter()
.find(|(k, _)| k == key)
.map(|(_, v)| v.as_str())
}
#[must_use]
pub fn xsi_type(&self) -> Option<&str> {
self.attrs
.iter()
.find(|(k, _)| k == "xsi:type" || (k.ends_with(":type") && k.contains("xsi")))
.map(|(_, v)| v.rsplit(':').next().unwrap_or(v))
}
}
#[derive(Debug)]
pub enum XmlEvent {
Start(StartTag),
End,
Text(String),
Eof,
}
pub struct XmlReader<'a> {
r: Reader<&'a [u8]>,
}
impl std::fmt::Debug for XmlReader<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("XmlReader").finish_non_exhaustive()
}
}
impl<'a> XmlReader<'a> {
#[must_use]
pub fn new(xml: &'a str) -> Self {
let mut r = Reader::from_str(xml);
r.config_mut().expand_empty_elements = true;
Self { r }
}
pub fn read(&mut self) -> Result<XmlEvent, XmlError> {
loop {
let ev = self
.r
.read_event()
.map_err(|e| XmlError::Parse(e.to_string()))?;
match ev {
Event::Start(e) => return Ok(XmlEvent::Start(to_start_tag(&e)?)),
Event::End(_) => return Ok(XmlEvent::End),
Event::Text(t) => {
let raw = t.decode().map_err(|e| XmlError::Parse(e.to_string()))?;
let s = quick_xml::escape::unescape(&raw)
.map_err(|e| XmlError::Parse(e.to_string()))?;
return Ok(XmlEvent::Text(s.into_owned()));
}
Event::Eof => return Ok(XmlEvent::Eof),
Event::GeneralRef(e) => {
if let Some(c) = e
.resolve_char_ref()
.map_err(|e| XmlError::Parse(e.to_string()))?
{
return Ok(XmlEvent::Text(c.to_string()));
}
let name = e.decode().map_err(|e| XmlError::Parse(e.to_string()))?;
let resolved = match name.as_ref() {
"amp" => "&",
"lt" => "<",
"gt" => ">",
"apos" => "'",
"quot" => "\"",
other => {
return Err(XmlError::Parse(format!("unknown entity &{other};")));
}
};
return Ok(XmlEvent::Text(resolved.to_string()));
}
_ => {}
}
}
}
pub fn skip_element(&mut self) -> Result<(), XmlError> {
let mut depth = 1i32;
while depth > 0 {
match self.read()? {
XmlEvent::Start(_) => depth += 1,
XmlEvent::End => depth -= 1,
XmlEvent::Eof => return Err(XmlError::Parse("unexpected EOF in element".into())),
XmlEvent::Text(_) => {}
}
}
Ok(())
}
}
fn to_start_tag(e: &BytesStart<'_>) -> Result<StartTag, XmlError> {
let qname = e.name();
let raw = String::from_utf8_lossy(qname.as_ref());
let name = raw.rsplit(':').next().unwrap_or(&raw).to_string();
let mut attrs = Vec::new();
for a in e.attributes() {
let a = a.map_err(|e| XmlError::Parse(e.to_string()))?;
let k = String::from_utf8_lossy(a.key.as_ref()).into_owned();
let raw = std::str::from_utf8(&a.value).map_err(|e| XmlError::Parse(e.to_string()))?;
let v = quick_xml::escape::unescape(raw)
.map_err(|e| XmlError::Parse(e.to_string()))?
.into_owned();
attrs.push((k, v));
}
Ok(StartTag { name, attrs })
}
pub fn from_xml<T: FromXml>(xml: &str) -> Result<T, XmlError> {
let mut reader = XmlReader::new(xml);
loop {
match reader.read()? {
XmlEvent::Start(s) => return T::from_xml(&mut reader, &s),
XmlEvent::Eof => return Err(XmlError::Parse("no root element".into())),
_ => {}
}
}
}
pub trait FromXml: Sized {
fn from_xml(reader: &mut XmlReader, start: &StartTag) -> Result<Self, XmlError>;
}
impl FromXml for String {
fn from_xml(reader: &mut XmlReader, _start: &StartTag) -> Result<Self, XmlError> {
let mut text = String::new();
loop {
match reader.read()? {
XmlEvent::Text(t) => text.push_str(&t),
XmlEvent::End => break,
XmlEvent::Start(_) => reader.skip_element()?, XmlEvent::Eof => return Err(XmlError::Parse("EOF in text element".into())),
}
}
Ok(text)
}
}
macro_rules! impl_from_xml_parse {
($($t:ty),*) => {$(
impl FromXml for $t {
fn from_xml(reader: &mut XmlReader, start: &StartTag) -> Result<Self, XmlError> {
let s = String::from_xml(reader, start)?;
s.trim().parse::<$t>().map_err(|e| XmlError::Parse(format!("{e}: {s:?}")))
}
}
)*};
}
impl_from_xml_parse!(bool, i32, i64, u8, f32, f64, char);
impl FromXml for uuid::Uuid {
fn from_xml(reader: &mut XmlReader, start: &StartTag) -> Result<Self, XmlError> {
let s = String::from_xml(reader, start)?;
uuid::Uuid::parse_str(s.trim()).map_err(|e| XmlError::Parse(e.to_string()))
}
}
impl<T: FromXml> FromXml for Box<T> {
fn from_xml(reader: &mut XmlReader, start: &StartTag) -> Result<Self, XmlError> {
Ok(Box::new(T::from_xml(reader, start)?))
}
}
impl FromXml for serde_json::Value {
fn from_xml(reader: &mut XmlReader, _start: &StartTag) -> Result<Self, XmlError> {
reader.skip_element()?;
Ok(serde_json::Value::Null)
}
}