Skip to main content

fml_xml/
model.rs

1//! The XML shape of a logical model, read from its StructureDefinitions.
2//!
3//! FHIR describes how a logical model looks in XML with element and structure extensions from the
4//! FHIR tools IG and the `ElementDefinition.representation` codes:
5//!
6//! - `xml-namespace` and `xml-name` give the namespace and the name of an element, or of the root
7//!   element when a structure is a document root;
8//! - `representation` says an element is an attribute (`xmlAttr`), the text content (`xmlText`),
9//!   an element whose type an `xsi:type` attribute states (`typeAttr`), or narrative kept as
10//!   markup (`cdaText`, `xhtml`);
11//! - `xml-choice-group` marks an element that is not in the XML: its children appear directly in
12//!   the parent, in any order (the parts of an address);
13//! - `elementdefinition-defaulttype` is the type of a `typeAttr` element without `xsi:type`;
14//! - `elementdefinition-date-format` gives the lexical form of a date and time in the XML.
15
16use std::collections::HashMap;
17use std::sync::Arc;
18
19use serde_json::Value;
20
21const XML_NAME: &str = "http://hl7.org/fhir/tools/StructureDefinition/xml-name";
22const XML_NAMESPACE: &[&str] = &[
23    "http://hl7.org/fhir/tools/StructureDefinition/xml-namespace",
24    "http://hl7.org/fhir/StructureDefinition/elementdefinition-namespace",
25];
26const CHOICE_GROUP: &str = "http://hl7.org/fhir/tools/StructureDefinition/xml-choice-group";
27const DEFAULT_TYPE: &str = "http://hl7.org/fhir/StructureDefinition/elementdefinition-defaulttype";
28const DATE_FORMAT: &str =
29    "http://hl7.org/fhir/tools/StructureDefinition/elementdefinition-date-format";
30
31/// How an element appears in XML.
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum Representation {
34    /// A child element.
35    Element,
36    /// An attribute.
37    Attribute,
38    /// The text content of the parent element.
39    Text,
40    /// Markup kept as it is (CDA narrative, XHTML): the element and its content as a string.
41    Markup,
42}
43
44/// An element of a type, in definition order.
45#[derive(Debug, Clone)]
46pub struct Element {
47    /// The name in the model, and the property name in JSON.
48    pub name: String,
49    /// The name in XML.
50    pub xml_name: String,
51    /// The namespace of the XML element: stated on the element, or the namespace of the structure
52    /// that defines it.
53    pub namespace: Option<String>,
54    /// Whether the namespace is stated on the element itself (an attribute is in no namespace
55    /// unless it is).
56    pub explicit_namespace: bool,
57    pub representation: Representation,
58    /// `xsi:type` states the type of the value (`typeAttr`).
59    pub type_attribute: bool,
60    /// Type codes or URLs.
61    pub types: Vec<String>,
62    /// The type of a `typeAttr` element without `xsi:type`.
63    pub default_type: Option<String>,
64    pub many: bool,
65    /// Not in the XML: the children appear directly in the parent, in any order.
66    pub choice_group: bool,
67    /// A date and time in the model's own lexical form (`YYYYMMDDHHMMSS.UUUU[+|-ZZzz]`).
68    pub date_format: bool,
69    /// Elements declared inline, which take the place of the type's own.
70    pub children: Vec<Element>,
71}
72
73impl Element {
74    /// The single type of the element, if it has one.
75    pub fn single_type(&self) -> Option<&str> {
76        match self.types.as_slice() {
77            [single] => Some(single),
78            _ => None,
79        }
80    }
81
82    pub fn child(&self, name: &str) -> Option<&Element> {
83        self.children.iter().find(|c| c.name == name)
84    }
85}
86
87/// A type of the model: a data type or a class.
88#[derive(Debug, Clone)]
89pub struct TypeDef {
90    pub url: String,
91    /// The structure's name: what `xsi:type` names it (`IVL_TS`).
92    pub name: String,
93    pub namespace: Option<String>,
94    /// The element name when the type is a document root (`ClinicalDocument`).
95    pub xml_name: Option<String>,
96    pub base: Option<String>,
97    pub is_abstract: bool,
98    /// All elements, inherited ones included, in definition order.
99    pub elements: Vec<Element>,
100}
101
102impl TypeDef {
103    pub fn element(&self, name: &str) -> Option<&Element> {
104        self.elements.iter().find(|e| e.name == name)
105    }
106}
107
108/// What a document's root element is: a type, or an element of a type (a fragment such as the
109/// `component` of a structured body, whose content is declared inline).
110#[derive(Debug, Clone, Copy)]
111pub enum Root<'m> {
112    Type(&'m TypeDef),
113    Element(&'m TypeDef, &'m Element),
114}
115
116impl<'m> Root<'m> {
117    /// How the root is named: the type's URL, or the URL and the element (`….StructuredBody.component`).
118    pub fn spec(&self) -> String {
119        match self {
120            Root::Type(ty) => ty.url.clone(),
121            Root::Element(ty, element) => format!("{}.{}", ty.url, element.name),
122        }
123    }
124
125    /// The first segment of JSON paths: the type name, or type and element (`StructuredBody.component`).
126    pub fn path(&self) -> String {
127        match self {
128            Root::Type(ty) => ty.name.clone(),
129            Root::Element(ty, element) => format!("{}.{}", ty.name, element.name),
130        }
131    }
132}
133
134/// The types of one or more logical models, by canonical URL.
135#[derive(Debug, Default, Clone)]
136pub struct Model {
137    types: HashMap<String, Arc<TypeDef>>,
138    /// `xsi:type` names: (namespace, name) and name alone.
139    by_name: HashMap<(Option<String>, String), String>,
140    by_bare_name: HashMap<String, Vec<String>>,
141    /// Root element names: (namespace, name).
142    roots: HashMap<(Option<String>, String), Vec<String>>,
143}
144
145impl Model {
146    pub fn new() -> Self {
147        Self::default()
148    }
149
150    /// Adds a StructureDefinition. Profiles (constraints on another type) are not types of their
151    /// own and are skipped, as are definitions without a snapshot. Returns whether it was added.
152    pub fn add(&mut self, resource: &Value) -> bool {
153        if resource["resourceType"] != "StructureDefinition"
154            || resource["derivation"] == "constraint"
155        {
156            return false;
157        }
158        let Some(url) = resource["url"].as_str() else {
159            return false;
160        };
161        let Some(snapshot) = resource["snapshot"]["element"].as_array() else {
162            return false;
163        };
164        let Some(root) = snapshot.first() else {
165            return false;
166        };
167        let root_path = root["path"].as_str().unwrap_or_default();
168        let namespace = namespace_of(resource);
169        let xml_name = extension_str(resource, XML_NAME).map(str::to_owned);
170        let mut elements: Vec<Element> = Vec::new();
171        for definition in &snapshot[1..] {
172            let Some(path) = definition["path"].as_str() else {
173                continue;
174            };
175            // Slices belong to profiles; a snapshot of a base type has none.
176            if definition["sliceName"].is_string() {
177                continue;
178            }
179            let Some(relative) = path
180                .strip_prefix(root_path)
181                .and_then(|p| p.strip_prefix('.'))
182            else {
183                continue;
184            };
185            let segments: Vec<&str> = relative.split('.').collect();
186            let element = element_of(definition, segments[segments.len() - 1], &namespace);
187            insert(&mut elements, &segments[..segments.len() - 1], element);
188        }
189        let name = resource["name"].as_str().unwrap_or(root_path).to_owned();
190        let def = TypeDef {
191            url: url.to_owned(),
192            name: name.clone(),
193            namespace: namespace.clone(),
194            xml_name: xml_name.clone(),
195            base: resource["baseDefinition"].as_str().map(str::to_owned),
196            is_abstract: resource["abstract"].as_bool().unwrap_or(false),
197            elements,
198        };
199        self.by_name
200            .insert((namespace.clone(), name.clone()), url.to_owned());
201        self.by_bare_name
202            .entry(name)
203            .or_default()
204            .push(url.to_owned());
205        if let Some(xml_name) = xml_name
206            && !def.is_abstract
207        {
208            self.roots
209                .entry((namespace, xml_name))
210                .or_default()
211                .push(url.to_owned());
212        }
213        self.types.insert(url.to_owned(), Arc::new(def));
214        true
215    }
216
217    /// Adds every StructureDefinition in a folder of JSON files (a package's `package` folder).
218    pub fn add_folder(&mut self, folder: &std::path::Path) -> std::io::Result<usize> {
219        let mut added = 0;
220        let mut paths: Vec<_> = std::fs::read_dir(folder)?
221            .filter_map(|entry| entry.ok().map(|e| e.path()))
222            .filter(|path| path.extension().is_some_and(|e| e == "json"))
223            .collect();
224        paths.sort();
225        for path in paths {
226            let Ok(text) = std::fs::read_to_string(&path) else {
227                continue;
228            };
229            let Ok(resource) = serde_json::from_str::<Value>(&text) else {
230                continue;
231            };
232            if self.add(&resource) {
233                added += 1;
234            }
235        }
236        Ok(added)
237    }
238
239    pub fn len(&self) -> usize {
240        self.types.len()
241    }
242
243    /// Every type, in no particular order.
244    pub fn types(&self) -> impl Iterator<Item = &Arc<TypeDef>> {
245        self.types.values()
246    }
247
248    pub fn is_empty(&self) -> bool {
249        self.types.is_empty()
250    }
251
252    /// The type at a canonical URL (a `|version` suffix is ignored).
253    pub fn get(&self, url: &str) -> Option<&Arc<TypeDef>> {
254        self.types.get(url.split('|').next().unwrap_or(url))
255    }
256
257    /// Whether the model has a type at `url`.
258    pub fn contains(&self, url: &str) -> bool {
259        self.get(url).is_some()
260    }
261
262    /// The type an `xsi:type` names: by namespace and name, or by name when only one type has it.
263    pub fn by_type_name(&self, namespace: Option<&str>, name: &str) -> Option<&Arc<TypeDef>> {
264        if let Some(url) = self
265            .by_name
266            .get(&(namespace.map(str::to_owned), name.to_owned()))
267        {
268            return self.get(url);
269        }
270        match self.by_bare_name.get(name).map(Vec::as_slice) {
271            Some([single]) => self.get(single),
272            _ => None,
273        }
274    }
275
276    /// The root a name gives: a canonical URL or type name, or either followed by `.element`.
277    pub fn root(&self, spec: &str) -> Option<Root<'_>> {
278        if let Some(ty) = self.get(spec).or_else(|| self.by_type_name(None, spec)) {
279            return Some(Root::Type(ty));
280        }
281        let (ty, element) = spec.rsplit_once('.')?;
282        let ty = self.get(ty).or_else(|| self.by_type_name(None, ty))?;
283        let element = ty.element(element)?;
284        Some(Root::Element(ty, element))
285    }
286
287    /// The types whose documents have this root element.
288    pub fn roots(&self, namespace: Option<&str>, name: &str) -> Vec<&Arc<TypeDef>> {
289        self.roots
290            .get(&(namespace.map(str::to_owned), name.to_owned()))
291            .map(|urls| urls.iter().filter_map(|url| self.get(url)).collect())
292            .unwrap_or_default()
293    }
294
295    /// The type a type code of an element names, resolved like the element's own URL: a URL as
296    /// it is, anything else in the namespace of `context` (`http://…/StructureDefinition/`).
297    pub fn type_of(&self, code: &str, context: &str) -> Option<&Arc<TypeDef>> {
298        if let Some(found) = self.get(code) {
299            return Some(found);
300        }
301        let prefix = context.rsplit_once('/').map(|(p, _)| p)?;
302        self.get(&format!("{prefix}/{code}"))
303    }
304
305    /// Whether `ty` is `ancestor` or derives from it.
306    pub fn derives_from(&self, ty: &TypeDef, ancestor: &str) -> bool {
307        let mut current = Some(ty.url.clone());
308        for _ in 0..32 {
309            let Some(url) = current else { return false };
310            if url == ancestor {
311                return true;
312            }
313            current = self.get(&url).and_then(|t| t.base.clone());
314        }
315        false
316    }
317}
318
319fn insert(elements: &mut Vec<Element>, parents: &[&str], element: Element) {
320    match parents.split_first() {
321        None => elements.push(element),
322        Some((first, rest)) => {
323            if let Some(parent) = elements.iter_mut().rev().find(|e| e.name == *first) {
324                insert(&mut parent.children, rest, element);
325            }
326        }
327    }
328}
329
330fn element_of(definition: &Value, segment: &str, type_namespace: &Option<String>) -> Element {
331    let name = segment.trim_end_matches("[x]").to_owned();
332    let explicit = XML_NAMESPACE
333        .iter()
334        .find_map(|url| extension_str(definition, url))
335        .map(str::to_owned);
336    let representation = definition["representation"]
337        .as_array()
338        .map(|codes| codes.iter().filter_map(Value::as_str).collect::<Vec<_>>())
339        .unwrap_or_default();
340    let has = |code: &str| representation.contains(&code);
341    let kind = if has("xmlAttr") {
342        Representation::Attribute
343    } else if has("xmlText") {
344        Representation::Text
345    } else if has("cdaText") || has("xhtml") {
346        Representation::Markup
347    } else {
348        Representation::Element
349    };
350    let types = definition["type"]
351        .as_array()
352        .map(|types| {
353            types
354                .iter()
355                .filter_map(|t| t["code"].as_str().map(str::to_owned))
356                .collect()
357        })
358        .unwrap_or_default();
359    let many = match definition["max"].as_str() {
360        Some("*") => true,
361        Some(max) => max.parse::<u32>().is_ok_and(|m| m > 1),
362        None => false,
363    };
364    Element {
365        xml_name: extension_str(definition, XML_NAME)
366            .map(str::to_owned)
367            .unwrap_or_else(|| name.clone()),
368        name,
369        namespace: explicit.clone().or_else(|| type_namespace.clone()),
370        explicit_namespace: explicit.is_some(),
371        representation: kind,
372        type_attribute: has("typeAttr"),
373        types,
374        default_type: extension_str(definition, DEFAULT_TYPE).map(str::to_owned),
375        many,
376        choice_group: definition["extension"].as_array().is_some_and(|exts| {
377            exts.iter()
378                .any(|e| e["url"] == CHOICE_GROUP && e["valueBoolean"] == true)
379        }),
380        date_format: extension_str(definition, DATE_FORMAT).is_some(),
381        children: Vec::new(),
382    }
383}
384
385fn namespace_of(resource: &Value) -> Option<String> {
386    XML_NAMESPACE
387        .iter()
388        .find_map(|url| extension_str(resource, url))
389        .map(str::to_owned)
390}
391
392/// The string value (`valueUri`, `valueString`, `valueCode`, `valueCanonical`) of an extension.
393fn extension_str<'a>(holder: &'a Value, url: &str) -> Option<&'a str> {
394    holder["extension"].as_array()?.iter().find_map(|ext| {
395        if ext["url"] != url {
396            return None;
397        }
398        [
399            "valueUri",
400            "valueString",
401            "valueCode",
402            "valueCanonical",
403            "valueUrl",
404        ]
405        .iter()
406        .find_map(|key| ext[*key].as_str())
407    })
408}