tlq_fhir_format/
lib.rs

1//! FHIR JSON ↔ XML conversion helpers.
2//! The implementation is schema‑agnostic but follows the official
3//! JSON/XML mapping rules used by HL7 FHIR:
4//! - Root element uses the `resourceType` name.
5//! - Primitive values are encoded with the `value` attribute.
6//! - Primitive metadata (`id`, `extension`) is carried through `_field` entries.
7//! - Arrays are represented by repeated elements and aligned metadata arrays.
8
9use quick_xml::Writer;
10use quick_xml::events::{BytesEnd, BytesStart, Event};
11use roxmltree::Document;
12use serde_json::{Map, Value};
13use std::collections::HashMap;
14use std::io::Cursor;
15use thiserror::Error;
16
17const FHIR_NS: &str = "http://hl7.org/fhir";
18const XHTML_NS: &str = "http://www.w3.org/1999/xhtml";
19
20#[derive(Debug, Error)]
21pub enum FormatError {
22    #[error("expected a JSON object for the resource")]
23    ExpectedObject,
24    #[error("missing resourceType property")]
25    MissingResourceType,
26    #[error("JSON parse error: {0}")]
27    Json(#[from] serde_json::Error),
28    #[error("XML parse error: {0}")]
29    Xml(#[from] roxmltree::Error),
30    #[error("UTF-8 error: {0}")]
31    Utf8(#[from] std::string::FromUtf8Error),
32    #[error("XML write error: {0}")]
33    XmlWrite(#[from] quick_xml::Error),
34}
35
36/// Convert a FHIR JSON payload into its XML representation.
37pub fn json_to_xml(input: &str) -> Result<String, FormatError> {
38    let value: Value = serde_json::from_str(input)?;
39    let obj = value.as_object().ok_or(FormatError::ExpectedObject)?;
40    let resource_type = obj
41        .get("resourceType")
42        .and_then(Value::as_str)
43        .ok_or(FormatError::MissingResourceType)?;
44
45    let mut writer = Writer::new_with_indent(Cursor::new(Vec::new()), b' ', 2);
46    let mut root = BytesStart::new(resource_type);
47    root.push_attribute(("xmlns", FHIR_NS));
48    writer.write_event(Event::Start(root.clone()))?;
49
50    let mut meta = HashMap::new();
51    for (k, v) in obj {
52        if k.starts_with('_') {
53            meta.insert(k.trim_start_matches('_').to_string(), v.clone());
54        }
55    }
56
57    for (k, v) in obj {
58        if k == "resourceType" || k.starts_with('_') {
59            continue;
60        }
61        let meta_entry = meta.get(k);
62        write_json_value(&mut writer, k, v, meta_entry)?;
63    }
64
65    // Handle metadata fields that don't have a corresponding value field
66    // (e.g., _active with extensions but no active field)
67    for (k, v) in &meta {
68        if !obj.contains_key(k) {
69            // This metadata has no corresponding value, write it as a primitive with no value
70            write_json_value(&mut writer, k, &Value::Null, Some(v))?;
71        }
72    }
73
74    writer.write_event(Event::End(BytesEnd::new(resource_type)))?;
75    let bytes = writer.into_inner().into_inner();
76    Ok(String::from_utf8(bytes)?)
77}
78
79/// Convert a FHIR XML payload into its JSON representation.
80pub fn xml_to_json(input: &str) -> Result<String, FormatError> {
81    let doc = Document::parse(input)?;
82    let root = doc.root_element();
83
84    let mut map = Map::new();
85    map.insert(
86        "resourceType".to_string(),
87        Value::String(root.tag_name().name().to_string()),
88    );
89
90    let mut accumulator = Map::new();
91    for child in root.children().filter(|n| n.is_element()) {
92        process_xml_child(input, &mut accumulator, &child)?;
93    }
94
95    map.extend(accumulator);
96    let json = Value::Object(map);
97    Ok(serde_json::to_string_pretty(&json)?)
98}
99
100fn write_json_value(
101    writer: &mut Writer<Cursor<Vec<u8>>>,
102    name: &str,
103    value: &Value,
104    meta: Option<&Value>,
105) -> Result<(), FormatError> {
106    match value {
107        Value::Array(items) => {
108            let meta_array = meta.and_then(Value::as_array);
109            for (idx, item) in items.iter().enumerate() {
110                let item_meta = meta_array.and_then(|m| m.get(idx));
111                write_json_value(writer, name, item, item_meta)?;
112            }
113        }
114        Value::Object(obj) => write_complex(writer, name, obj)?,
115        Value::Null => {}
116        primitive => write_primitive(writer, name, primitive, meta)?,
117    }
118    Ok(())
119}
120
121fn write_complex(
122    writer: &mut Writer<Cursor<Vec<u8>>>,
123    name: &str,
124    obj: &Map<String, Value>,
125) -> Result<(), FormatError> {
126    let mut meta = HashMap::new();
127    for (k, v) in obj {
128        if k.starts_with('_') {
129            meta.insert(k.trim_start_matches('_').to_string(), v.clone());
130        }
131    }
132
133    let mut start = BytesStart::new(name);
134    if let Some(Value::String(id)) = obj.get("id") {
135        start.push_attribute(("id", id.as_str()));
136    }
137
138    writer.write_event(Event::Start(start))?;
139
140    for (k, v) in obj {
141        if k.starts_with('_') || k == "id" {
142            continue;
143        }
144        let meta_entry = meta.get(k);
145        write_json_value(writer, k, v, meta_entry)?;
146    }
147
148    writer.write_event(Event::End(BytesEnd::new(name)))?;
149    Ok(())
150}
151
152fn write_primitive(
153    writer: &mut Writer<Cursor<Vec<u8>>>,
154    name: &str,
155    value: &Value,
156    meta: Option<&Value>,
157) -> Result<(), FormatError> {
158    let mut elem = BytesStart::new(name);
159
160    // Only add value attribute if the value is not null
161    let has_value = !matches!(value, Value::Null);
162    if has_value {
163        elem.push_attribute(("value", primitive_to_string(value).as_str()));
164    }
165
166    let mut has_children = false;
167    if let Some(Value::Object(m)) = meta {
168        if let Some(Value::String(id)) = m.get("id") {
169            elem.push_attribute(("id", id.as_str()));
170        }
171        if m.get("extension").is_some() {
172            has_children = true;
173        }
174    }
175
176    // If we have neither a value nor children, skip writing this element
177    if !has_value && !has_children {
178        return Ok(());
179    }
180
181    if has_children {
182        writer.write_event(Event::Start(elem.clone()))?;
183        if let Some(Value::Object(m)) = meta {
184            if let Some(ext) = m.get("extension") {
185                write_json_value(writer, "extension", ext, None)?;
186            }
187        }
188        writer.write_event(Event::End(BytesEnd::new(name)))?;
189    } else {
190        writer.write_event(Event::Empty(elem))?;
191    }
192    Ok(())
193}
194
195fn primitive_to_string(value: &Value) -> String {
196    match value {
197        Value::String(s) => s.clone(),
198        Value::Number(n) => n.to_string(),
199        Value::Bool(b) => b.to_string(),
200        Value::Null => "".to_string(),
201        other => other.to_string(),
202    }
203}
204
205fn process_xml_child(
206    source: &str,
207    target: &mut Map<String, Value>,
208    node: &roxmltree::Node,
209) -> Result<(), FormatError> {
210    let name = node.tag_name().name().to_string();
211    let (value, meta) = xml_element_to_value(source, node)?;
212
213    insert_json_property(target, &name, value, meta);
214    Ok(())
215}
216
217fn xml_element_to_value(
218    source: &str,
219    node: &roxmltree::Node,
220) -> Result<(Value, Option<Value>), FormatError> {
221    if node.tag_name().namespace().is_some_and(|ns| ns == XHTML_NS) {
222        let snippet = &source[node.range()];
223        return Ok((Value::String(snippet.to_string()), None));
224    }
225
226    let mut meta_map = Map::new();
227    if let Some(id) = node.attribute("id") {
228        meta_map.insert("id".to_string(), Value::String(id.to_string()));
229    }
230
231    if let Some(val) = node.attribute("value") {
232        let mut extensions = Vec::new();
233        for child in node.children().filter(|c| c.is_element()) {
234            if child.tag_name().name() == "extension" {
235                let (ext_val, _ext_meta) = xml_element_to_value(source, &child)?;
236                extensions.push(ext_val);
237            }
238        }
239        if !extensions.is_empty() {
240            meta_map.insert("extension".to_string(), Value::Array(extensions));
241        }
242        let prim = parse_primitive(val);
243        let meta = if meta_map.is_empty() {
244            None
245        } else {
246            Some(Value::Object(meta_map))
247        };
248        return Ok((prim, meta));
249    }
250
251    let mut obj = Map::new();
252    if let Some(id) = node.attribute("id") {
253        obj.insert("id".to_string(), Value::String(id.to_string()));
254    }
255
256    for child in node.children().filter(|c| c.is_element()) {
257        process_xml_child(source, &mut obj, &child)?;
258    }
259
260    Ok((Value::Object(obj), None))
261}
262
263fn insert_json_property(
264    map: &mut Map<String, Value>,
265    name: &str,
266    value: Value,
267    meta: Option<Value>,
268) {
269    let entry = map.entry(name.to_string());
270    match entry {
271        serde_json::map::Entry::Vacant(v) => {
272            v.insert(value);
273        }
274        serde_json::map::Entry::Occupied(mut o) => match o.get_mut() {
275            Value::Array(arr) => arr.push(value),
276            existing => {
277                let old = existing.take();
278                *existing = Value::Array(vec![old, value]);
279            }
280        },
281    }
282
283    if meta.is_none() && !map.contains_key(&format!("_{}", name)) {
284        return;
285    }
286
287    let meta_key = format!("_{}", name);
288    let value_is_array = matches!(map.get(name), Some(Value::Array(_)));
289    let value_count = match map.get(name) {
290        Some(Value::Array(arr)) => arr.len(),
291        Some(_) => 1,
292        None => 0,
293    };
294
295    match map.entry(meta_key) {
296        serde_json::map::Entry::Vacant(v) => {
297            if let Some(m) = meta {
298                if value_is_array {
299                    let mut arr = Vec::new();
300                    if value_count > 1 {
301                        arr.resize(value_count - 1, Value::Null);
302                    }
303                    arr.push(m);
304                    v.insert(Value::Array(arr));
305                } else {
306                    v.insert(m);
307                }
308            }
309        }
310        serde_json::map::Entry::Occupied(mut o) => match o.get_mut() {
311            Value::Array(arr) => {
312                if let Some(m) = meta {
313                    if arr.len() + 1 < value_count {
314                        arr.resize(value_count - 1, Value::Null);
315                    }
316                    arr.push(m);
317                } else {
318                    arr.push(Value::Null);
319                }
320            }
321            existing => {
322                if value_is_array {
323                    let first = existing.take();
324                    let mut arr = Vec::new();
325                    arr.push(first);
326                    if value_count > 1 {
327                        arr.resize(value_count - 1, Value::Null);
328                    }
329                    if let Some(m) = meta {
330                        arr.push(m);
331                    } else {
332                        arr.push(Value::Null);
333                    }
334                    *existing = Value::Array(arr);
335                } else if let Some(m) = meta {
336                    *existing = m;
337                }
338            }
339        },
340    }
341}
342
343fn parse_primitive(input: &str) -> Value {
344    match input {
345        "true" => Value::Bool(true),
346        "false" => Value::Bool(false),
347        _ => {
348            if let Ok(int) = input.parse::<i64>() {
349                Value::Number(int.into())
350            } else {
351                Value::String(input.to_string())
352            }
353        }
354    }
355}
356
357#[cfg(test)]
358mod tests {
359    use super::*;
360
361    #[test]
362    fn json_to_xml_basic_patient() {
363        let json = r#"
364        {
365            "resourceType": "Patient",
366            "id": "pat-1",
367            "active": true,
368            "name": [
369                { "family": "Everyman", "given": ["Adam"] }
370            ]
371        }
372        "#;
373
374        let xml = json_to_xml(json).expect("conversion failed");
375        assert!(xml.contains("<Patient"));
376        assert!(xml.contains(r#"<id value="pat-1"/>"#));
377        assert!(xml.contains(r#"<active value="true"/>"#));
378        assert!(xml.contains(r#"<family value="Everyman"/>"#));
379    }
380
381    #[test]
382    fn xml_to_json_round_trip() {
383        let xml = r#"
384        <Patient xmlns="http://hl7.org/fhir">
385            <id value="p1"/>
386            <active value="true"/>
387            <name>
388                <family value="Everyman"/>
389                <given value="Adam"/>
390            </name>
391        </Patient>
392        "#;
393
394        let json = xml_to_json(xml).expect("xml->json failed");
395        let value: Value = serde_json::from_str(&json).unwrap();
396        assert_eq!(value["resourceType"], "Patient");
397        assert_eq!(value["id"], "p1");
398        assert_eq!(value["active"], true);
399        let family = if value["name"].is_array() {
400            value["name"][0]["family"].clone()
401        } else {
402            value["name"]["family"].clone()
403        };
404        assert_eq!(family, "Everyman");
405    }
406
407    #[test]
408    fn primitive_metadata_survives_roundtrip() {
409        let json = r#"
410        {
411            "resourceType": "Patient",
412            "birthDate": "1974-12-25",
413            "_birthDate": { "id": "bd1" }
414        }
415        "#;
416
417        let xml = json_to_xml(json).unwrap();
418        assert!(xml.contains("<birthDate"));
419        assert!(xml.contains(r#"value="1974-12-25""#));
420        assert!(xml.contains(r#"id="bd1""#));
421
422        let back = xml_to_json(&xml).unwrap();
423        let val: Value = serde_json::from_str(&back).unwrap();
424        assert_eq!(val["birthDate"], "1974-12-25");
425        assert_eq!(val["_birthDate"]["id"], "bd1");
426    }
427}