use ::serde::Serialize;
use ::serde::de::DeserializeOwned;
use ::serde_json::{Map, Value};
use crate::meta::ElementMeta;
const FHIR_NS: &str = "http://hl7.org/fhir";
fn is_number_type(code: &str) -> bool {
matches!(code, "integer" | "decimal" | "positiveInt" | "unsignedInt")
}
#[must_use]
pub fn to_xml<T: Serialize>(resource: &T, resource_type: &str) -> String {
let value = ::serde_json::to_value(resource).unwrap_or(Value::Null);
let empty = Map::new();
let obj = value.as_object().unwrap_or(&empty);
let mut out = String::new();
write_complex(&mut out, resource_type, obj, true);
out
}
fn escape(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
}
fn scalar_str(v: &Value) -> Option<String> {
match v {
Value::String(s) => Some(s.clone()),
Value::Bool(b) => Some(b.to_string()),
Value::Number(n) => Some(n.to_string()),
_ => None,
}
}
fn write_complex(out: &mut String, name: &str, obj: &Map<String, Value>, is_resource_root: bool) {
let mut consumed: Vec<&str> = Vec::new();
out.push('<');
out.push_str(name);
if is_resource_root {
out.push_str(" xmlns=\"");
out.push_str(FHIR_NS);
out.push('"');
}
if (name == "extension" || name == "modifierExtension")
&& let Some(Value::String(url)) = obj.get("url")
{
out.push_str(" url=\"");
out.push_str(&escape(url));
out.push('"');
consumed.push("url");
}
if !is_resource_root
&& let Some(Value::String(id)) = obj.get("id")
{
out.push_str(" id=\"");
out.push_str(&escape(id));
out.push('"');
consumed.push("id");
}
let children: Vec<(&String, &Value)> = obj
.iter()
.filter(|(k, _)| k.as_str() != "resourceType" && !consumed.contains(&k.as_str()))
.collect();
if children.is_empty() {
out.push_str("/>");
return;
}
out.push('>');
for (key, value) in children {
write_child(out, key, value);
}
out.push_str("</");
out.push_str(name);
out.push('>');
}
fn write_child(out: &mut String, key: &str, value: &Value) {
match value {
Value::Null => {}
Value::Array(items) => {
for item in items {
write_child(out, key, item);
}
}
Value::Object(o) => write_complex(out, key, o, false),
scalar => {
if let Some(s) = scalar_str(scalar) {
out.push('<');
out.push_str(key);
out.push_str(" value=\"");
out.push_str(&escape(&s));
out.push_str("\"/>");
}
}
}
}
#[derive(Debug)]
pub enum XmlError {
Xml(quick_xml::Error),
Deserialize(String),
Custom(String),
}
impl std::fmt::Display for XmlError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
XmlError::Xml(e) => write!(f, "XML error: {e}"),
XmlError::Deserialize(m) | XmlError::Custom(m) => f.write_str(m),
}
}
}
impl std::error::Error for XmlError {}
impl From<quick_xml::Error> for XmlError {
fn from(e: quick_xml::Error) -> Self {
XmlError::Xml(e)
}
}
pub fn from_xml<T: DeserializeOwned>(
table: &'static [ElementMeta],
xml: &str,
) -> Result<T, XmlError> {
let value = xml_to_value(table, xml)?;
::serde_json::from_value(value).map_err(|e| XmlError::Deserialize(e.to_string()))
}
pub fn xml_to_value(table: &'static [ElementMeta], xml: &str) -> Result<Value, XmlError> {
use quick_xml::events::Event;
let mut reader = quick_xml::Reader::from_str(xml);
reader.config_mut().trim_text(true);
loop {
match reader.read_event()? {
Event::Start(e) => {
let name = String::from_utf8_lossy(e.name().as_ref()).into_owned();
let (id, _url, _val) = read_attrs(&e);
let mut obj = Map::new();
obj.insert("resourceType".to_string(), Value::String(name.clone()));
if let Some(id) = id {
obj.insert("id".to_string(), Value::String(id));
}
read_children(table, &mut reader, &name, &name, &mut obj)?;
return Ok(Value::Object(obj));
}
Event::Eof => return Err(XmlError::Custom("no root element".into())),
_ => {}
}
}
}
fn read_attrs(e: &quick_xml::events::BytesStart) -> (Option<String>, Option<String>, Option<String>) {
let (mut id, mut url, mut val) = (None, None, None);
for attr in e.attributes().flatten() {
let key = attr.key.as_ref();
let value = String::from_utf8_lossy(&attr.value).into_owned();
match key {
b"id" => id = Some(value),
b"url" => url = Some(value),
b"value" => val = Some(value),
_ => {}
}
}
(id, url, val)
}
fn read_children(
table: &'static [ElementMeta],
reader: &mut quick_xml::Reader<&[u8]>,
elem_name: &str,
context: &str,
obj: &mut Map<String, Value>,
) -> Result<(), XmlError> {
use quick_xml::events::Event;
loop {
match reader.read_event()? {
Event::Start(e) => {
let name = String::from_utf8_lossy(e.name().as_ref()).into_owned();
let child = read_element(table, reader, &name, context, &e, false)?;
insert_child(table, obj, &name, context, child);
}
Event::Empty(e) => {
let name = String::from_utf8_lossy(e.name().as_ref()).into_owned();
let child = read_element(table, reader, &name, context, &e, true)?;
insert_child(table, obj, &name, context, child);
}
Event::End(e) => {
if String::from_utf8_lossy(e.name().as_ref()) == elem_name {
return Ok(());
}
}
Event::Eof => return Ok(()),
_ => {}
}
}
}
fn read_element(
table: &'static [ElementMeta],
reader: &mut quick_xml::Reader<&[u8]>,
name: &str,
parent_context: &str,
start: &quick_xml::events::BytesStart,
is_empty: bool,
) -> Result<Value, XmlError> {
let (id, url, val) = read_attrs(start);
let path = format!("{parent_context}.{name}");
let el = element_meta(table, &path, parent_context, name);
let type_code = el.as_ref().and_then(|e| e.types.first().map(|t| t.code)).unwrap_or("");
if is_empty {
if let Some(v) = val {
return Ok(coerce(&v, type_code));
}
let mut obj = Map::new();
if let Some(id) = id {
obj.insert("id".to_string(), Value::String(id));
}
if let Some(url) = url {
obj.insert("url".to_string(), Value::String(url));
}
return Ok(Value::Object(obj));
}
let mut obj = Map::new();
if let Some(id) = id {
obj.insert("id".to_string(), Value::String(id));
}
if let Some(url) = url {
obj.insert("url".to_string(), Value::String(url));
}
let child_context = if is_datatype(type_code) { type_code.to_string() } else { path };
read_children(table, reader, name, &child_context, &mut obj)?;
Ok(Value::Object(obj))
}
fn insert_child(
table: &'static [ElementMeta],
obj: &mut Map<String, Value>,
name: &str,
context: &str,
child: Value,
) {
let multiple = element_meta(table, &format!("{context}.{name}"), context, name)
.is_some_and(ElementMeta::is_multiple);
match obj.get_mut(name) {
Some(Value::Array(arr)) => arr.push(child),
Some(existing) => {
let prev = existing.take();
*existing = Value::Array(vec![prev, child]);
}
None => {
if multiple {
obj.insert(name.to_string(), Value::Array(vec![child]));
} else {
obj.insert(name.to_string(), child);
}
}
}
}
fn element_meta(
table: &'static [ElementMeta],
path: &str,
context: &str,
name: &str,
) -> Option<&'static ElementMeta> {
if let Some(e) = crate::meta::find(table, path) {
return Some(e);
}
let prefix = format!("{context}.");
table.iter().filter(|e| e.path.starts_with(&prefix)).find(|e| {
e.path.ends_with("[x]") && {
let base = &e.path[context.len() + 1..e.path.len() - 3];
name.len() > base.len()
&& name.starts_with(base)
&& name[base.len()..].chars().next().is_some_and(char::is_uppercase)
}
})
}
fn is_datatype(code: &str) -> bool {
!code.is_empty()
&& code.chars().next().is_some_and(char::is_uppercase)
&& code != "BackboneElement"
&& code != "Element"
}
fn coerce(value: &str, type_code: &str) -> Value {
if type_code == "boolean" && let Ok(b) = value.parse::<bool>() {
return Value::Bool(b);
}
if is_number_type(type_code) && let Ok(n) = value.parse::<::serde_json::Number>() {
return Value::Number(n);
}
Value::String(value.to_string())
}