#![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 std::collections::{HashSet, VecDeque};
use std::sync::LazyLock;
use openehr_rm::v1_2::paths::{PathSegment, is_archetype_root_node_id};
use serde_json::{Map, Value, json};
use crate::flat::error::FlatError;
use crate::flat::rmpath;
use crate::flat::webtemplate::model::{WebTemplate, WebTemplateNode};
pub const TDD_TEMPLATE_NS: &str = "http://schemas.oceanehr.com/templates";
const RM_VERSION: &str = "1.2.0";
const DEFAULT_TIME: &str = "1970-01-01T00:00:00Z";
struct El {
name: String,
attrs: Vec<(String, String)>,
text: String,
children: Vec<El>,
}
impl El {
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))
}
fn child(&self, name: &str) -> Option<&El> {
self.children.iter().find(|c| c.name == name)
}
}
fn local_name(raw: &str) -> String {
raw.rsplit(':').next().unwrap_or(raw).to_string()
}
fn parse_tree(xml: &str) -> Result<El, FlatError> {
use quick_xml::Reader;
use quick_xml::events::Event;
let mut reader = Reader::from_str(xml);
let mut stack: Vec<El> = Vec::new();
let mut root: Option<El> = None;
loop {
match reader
.read_event()
.map_err(|e| FlatError::Conversion(format!("TDD is not well-formed XML: {e}")))?
{
Event::Start(e) => stack.push(start_element(&e)?),
Event::Empty(e) => {
stack.push(start_element(&e)?);
close_element(&mut stack, &mut root);
}
Event::End(_) => close_element(&mut stack, &mut root),
Event::Text(t) => {
push_text(&mut stack, t.as_ref().trim());
}
Event::CData(t) => {
push_text(&mut stack, t.as_ref());
}
Event::Eof => break,
_ => {}
}
}
root.ok_or_else(|| FlatError::Conversion("TDD has no root element".into()))
}
fn start_element(e: &quick_xml::events::BytesStart<'_>) -> Result<El, FlatError> {
let mut attrs = Vec::new();
for a in e.attributes() {
let a =
a.map_err(|err| FlatError::Conversion(format!("malformed TDD attribute: {err}")))?;
let key = a.key.as_ref().to_owned();
let val = a.value.as_ref().to_owned();
attrs.push((key, val));
}
Ok(El {
name: local_name(e.name().as_ref()),
attrs,
text: String::new(),
children: Vec::new(),
})
}
fn close_element(stack: &mut Vec<El>, root: &mut Option<El>) {
if let Some(done) = stack.pop() {
match stack.last_mut() {
Some(parent) => parent.children.push(done),
None => *root = Some(done),
}
}
}
fn push_text(stack: &mut [El], text: &str) {
if let Some(top) = stack.last_mut() {
top.text.push_str(text);
}
}
fn to_canonical_xml(el: &El, type_hint: Option<&str>) -> String {
let mut out = String::new();
write_el(el, true, type_hint, &mut out);
out
}
fn write_el(el: &El, is_root: bool, type_hint: Option<&str>, out: &mut String) {
out.push('<');
out.push_str(&el.name);
if is_root {
out.push_str(" xmlns=\"http://schemas.openehr.org/v1\"");
out.push_str(" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"");
}
let mut has_xsi = false;
for (k, v) in &el.attrs {
if k.starts_with("xmlns") {
continue;
}
if k == "xsi:type" || (k.ends_with(":type") && k.contains("xsi")) {
has_xsi = true;
let v = v.rsplit(':').next().unwrap_or(v);
out.push_str(" xsi:type=\"");
out.push_str(&xml_escape(v));
out.push('"');
}
}
if is_root
&& !has_xsi
&& let Some(h) = type_hint
{
out.push_str(" xsi:type=\"");
out.push_str(&xml_escape(h));
out.push('"');
}
if el.children.is_empty() && el.text.is_empty() {
out.push_str("/>");
return;
}
out.push('>');
if !el.text.is_empty() {
out.push_str(&xml_escape(&el.text));
}
for c in &el.children {
write_el(c, false, None, out);
}
out.push_str("</");
out.push_str(&el.name);
out.push('>');
}
fn xml_escape(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
}
fn parse_typed(el: &El, type_name: &str) -> Result<Value, FlatError> {
use openehr_base::prelude::UidBasedId;
use openehr_its::xml::from_canonical_xml as fx;
use openehr_rm::prelude::{
CodePhrase, DataValue, DvBoolean, DvCodedText, DvCount, DvDate, DvDateTime, DvDuration,
DvEhrUri, DvIdentifier, DvMultimedia, DvOrdinal, DvParagraph, DvParsable, DvProportion,
DvQuantity, DvScale, DvState, DvText, DvTime, DvUri, EventContext, FeederAudit,
IsmTransition, Link, Participation, PartyProxy,
};
let resolved = el.xsi_type().unwrap_or(type_name);
let xml = to_canonical_xml(el, Some(resolved));
macro_rules! p {
($t:ty) => {{
let v: $t = fx(&xml)
.map_err(|e| FlatError::Conversion(format!("TDD {}: {e}", stringify!($t))))?;
openehr_its::json::to_canonical_value(&v)
}};
}
let value = match resolved {
"DV_TEXT" => p!(DvText),
"DV_PARAGRAPH" => p!(DvParagraph),
"DV_CODED_TEXT" => p!(DvCodedText),
"DV_STATE" => p!(DvState),
"CODE_PHRASE" => p!(CodePhrase),
"DV_DATE_TIME" => p!(DvDateTime),
"DV_DATE" => p!(DvDate),
"DV_TIME" => p!(DvTime),
"DV_DURATION" => p!(DvDuration),
"DV_QUANTITY" => p!(DvQuantity),
"DV_COUNT" => p!(DvCount),
"DV_PROPORTION" => p!(DvProportion),
"DV_ORDINAL" => p!(DvOrdinal),
"DV_SCALE" => p!(DvScale),
"DV_BOOLEAN" => p!(DvBoolean),
"DV_IDENTIFIER" => p!(DvIdentifier),
"DV_MULTIMEDIA" => p!(DvMultimedia),
"DV_URI" => p!(DvUri),
"DV_EHR_URI" => p!(DvEhrUri),
"DV_PARSABLE" => p!(DvParsable),
"PARTY_PROXY" | "PARTY_IDENTIFIED" | "PARTY_SELF" | "PARTY_RELATED" => p!(PartyProxy),
"PARTICIPATION" => p!(Participation),
"EVENT_CONTEXT" => p!(EventContext),
"LINK" => p!(Link),
"ISM_TRANSITION" => p!(IsmTransition),
"FEEDER_AUDIT" => p!(FeederAudit),
"UID_BASED_ID" | "HIER_OBJECT_ID" | "OBJECT_VERSION_ID" => p!(UidBasedId),
_ => p!(DataValue),
};
Ok(value)
}
enum Simple {
Typed(&'static str),
Text,
}
fn simple_attr(rm_type: &str, attr: &str) -> Option<Simple> {
use Simple::{Text, Typed};
let comp = rm_type == "COMPOSITION";
let entry = entry_family(rm_type);
Some(match attr {
"name" if comp || entry || matches!(rm_type, "SECTION" | "CLUSTER" | "ACTIVITY") => {
Typed("DV_TEXT")
}
"language" if comp || entry => Typed("CODE_PHRASE"),
"territory" if comp => Typed("CODE_PHRASE"),
"composer" if comp => Typed("PARTY_PROXY"),
"context" if comp => Typed("EVENT_CONTEXT"),
"links" if comp => Typed("LINK"),
"encoding" if entry => Typed("CODE_PHRASE"),
"subject" | "provider" if entry => Typed("PARTY_PROXY"),
"other_participations" if entry => Typed("PARTICIPATION"),
"narrative" if entry => Typed("DV_TEXT"),
"expiry_time" | "time" if entry => Typed("DV_DATE_TIME"),
"ism_transition" if entry => Typed("ISM_TRANSITION"),
"timing" if rm_type == "ACTIVITY" => Typed("DV_PARSABLE"),
"action_archetype_id" if rm_type == "ACTIVITY" => Text,
_ => return None,
})
}
fn is_multi_simple(attr: &str) -> bool {
matches!(attr, "links" | "other_participations")
}
fn entry_family(rm_type: &str) -> bool {
matches!(
rm_type,
"OBSERVATION" | "EVALUATION" | "INSTRUCTION" | "ACTION" | "ADMIN_ENTRY" | "GENERIC_ENTRY"
)
}
fn is_locatable(rm_type: &str) -> bool {
!(rm_type.starts_with("DV_")
|| matches!(
rm_type,
"CODE_PHRASE" | "EVENT_CONTEXT" | "ISM_TRANSITION" | "PARTY_PROXY" | "PARTICIPATION"
))
}
pub fn from_tdd(tdd_xml: &str, wt: &WebTemplate) -> Result<Value, FlatError> {
let root_el = parse_tree(tdd_xml)?;
let root_id = &wt.tree.id;
if !name_matches(&root_el.name, node_display(&wt.tree)) {
return Err(FlatError::Conversion(format!(
"TDD root element <{}> does not match template root {:?} ({})",
root_el.name, root_id, wt.template_id
)));
}
check_conformance(&root_el, &wt.tree, "COMPOSITION")?;
let mut comp = build_node(&root_el, &wt.tree, "COMPOSITION", "", true)?;
if let Value::Object(m) = &mut comp {
ensure_template_id(m, &wt.tree, &wt.template_id);
ensure_category(m);
}
complete_tree(&mut comp);
Ok(comp)
}
const WRAPPER_METADATA: [&str; 6] = ["name", "uid", "links", "feeder_audit", "origin", "time"];
fn check_conformance(el: &El, wt: &WebTemplateNode, rm_type: &str) -> Result<(), FlatError> {
for (child, role) in el.children.iter().zip(classify_children(el, wt, rm_type)) {
match role {
ChildRole::Matched(wc) if !wc.has_input() => {
check_conformance(child, wc, concrete_type(&wc.rm_type))?;
}
ChildRole::Wrapper => check_conformance(child, wt, rm_type)?,
ChildRole::Unmatched if !WRAPPER_METADATA.contains(&child.name.as_str()) => {
return Err(FlatError::Conversion(format!(
"TDD element <{}> (under <{}>) matches no node of the operational template — \
the document does not conform to the template-derived TDS",
child.name, el.name
)));
}
ChildRole::Simple | ChildRole::Matched(_) | ChildRole::Unmatched => {}
}
}
Ok(())
}
enum ChildRole<'a> {
Simple,
Matched(&'a WebTemplateNode),
Wrapper,
Unmatched,
}
fn classify_children<'a>(el: &El, wt: &'a WebTemplateNode, rm_type: &str) -> Vec<ChildRole<'a>> {
let matches: Vec<(usize, bool, &WebTemplateNode)> = wt
.children
.iter()
.flat_map(|wc| {
match_indices(el, node_display(wc))
.into_iter()
.map(move |(idx, is_direct)| (idx, is_direct, wc))
})
.collect();
el.children
.iter()
.enumerate()
.map(|(idx, c)| {
if simple_attr(rm_type, &c.name).is_some() {
return ChildRole::Simple;
}
if let Some((_, _, wc)) = matches.iter().rev().find(|(i, d, _)| *i == idx && *d) {
return ChildRole::Matched(wc);
}
if matches.iter().any(|(i, d, _)| *i == idx && !*d) {
return ChildRole::Wrapper;
}
ChildRole::Unmatched
})
.collect()
}
fn match_indices(parent: &El, display: &str) -> Vec<(usize, bool)> {
let mut out = Vec::new();
for (idx, c) in parent.children.iter().enumerate() {
if name_matches(&c.name, display) {
out.push((idx, true));
} else if !find_matches(c, display).is_empty() {
out.push((idx, false));
}
}
out
}
fn build_node(
el: &El,
wt: &WebTemplateNode,
rm_type: &str,
path: &str,
is_root: bool,
) -> Result<Value, FlatError> {
let mut obj = Map::new();
obj.insert("_type".into(), json!(rm_type));
let node_id = if is_root {
wt.node_id.clone()
} else {
last_node_id_str(path)
.map(str::to_owned)
.or_else(|| wt.node_id.clone())
};
if is_locatable(rm_type)
&& let Some(nid) = &node_id
{
obj.insert("archetype_node_id".into(), json!(nid));
if is_archetype_root_node_id(nid) {
obj.insert("archetype_details".into(), archetyped(nid, None));
}
}
build_simple_attrs(el, rm_type, &mut obj)?;
build_content_children(el, wt, rm_type, path, &mut obj)?;
Ok(Value::Object(obj))
}
fn build_simple_attrs(
el: &El,
rm_type: &str,
obj: &mut Map<String, Value>,
) -> Result<(), FlatError> {
for child in &el.children {
let Some(hint) = simple_attr(rm_type, &child.name) else {
continue;
};
match hint {
Simple::Text => {
obj.insert(child.name.clone(), json!(child.text));
}
Simple::Typed(ty) if is_multi_simple(&child.name) => {
let arr = obj
.entry(child.name.clone())
.or_insert_with(|| json!([]))
.as_array_mut();
if let Some(arr) = arr {
arr.push(parse_typed(child, ty)?);
}
}
Simple::Typed(ty) => {
if !obj.contains_key(&child.name) {
obj.insert(child.name.clone(), parse_typed(child, ty)?);
}
}
}
}
Ok(())
}
fn build_content_children(
el: &El,
wt: &WebTemplateNode,
rm_type: &str,
path: &str,
obj: &mut Map<String, Value>,
) -> Result<(), FlatError> {
for wc in &wt.children {
let rel = rmpath::relative(path, &wc.aql_path);
if rel.is_empty() {
continue;
}
if (wc.in_context == Some(true) || wc.rm_type == "EVENT_CONTEXT")
&& rel
.last()
.is_some_and(|s| simple_attr(rm_type, &s.attribute).is_some())
{
continue;
}
for (wrappers, cel) in find_matches_with_wrappers(el, node_display(wc)) {
let child_value = if wc.has_input() {
leaf_value(cel, wc)?
} else {
build_node(cel, wc, concrete_type(&wc.rm_type), &wc.aql_path, false)?
};
let mut metas = WrapperMeta::carriers(&wrappers);
place(obj, &rel, child_value, wc, &mut metas)?;
if let Some(unused) = metas.front() {
return Err(FlatError::Conversion(format!(
"TDD wrapper <{}> carries instance data ({}) that corresponds to no \
re-materialised node on the path to {:?}",
unused.el.name,
unused.keys.join(", "),
wc.id
)));
}
}
}
Ok(())
}
fn leaf_value(el: &El, wc: &WebTemplateNode) -> Result<Value, FlatError> {
let dv_type = concrete_type(&wc.rm_type);
let frag = if wc.aql_path.ends_with("/value") {
el.child("value").unwrap_or(el)
} else {
el
};
parse_typed(frag, dv_type)
}
fn is_multiple(attr: &str) -> bool {
is_multiple_attr(attr)
}
static MULTIVALUED_ATTRS: LazyLock<HashSet<&str>> = LazyLock::new(|| {
let mut set: HashSet<&'static str> = HashSet::new();
let mut seen: HashSet<&'static str> = HashSet::new();
let mut queue: Vec<&'static str> = vec!["COMPOSITION", "EHR_STATUS", "FOLDER"];
while let Some(cls) = queue.pop() {
if !seen.insert(cls) {
continue;
}
for attr in openehr_rm::v1_2::model::attributes(cls) {
if openehr_rm::v1_2::model::class(attr.declared_type).is_none() {
continue;
}
if attr.container != openehr_rm::v1_2::model::Container::None {
set.insert(attr.name);
}
queue.push(attr.declared_type);
queue.extend(
openehr_rm::v1_2::model::descendants(attr.declared_type)
.iter()
.copied(),
);
}
}
set
});
pub(crate) fn is_multiple_attr(attr: &str) -> bool {
MULTIVALUED_ATTRS.contains(attr)
}
struct WrapperMeta<'a> {
el: &'a El,
keys: Vec<&'a str>,
}
impl<'a> WrapperMeta<'a> {
fn carriers(wrappers: &[&'a El]) -> VecDeque<WrapperMeta<'a>> {
wrappers
.iter()
.filter_map(|w| {
let mut keys: Vec<&str> = w
.children
.iter()
.map(|c| c.name.as_str())
.filter(|n| WRAPPER_METADATA.contains(n))
.collect();
keys.dedup();
if keys.is_empty() {
None
} else {
Some(WrapperMeta { el: w, keys })
}
})
.collect()
}
fn corresponds(&self, attribute: &str, rm_type: &str) -> bool {
if name_matches(&self.el.name, attribute) {
return true;
}
if let Some((_, suffix)) = self.el.name.rsplit_once("_as_")
&& suffix.eq_ignore_ascii_case(rm_type)
{
return true;
}
(self.keys.contains(&"origin") || self.keys.contains(&"time")) && self.fits(rm_type)
}
fn fits(&self, rm_type: &str) -> bool {
self.keys
.iter()
.all(|k| carried_attribute(rm_type, k).is_some())
}
fn apply(&self, rm_type: &str, obj: &mut Map<String, Value>) -> Result<(), FlatError> {
for key in &self.keys {
let Some(attr) = carried_attribute(rm_type, key) else {
return Err(FlatError::Conversion(format!(
"TDD wrapper <{}> spells out `{key}`, which is not an attribute of the \
re-materialised {rm_type}",
self.el.name
)));
};
let children = self.el.children.iter().filter(|c| c.name == **key);
if attr.container == openehr_rm::v1_2::model::Container::None {
if let Some(c) = children.into_iter().next() {
obj.insert((*key).to_owned(), parse_typed(c, attr.declared_type)?);
}
} else {
let mut arr = Vec::new();
for c in children {
arr.push(parse_typed(c, attr.declared_type)?);
}
obj.insert((*key).to_owned(), Value::Array(arr));
}
}
Ok(())
}
}
fn carried_attribute(
rm_type: &str,
attr: &str,
) -> Option<&'static openehr_rm::v1_2::model::RmAttribute> {
openehr_rm::v1_2::model::attribute(rm_type, attr).or_else(|| {
openehr_rm::v1_2::model::ancestors(rm_type)
.iter()
.find_map(|a| openehr_rm::v1_2::model::attribute(a, attr))
})
}
fn take_corresponding<'a>(
attribute: &str,
rm_type: &str,
metas: &mut VecDeque<WrapperMeta<'a>>,
) -> Option<WrapperMeta<'a>> {
if metas
.front()
.is_some_and(|m| m.corresponds(attribute, rm_type))
{
metas.pop_front()
} else {
None
}
}
fn discard_corresponding(
existing: Option<&Value>,
attribute: &str,
metas: &mut VecDeque<WrapperMeta<'_>>,
) {
if let Some(ty) = existing
.and_then(|v| v.get("_type"))
.and_then(Value::as_str)
{
let _consumed = take_corresponding(attribute, ty, metas);
}
}
fn place(
parent: &mut Map<String, Value>,
rel: &[PathSegment],
child_value: Value,
wc: &WebTemplateNode,
metas: &mut VecDeque<WrapperMeta<'_>>,
) -> Result<(), FlatError> {
if rel.is_empty() {
return Ok(());
}
let id_idx = rel.iter().rposition(|s| is_multiple(&s.attribute));
place_rec(parent, rel, 0, id_idx, child_value, wc, metas)
}
#[expect(
clippy::indexing_slicing,
reason = "`i` is a cursor into `rel` that the recursion only advances while `i + 1 < rel.len()` (the `last` guard), so `rel[i]` and `rel[i + 1..]` are in bounds at every call"
)]
fn place_rec(
cur: &mut Map<String, Value>,
rel: &[PathSegment],
i: usize,
id_idx: Option<usize>,
child_value: Value,
wc: &WebTemplateNode,
metas: &mut VecDeque<WrapperMeta<'_>>,
) -> Result<(), FlatError> {
let seg = &rel[i];
let node_id = seg.predicate.archetype_node_id.as_deref();
let last = i + 1 == rel.len();
if Some(i) == id_idx {
let mut entry = if last {
let mut el = child_value;
set_node_id(&mut el, node_id);
el
} else {
let mut el = new_struct(seg, rel.get(i + 1), wc.name.as_deref(), metas)?;
if let Value::Object(m) = &mut el {
place(m, &rel[i + 1..], child_value, wc, metas)?;
}
el
};
if let Some(arr) = cur
.entry(seg.attribute.clone())
.or_insert_with(|| json!([]))
.as_array_mut()
{
arr.push(std::mem::take(&mut entry));
}
return Ok(());
}
if is_multiple(&seg.attribute) {
let created = {
let Some(arr) = cur
.entry(seg.attribute.clone())
.or_insert_with(|| json!([]))
.as_array_mut()
else {
return Ok(());
};
arr.iter()
.position(|e| e.get("archetype_node_id").and_then(Value::as_str) == node_id)
};
let idx = if let Some(pos) = created {
pos
} else {
let fresh = new_struct(seg, rel.get(i + 1), None, metas)?;
let Some(arr) = cur.get_mut(&seg.attribute).and_then(Value::as_array_mut) else {
return Ok(());
};
arr.push(fresh);
arr.len() - 1
};
let Some(arr) = cur.get_mut(&seg.attribute).and_then(Value::as_array_mut) else {
return Ok(());
};
if created.is_some() {
discard_corresponding(arr.get(idx), &seg.attribute, metas);
}
if let Some(Value::Object(m)) = arr.get_mut(idx) {
place_rec(m, rel, i + 1, id_idx, child_value, wc, metas)?;
}
return Ok(());
}
if last {
cur.insert(seg.attribute.clone(), child_value);
return Ok(());
}
if cur.contains_key(&seg.attribute) {
discard_corresponding(cur.get(&seg.attribute), &seg.attribute, metas);
} else {
let fresh = new_struct(seg, rel.get(i + 1), None, metas)?;
cur.insert(seg.attribute.clone(), fresh);
}
if let Some(Value::Object(m)) = cur.get_mut(&seg.attribute) {
place_rec(m, rel, i + 1, id_idx, child_value, wc, metas)?;
}
Ok(())
}
fn new_struct(
seg: &PathSegment,
next: Option<&PathSegment>,
name: Option<&str>,
metas: &mut VecDeque<WrapperMeta<'_>>,
) -> Result<Value, FlatError> {
let rm_type = infer_type(&seg.attribute, next.map(|s| s.attribute.as_str()));
let node_id = seg.predicate.archetype_node_id.as_deref();
let mut o = Map::new();
o.insert("_type".into(), json!(rm_type));
let display = name.or(node_id).unwrap_or(rm_type).to_owned();
o.insert("name".into(), json!({"_type": "DV_TEXT", "value": display}));
if let Some(nid) = node_id {
o.insert("archetype_node_id".into(), json!(nid));
if is_archetype_root_node_id(nid) {
o.insert("archetype_details".into(), archetyped(nid, None));
}
}
match rm_type {
"HISTORY" => {
o.insert(
"origin".into(),
json!({"_type": "DV_DATE_TIME", "value": DEFAULT_TIME}),
);
o.insert("events".into(), json!([]));
}
"POINT_EVENT" | "EVENT" | "INTERVAL_EVENT" => {
o.insert(
"time".into(),
json!({"_type": "DV_DATE_TIME", "value": DEFAULT_TIME}),
);
}
"ITEM_TREE" | "ITEM_LIST" | "ITEM_SINGLE" | "ITEM_TABLE" | "CLUSTER" => {
o.insert("items".into(), json!([]));
}
_ => {}
}
if let Some(meta) = take_corresponding(&seg.attribute, rm_type, metas) {
meta.apply(rm_type, &mut o)?;
}
Ok(Value::Object(o))
}
fn infer_type(attr: &str, next: Option<&str>) -> &'static str {
match (attr, next) {
("data", Some("events")) => "HISTORY",
("events", _) => "POINT_EVENT",
("items", _) => "ELEMENT",
("activities", _) => "ACTIVITY",
_ => "ITEM_TREE",
}
}
fn set_node_id(v: &mut Value, node_id: Option<&str>) {
if let (Value::Object(m), Some(nid)) = (v, node_id) {
m.entry("archetype_node_id".to_owned())
.or_insert_with(|| json!(nid));
}
}
fn complete_tree(v: &mut Value) {
match v {
Value::Object(m) => {
if let Some(ty) = m.get("_type").and_then(Value::as_str).map(str::to_owned) {
fill_structural_mandatory(m, &ty);
}
for child in m.values_mut() {
complete_tree(child);
}
}
Value::Array(a) => a.iter_mut().for_each(complete_tree),
_ => {}
}
}
fn fill_structural_mandatory(obj: &mut Map<String, Value>, rm_type: &str) {
let dt = || json!({"_type": "DV_DATE_TIME", "value": DEFAULT_TIME});
let empty_tree = || json!({"_type": "ITEM_TREE", "name": {"_type": "DV_TEXT", "value": "Tree"}, "items": []});
match rm_type {
"HISTORY" => {
obj.entry("origin".to_owned()).or_insert_with(dt);
obj.entry("events".to_owned()).or_insert_with(|| json!([]));
}
"POINT_EVENT" | "EVENT" => {
obj.entry("time".to_owned()).or_insert_with(dt);
obj.entry("data".to_owned()).or_insert_with(empty_tree);
}
"INTERVAL_EVENT" => {
obj.entry("time".to_owned()).or_insert_with(dt);
obj.entry("data".to_owned()).or_insert_with(empty_tree);
obj.entry("width".to_owned())
.or_insert_with(|| json!({"_type": "DV_DURATION", "value": "P0D"}));
obj.entry("math_function".to_owned()).or_insert_with(|| {
json!({"_type": "DV_CODED_TEXT", "value": "mean",
"defining_code": code_phrase("openehr", "146")})
});
}
"ITEM_TREE" | "ITEM_LIST" | "ITEM_SINGLE" | "ITEM_TABLE" | "CLUSTER" => {
obj.entry("items".to_owned()).or_insert_with(|| json!([]));
}
"ACTIVITY" => {
obj.entry("action_archetype_id".to_owned())
.or_insert_with(|| json!("/.*/"));
obj.entry("description".to_owned())
.or_insert_with(empty_tree);
}
"ISM_TRANSITION" => {
obj.entry("current_state".to_owned())
.or_insert_with(|| dv_coded_text("initial", "openehr", "524"));
}
_ => {}
}
}
fn code_phrase(terminology: &str, code: &str) -> Value {
json!({
"_type": "CODE_PHRASE",
"terminology_id": {"_type": "TERMINOLOGY_ID", "value": terminology},
"code_string": code,
})
}
fn dv_coded_text(value: &str, terminology: &str, code: &str) -> Value {
json!({"_type": "DV_CODED_TEXT", "value": value, "defining_code": code_phrase(terminology, code)})
}
fn archetyped(archetype_id: &str, template_id: Option<&str>) -> Value {
let mut a = Map::new();
a.insert("_type".into(), json!("ARCHETYPED"));
a.insert(
"archetype_id".into(),
json!({"_type": "ARCHETYPE_ID", "value": archetype_id}),
);
if let Some(t) = template_id {
a.insert(
"template_id".into(),
json!({"_type": "TEMPLATE_ID", "value": t}),
);
}
a.insert("rm_version".into(), json!(RM_VERSION));
Value::Object(a)
}
fn ensure_template_id(comp: &mut Map<String, Value>, root: &WebTemplateNode, template_id: &str) {
let ad = comp
.entry("archetype_details".to_owned())
.or_insert_with(|| {
json!({"_type": "ARCHETYPED",
"archetype_id": {"_type": "ARCHETYPE_ID", "value": root.node_id},
"rm_version": RM_VERSION})
});
if let Value::Object(ad) = ad {
ad.entry("template_id".to_owned())
.or_insert_with(|| json!({"_type": "TEMPLATE_ID", "value": template_id}));
}
}
fn ensure_category(comp: &mut Map<String, Value>) {
comp.entry("category".to_owned())
.or_insert_with(|| dv_coded_text("event", "openehr", "433"));
}
fn node_display(wc: &WebTemplateNode) -> &str {
wc.name.as_deref().unwrap_or(&wc.id)
}
fn name_matches(el_name: &str, display: &str) -> bool {
let base = el_name.split("_as_").next().unwrap_or(el_name);
normalise(base) == normalise(display)
}
fn normalise(s: &str) -> String {
s.replace(' ', "_")
}
fn find_matches<'a>(parent: &'a El, display: &str) -> Vec<&'a El> {
find_matches_with_wrappers(parent, display)
.into_iter()
.map(|(_, el)| el)
.collect()
}
fn find_matches_with_wrappers<'a>(parent: &'a El, display: &str) -> Vec<(Vec<&'a El>, &'a El)> {
fn walk<'a>(
parent: &'a El,
display: &str,
chain: &mut Vec<&'a El>,
out: &mut Vec<(Vec<&'a El>, &'a El)>,
) {
for c in &parent.children {
if name_matches(&c.name, display) {
out.push((chain.clone(), c));
} else {
chain.push(c);
walk(c, display, chain, out);
chain.pop();
}
}
}
let mut out = Vec::new();
walk(parent, display, &mut Vec::new(), &mut out);
out
}
fn concrete_type(rm_type: &str) -> &str {
match rm_type.split('<').next().unwrap_or(rm_type) {
"EVENT" => "POINT_EVENT",
other => other,
}
}
fn last_node_id_str(path: &str) -> Option<&str> {
let close = path.rfind(']')?;
let open = path.get(..close)?.rfind('[')?;
path.get(open + 1..close)
}
#[cfg(test)]
mod multiplicity_tests {
use super::is_multiple_attr;
#[test]
fn covers_legacy_hardcoded_set() {
for attr in ["content", "items", "events", "activities"] {
assert!(
is_multiple_attr(attr),
"model-driven multiplicity set must include the legacy member `{attr}`",
);
}
}
#[test]
fn data_stays_single() {
assert!(
!is_multiple_attr("data"),
"`data` must not be treated as multi-valued (OBSERVATION.data is a single HISTORY)",
);
}
#[test]
fn covers_reachable_multi_valued_attributes() {
assert!(is_multiple_attr("other_participations"));
}
#[test]
fn rejects_single_and_unknown() {
assert!(!is_multiple_attr("value"));
assert!(!is_multiple_attr("not_a_real_attribute"));
}
}