#![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 openehr_rm::v1_2::paths::PathSegment;
use serde_json::{Map, Value, json};
use crate::flat::ctx;
use crate::flat::error::FlatError;
use crate::flat::map;
use crate::flat::rmpath;
use crate::flat::sim::{SimDocument, SimNode};
use crate::flat::webtemplate::model::{CodedName, WebTemplate, WebTemplateNode};
pub(crate) const RM_VERSION: &str = "1.2.0";
pub(crate) const DEFAULT_TIME: &str = "1970-01-01T00:00:00Z";
pub fn build_composition(
doc: &SimDocument,
wt: &WebTemplate,
now: &str,
) -> Result<Value, FlatError> {
let root_id = &wt.tree.id;
let mut data_root = None;
let mut ctx_node = None;
for (name, child) in &doc.children {
match name.as_str() {
"ctx" => ctx_node = child.occurrences.first(),
n if n == root_id => data_root = child.occurrences.first(),
other => return Err(FlatError::UnknownPath(other.to_owned())),
}
}
if data_root.is_none() && ctx_node.is_none() {
return Err(FlatError::Conversion(
"empty simplified document: no data root and no ctx".to_owned(),
));
}
let empty = SimNode::default();
let data_root = data_root.unwrap_or(&empty);
let merged_ctx = merge_root_ctx_spellings(ctx_node, data_root);
let explicit_event_context = has_explicit_event_context(merged_ctx.as_ref());
let defaults = ctx::resolve(merged_ctx.as_ref(), now)?;
let mut slot_types: Vec<(Vec<PathSegment>, String)> = Vec::new();
collect_slot_types(&wt.tree, &mut slot_types);
let mut comp = match build(&wt.tree, data_root, root_id, &slot_types)? {
Some(Value::Object(m)) => m,
_ => Map::new(),
};
finish_identity(&mut comp, &wt.tree, true, &wt.template_id);
ensure_template_id(&mut comp, &wt.tree, &wt.template_id);
ensure_category(&mut comp);
apply_composition_ctx(&mut comp, &defaults, explicit_event_context);
let mut value = Value::Object(comp);
apply_entry_defaults(&mut value, &defaults);
complete_tree(&mut value);
Ok(value)
}
fn merge_root_ctx_spellings(ctx_node: Option<&SimNode>, data_root: &SimNode) -> Option<SimNode> {
let mut merged = ctx_node.cloned().unwrap_or_default();
let mut fill = |key: &str, value: Option<&Value>| {
if let Some(v) = value
&& !merged.children.contains_key(key)
{
merged
.occurrence_mut(key, None)
.attrs
.insert(String::new(), v.clone());
}
};
let attr = |name: &str, suffix: &str| data_root.child(name).and_then(|n| n.attrs.get(suffix));
fill("language", attr("language", "code"));
fill("territory", attr("territory", "code"));
fill("composer_name", attr("composer", "name"));
fill("composer_id", attr("composer", "id"));
fill("id_namespace", attr("composer", "id_namespace"));
fill("id_scheme", attr("composer", "id_scheme"));
if merged.is_empty() {
None
} else {
Some(merged)
}
}
fn has_explicit_event_context(ctx_node: Option<&SimNode>) -> bool {
let Some(ctx) = ctx_node else { return false };
ctx.children.keys().any(|key| {
matches!(
key.as_str(),
"time" | "end_time" | "setting" | "location" | "health_care_facility"
) || key.starts_with("participation_")
})
}
fn build_raw(raw: &Value, path: &str) -> Result<Value, FlatError> {
if raw.get("_type").and_then(Value::as_str).is_none() {
return Err(FlatError::InvalidRaw {
path: path.to_owned(),
reason: "the |raw value must be a canonical-JSON object with _type".to_owned(),
});
}
Ok(raw.clone())
}
fn is_element_level(seg: &str) -> bool {
matches!(
seg,
"_uid" | "_null_flavour" | "_null_reason" | "_link" | "_feeder_audit"
)
}
fn leaf_consumes_segment(base: &str, seg: &str) -> bool {
matches!(
seg,
map::parties::RELATIONSHIP_TABLE_SEGMENT | map::parties::IDENTIFIER_SEGMENT
) && matches!(
base,
"PARTY_PROXY" | "PARTY_SELF" | "PARTY_IDENTIFIED" | "PARTY_RELATED"
)
}
fn collect_slot_types(node: &WebTemplateNode, out: &mut Vec<(Vec<PathSegment>, String)>) {
for slot in &node.slots {
out.push((rmpath::parse(&slot.path), slot.rm_type.clone()));
}
for child in &node.children {
collect_slot_types(child, out);
}
}
fn segments_match(a: &[PathSegment], b: &[PathSegment]) -> bool {
a.len() == b.len()
&& a.iter().zip(b).all(|(x, y)| {
x.attribute == y.attribute
&& x.predicate.archetype_node_id == y.predicate.archetype_node_id
})
}
fn wrapper_types(
child_aql: &str,
rel_len: usize,
slots: &[(Vec<PathSegment>, String)],
) -> Vec<Option<String>> {
let child_abs = rmpath::parse(child_aql);
if child_abs.len() < rel_len {
return vec![None; rel_len];
}
let base = child_abs.len() - rel_len;
(0..rel_len)
.map(|i| {
let abs = child_abs.get(..=base + i)?;
slots
.iter()
.find(|(segs, _)| segments_match(segs, abs))
.map(|(_, ty)| ty.clone())
})
.collect()
}
fn build(
node: &WebTemplateNode,
sim: &SimNode,
path: &str,
slots: &[(Vec<PathSegment>, String)],
) -> Result<Option<Value>, FlatError> {
if node.has_input() {
return build_leaf(node, sim, path);
}
let mut obj = Map::new();
obj.insert("_type".into(), json!(concrete_type(&node.rm_type)));
let mut history_origin: Option<String> = None;
walk_template_children(node, sim, path, slots, &mut obj)?;
apply_container_attrs(node, sim, path, &mut obj, &mut history_origin)?;
if let Some(raw) = sim.attrs.get("raw") {
return build_raw(raw, path).map(Some);
}
apply_container_datum_attrs(node, sim, path, &mut obj)?;
let mut value = Value::Object(obj);
if let Value::Object(m) = &mut value {
finish_identity(m, node, false, "");
if let Some(ts) = &history_origin
&& let Some(Value::Object(hist)) = m.get_mut("data")
{
hist.insert("origin".to_owned(), dv_date_time(ts));
}
apply_interval_flags(m, &node.rm_type);
}
Ok(Some(value))
}
fn build_leaf(
node: &WebTemplateNode,
sim: &SimNode,
path: &str,
) -> Result<Option<Value>, FlatError> {
if is_null_flavoured(sim) {
return Ok(None);
}
let base = base_type(&node.rm_type);
let mut dv = map::build_leaf(sim, base, Some(node), path)?;
if let Value::Object(m) = &mut dv {
for (seg, child) in &sim.children {
if seg.starts_with('_') && !is_element_level(seg) && !leaf_consumes_segment(base, seg) {
apply_rm_attr(m, seg, &child.occurrences, base, path)?;
}
}
}
Ok(Some(dv))
}
fn walk_template_children(
node: &WebTemplateNode,
sim: &SimNode,
path: &str,
slots: &[(Vec<PathSegment>, String)],
obj: &mut Map<String, Value>,
) -> Result<(), FlatError> {
for child in &node.children {
if !child_is_walked(node, child) {
continue;
}
let Some(sim_child) = sim.children.get(&child.id).or_else(|| {
child
.alt_json_id
.as_deref()
.and_then(|a| sim.children.get(a))
}) else {
continue;
};
let rel = rmpath::relative(&node.aql_path, &child.aql_path);
for (i, occ) in sim_child.occurrences.iter().enumerate() {
if occ.is_empty() {
continue; }
let child_path = format!("{path}/{}:{i}", child.id);
let child_val = build(child, occ, &child_path, slots)?;
place(obj, &rel, child_val, child, occ, &child_path, slots)?;
}
}
Ok(())
}
fn apply_container_attrs(
node: &WebTemplateNode,
sim: &SimNode,
path: &str,
obj: &mut Map<String, Value>,
history_origin: &mut Option<String>,
) -> Result<(), FlatError> {
for (seg, child) in &sim.children {
if seg.starts_with('_') {
apply_rm_attr(obj, seg, &child.occurrences, base_type(&node.rm_type), path)?;
continue;
}
let known = node.children.iter().any(|c| {
(c.id == *seg || c.alt_json_id.as_deref() == Some(seg)) && child_is_walked(node, c)
});
let ctx_covered = node.rm_type == "COMPOSITION"
&& matches!(
seg.as_str(),
"language" | "territory" | "composer" | "category"
);
if known || ctx_covered {
continue;
}
if place_direct_rm_path(
obj,
&node.rm_type,
seg,
&child.occurrences,
path,
history_origin,
)? {
continue;
}
return Err(FlatError::UnknownPath(format!("{path}/{seg}")));
}
Ok(())
}
fn is_null_flavoured(sim: &SimNode) -> bool {
sim.attrs.is_empty()
&& sim
.children
.get("_null_flavour")
.is_some_and(|c| c.occurrences.iter().any(|o| !o.is_empty()))
}
fn child_is_walked(node: &WebTemplateNode, child: &WebTemplateNode) -> bool {
if node.rm_type == "EVENT_CONTEXT" && !child.aql_path.contains("other_context") {
return false;
}
if node.rm_type == "COMPOSITION"
&& matches!(child.id.as_str(), "language" | "territory" | "composer")
{
return false;
}
if is_entry_family(&node.rm_type) && matches!(child.id.as_str(), "language" | "encoding") {
return false;
}
true
}
fn apply_interval_flags(m: &mut Map<String, Value>, rm_type: &str) {
if base_type(rm_type) != "DV_INTERVAL" {
return;
}
let has_lower = m.contains_key("lower");
let has_upper = m.contains_key("upper");
m.entry("lower_unbounded").or_insert(json!(!has_lower));
m.entry("upper_unbounded").or_insert(json!(!has_upper));
m.entry("lower_included").or_insert(json!(has_lower));
m.entry("upper_included").or_insert(json!(has_upper));
}
enum DirectPath {
Leaf {
attr: &'static str,
rm_type: &'static str,
},
Ism,
CodedGroup {
attr: &'static str,
group: &'static str,
},
ActionArchetypeId,
HistoryOrigin,
}
fn direct_rm_path(host: &str, seg: &str) -> Option<DirectPath> {
use DirectPath::{ActionArchetypeId, CodedGroup, HistoryOrigin, Ism, Leaf};
Some(match (host, seg) {
(_, "language") if is_entry_family(host) => Leaf {
attr: "language",
rm_type: "CODE_PHRASE",
},
(_, "encoding") if is_entry_family(host) => Leaf {
attr: "encoding",
rm_type: "CODE_PHRASE",
},
("ACTION" | "POINT_EVENT" | "INTERVAL_EVENT" | "EVENT", "time") => Leaf {
attr: "time",
rm_type: "DV_DATE_TIME",
},
("ACTION", "ism_transition") => Ism,
("ISM_TRANSITION", "current_state") => CodedGroup {
attr: "current_state",
group: "instruction_states",
},
("ISM_TRANSITION", "transition") => CodedGroup {
attr: "transition",
group: "instruction_transitions",
},
("ISM_TRANSITION", "careflow_step") => Leaf {
attr: "careflow_step",
rm_type: "DV_CODED_TEXT",
},
("INSTRUCTION", "narrative") => Leaf {
attr: "narrative",
rm_type: "DV_TEXT",
},
("OBSERVATION", "history_origin") => HistoryOrigin,
("ACTIVITY", "timing") => Leaf {
attr: "timing",
rm_type: "DV_PARSABLE",
},
("ACTIVITY", "action_archetype_id") => ActionArchetypeId,
("INTERVAL_EVENT", "width") => Leaf {
attr: "width",
rm_type: "DV_DURATION",
},
("INTERVAL_EVENT", "math_function") => Leaf {
attr: "math_function",
rm_type: "DV_CODED_TEXT",
},
("EVENT_CONTEXT", "start_time") => Leaf {
attr: "start_time",
rm_type: "DV_DATE_TIME",
},
("EVENT_CONTEXT", "setting") => CodedGroup {
attr: "setting",
group: "setting",
},
_ => return None,
})
}
fn apply_container_datum_attrs(
node: &WebTemplateNode,
sim: &SimNode,
path: &str,
obj: &mut Map<String, Value>,
) -> Result<(), FlatError> {
let host = base_type(&node.rm_type);
for (chain, datum) in &sim.attrs {
if let Some(attr) = container_datum_attr(host, chain) {
obj.insert(attr.to_owned(), datum.clone());
}
}
if let Some((chain, _)) = sim
.attrs
.iter()
.find(|(c, _)| !c.is_empty() && container_datum_attr(host, c.as_str()).is_none())
{
return Err(FlatError::UnknownSuffix {
rm_type: node.rm_type.clone(),
suffix: chain.clone(),
path: path.to_owned(),
});
}
Ok(())
}
fn container_datum_attr(host: &str, suffix: &str) -> Option<&'static str> {
match (host, suffix) {
("INTERVAL_EVENT", "sample_count") => Some("sample_count"),
_ => None,
}
}
fn place_direct_rm_path(
obj: &mut Map<String, Value>,
host_rm_type: &str,
seg: &str,
occurrences: &[SimNode],
path: &str,
history_origin: &mut Option<String>,
) -> Result<bool, FlatError> {
let Some(dp) = direct_rm_path(base_type(host_rm_type), seg) else {
return Ok(false);
};
let occ = occurrences.iter().find(|o| !o.is_empty());
let sub_path = format!("{path}/{seg}");
match dp {
DirectPath::Leaf { attr, rm_type } => {
if let Some(node) = occ
&& !obj.contains_key(attr)
{
let value = map::build_leaf(node, rm_type, None, &sub_path)?;
obj.insert(attr.to_owned(), value);
}
}
DirectPath::CodedGroup { attr, group } => {
if let Some(node) = occ
&& !obj.contains_key(attr)
{
let value = coded_group_value(node, group, &sub_path)?;
obj.insert(attr.to_owned(), value);
}
}
DirectPath::Ism => {
if let Some(node) = occ
&& !obj.contains_key("ism_transition")
{
let value = build_ism_transition(node, &sub_path)?;
obj.insert("ism_transition".to_owned(), value);
}
}
DirectPath::ActionArchetypeId => {
if let Some(v) = occ.and_then(SimNode::bare) {
obj.entry("action_archetype_id".to_owned())
.or_insert_with(|| v.clone());
}
}
DirectPath::HistoryOrigin => {
*history_origin = occ
.and_then(SimNode::bare)
.and_then(Value::as_str)
.map(str::to_owned);
}
}
Ok(true)
}
fn coded_group_value(node: &SimNode, group: &str, sub_path: &str) -> Result<Value, FlatError> {
let bound = node
.attrs
.get("code")
.and_then(Value::as_str)
.filter(|_| !node.attrs.contains_key("value") && !node.attrs.contains_key("terminology"))
.map(|code| map::coded_from_group(group, code));
match bound {
Some(v) => Ok(v),
None => map::build_leaf(node, "DV_CODED_TEXT", None, sub_path),
}
}
fn build_ism_transition(node: &SimNode, path: &str) -> Result<Value, FlatError> {
let mut o = Map::new();
o.insert("_type".into(), json!("ISM_TRANSITION"));
for (seg, group) in [
("current_state", Some("instruction_states")),
("transition", Some("instruction_transitions")),
("careflow_step", None),
] {
if let Some(cs) = node.child(seg).filter(|n| !n.is_empty()) {
o.insert(
seg.to_owned(),
build_ism_coded(cs, group, &format!("{path}/{seg}"))?,
);
}
}
for (seg, child) in &node.children {
if seg.starts_with('_') {
apply_rm_attr(&mut o, seg, &child.occurrences, "ISM_TRANSITION", path)?;
} else if !matches!(
seg.as_str(),
"current_state" | "transition" | "careflow_step"
) {
return Err(FlatError::UnknownPath(format!("{path}/{seg}")));
}
}
Ok(Value::Object(o))
}
fn build_ism_coded(node: &SimNode, group: Option<&str>, path: &str) -> Result<Value, FlatError> {
let has_value = node.attrs.contains_key("value");
let terminology = node.attrs.get("terminology").and_then(Value::as_str);
if !has_value
&& let Some(group) = group
&& let Some(code) = node.attrs.get("code").and_then(Value::as_str)
&& matches!(terminology, None | Some("openehr"))
{
return Ok(map::coded_from_group(group, code));
}
map::build_leaf(node, "DV_CODED_TEXT", None, path)
}
fn apply_rm_attr(
obj: &mut Map<String, Value>,
seg: &str,
occurrences: &[SimNode],
host_rm_type: &str,
path: &str,
) -> Result<(), FlatError> {
if let Some((attr, value)) = map::build_rm_attr(seg, occurrences, host_rm_type, path)? {
obj.insert(attr, value);
}
Ok(())
}
fn place(
parent: &mut Map<String, Value>,
rel: &[PathSegment],
child_value: Option<Value>,
child: &WebTemplateNode,
occ: &SimNode,
path: &str,
slots: &[(Vec<PathSegment>, String)],
) -> Result<(), FlatError> {
if rel.is_empty() {
return Ok(());
}
let types = wrapper_types(&child.aql_path, rel.len(), slots);
let placement = Placement {
rel,
id_idx: rel.iter().rposition(|s| is_multiple(&s.attribute)),
child,
occ,
path,
types: &types,
slots,
};
place_rec(parent, 0, child_value, &placement)
}
struct Placement<'a> {
rel: &'a [PathSegment],
id_idx: Option<usize>,
child: &'a WebTemplateNode,
occ: &'a SimNode,
path: &'a str,
types: &'a [Option<String>],
slots: &'a [(Vec<PathSegment>, String)],
}
#[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>,
i: usize,
child_value: Option<Value>,
p: &Placement<'_>,
) -> Result<(), FlatError> {
if Some(i) == p.id_idx {
return place_own_level(cur, i, child_value, p);
}
if is_multiple(&p.rel[i].attribute) {
return place_structural_level(cur, i, child_value, p);
}
place_single_attribute(cur, i, child_value, p)
}
#[expect(
clippy::indexing_slicing,
reason = "`i` indexes `rel` under the same cursor invariant as `place_rec`"
)]
fn place_own_level(
cur: &mut Map<String, Value>,
i: usize,
child_value: Option<Value>,
p: &Placement<'_>,
) -> Result<(), FlatError> {
let seg = &p.rel[i];
let Some(arr) = cur
.entry(seg.attribute.clone())
.or_insert_with(|| json!([]))
.as_array_mut()
else {
return Ok(());
};
if i + 1 == p.rel.len() {
let Some(mut el) = child_value else {
return Ok(());
};
set_node_id(&mut el, seg.predicate.archetype_node_id.as_deref());
arr.push(el);
return Ok(());
}
let mut el = new_struct(
seg,
p.rel.get(i + 1),
p.child.name.as_deref(),
p.child.name_coded.as_ref(),
p.types.get(i).and_then(Option::as_deref),
);
if let Value::Object(m) = &mut el {
place(
m,
&p.rel[i + 1..],
child_value,
p.child,
p.occ,
p.path,
p.slots,
)?;
apply_element_attrs(m, p.occ, p.path)?;
}
arr.push(el);
Ok(())
}
#[expect(
clippy::indexing_slicing,
reason = "`i` indexes `rel` under the same cursor invariant as `place_rec`"
)]
fn place_structural_level(
cur: &mut Map<String, Value>,
i: usize,
child_value: Option<Value>,
p: &Placement<'_>,
) -> Result<(), FlatError> {
let seg = &p.rel[i];
let node_id = seg.predicate.archetype_node_id.as_deref();
let Some(arr) = cur
.entry(seg.attribute.clone())
.or_insert_with(|| json!([]))
.as_array_mut()
else {
return Ok(());
};
let existing = arr
.iter()
.position(|e| e.get("archetype_node_id").and_then(Value::as_str) == node_id);
let idx = if let Some(pos) = existing {
pos
} else {
arr.push(new_struct(
seg,
p.rel.get(i + 1),
None,
None,
p.types.get(i).and_then(Option::as_deref),
));
arr.len() - 1
};
if let Some(Value::Object(m)) = arr.get_mut(idx) {
place_rec(m, i + 1, child_value, p)?;
}
Ok(())
}
#[expect(
clippy::indexing_slicing,
reason = "`i` indexes `rel` under the same cursor invariant as `place_rec`"
)]
fn place_single_attribute(
cur: &mut Map<String, Value>,
i: usize,
child_value: Option<Value>,
p: &Placement<'_>,
) -> Result<(), FlatError> {
let seg = &p.rel[i];
if i + 1 == p.rel.len() {
let wraps_element = seg.attribute == "value";
if let Some(child_value) = child_value {
cur.insert(seg.attribute.clone(), child_value);
}
if wraps_element {
apply_element_attrs(cur, p.occ, p.path)?;
}
return Ok(());
}
if !cur.contains_key(&seg.attribute) {
cur.insert(
seg.attribute.clone(),
new_struct(
seg,
p.rel.get(i + 1),
None,
None,
p.types.get(i).and_then(Option::as_deref),
),
);
}
if let Some(Value::Object(m)) = cur.get_mut(&seg.attribute) {
place_rec(m, i + 1, child_value, p)?;
}
Ok(())
}
fn apply_element_attrs(
element: &mut Map<String, Value>,
occ: &SimNode,
path: &str,
) -> Result<(), FlatError> {
for (seg, child) in &occ.children {
if is_element_level(seg) {
apply_rm_attr(element, seg, &child.occurrences, "ELEMENT", path)?;
}
}
Ok(())
}
fn is_multiple(attr: &str) -> bool {
crate::flat::tdd::is_multiple_attr(attr)
}
fn concrete_type(rm_type: &str) -> &str {
match base_type(rm_type) {
"EVENT" => "POINT_EVENT",
other => other,
}
}
fn concretize_structural(rm_type: &str) -> &str {
match rm_type {
"EVENT" => "POINT_EVENT",
"ITEM_STRUCTURE" => "ITEM_TREE",
other => other,
}
}
fn base_type(rm_type: &str) -> &str {
rm_type.split('<').next().unwrap_or(rm_type)
}
fn is_entry_family(rm_type: &str) -> bool {
matches!(
rm_type,
"OBSERVATION" | "EVALUATION" | "INSTRUCTION" | "ACTION" | "ADMIN_ENTRY" | "GENERIC_ENTRY"
)
}
fn name_value(display: &str, coded: Option<&CodedName>) -> Value {
match coded {
Some(CodedName {
terminology, code, ..
}) => json!({
"_type": "DV_CODED_TEXT",
"value": display,
"defining_code": {
"_type": "CODE_PHRASE",
"terminology_id": {"_type": "TERMINOLOGY_ID", "value": terminology},
"code_string": code,
},
}),
None => json!({"_type": "DV_TEXT", "value": display}),
}
}
fn new_struct(
seg: &PathSegment,
next: Option<&PathSegment>,
name: Option<&str>,
coded_name: Option<&CodedName>,
constrained: Option<&str>,
) -> Value {
let rm_type = constrained.map_or_else(
|| infer_type(&seg.attribute, next.map(|s| s.attribute.as_str())),
concretize_structural,
);
let node_id = seg.predicate.archetype_node_id.as_deref();
let mut o = Map::new();
o.insert("_type".into(), json!(rm_type));
if is_locatable(rm_type) {
let display = seg
.predicate
.name_value
.as_deref()
.or(name)
.or(node_id)
.unwrap_or(rm_type);
o.insert("name".into(), name_value(display, coded_name));
if let Some(nid) = node_id {
o.insert("archetype_node_id".into(), json!(nid));
}
}
match rm_type {
"HISTORY" => {
o.insert("origin".into(), dv_date_time(DEFAULT_TIME));
o.insert("events".into(), json!([]));
}
"POINT_EVENT" | "EVENT" | "INTERVAL_EVENT" => {
o.insert("time".into(), dv_date_time(DEFAULT_TIME));
}
"ITEM_TREE" | "ITEM_LIST" | "CLUSTER" => {
o.insert("items".into(), json!([]));
}
_ => {}
}
Value::Object(o)
}
fn infer_type(attr: &str, next: Option<&str>) -> &'static str {
match (attr, next) {
("data", Some("events")) => "HISTORY",
("events", _) => "POINT_EVENT",
("items" | "item", _) => "ELEMENT",
("activities", _) => "ACTIVITY",
_ => "ITEM_TREE",
}
}
pub(crate) fn is_locatable(rm_type: &str) -> bool {
openehr_rm::v1_2::model::is_a(rm_type, "LOCATABLE")
}
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));
}
}
pub(crate) fn dv_date_time(time: &str) -> Value {
json!({"_type": "DV_DATE_TIME", "value": time})
}
pub(crate) fn code_phrase(terminology: &str, code: &str) -> Value {
json!({
"_type": "CODE_PHRASE",
"terminology_id": {"_type": "TERMINOLOGY_ID", "value": terminology},
"code_string": code,
})
}
pub(crate) 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 empty_item_tree() -> Value {
json!({
"_type": "ITEM_TREE",
"archetype_node_id": "at0001",
"name": {"_type": "DV_TEXT", "value": "Tree"},
"items": []
})
}
fn stamp_locatable_identity(obj: &mut Map<String, Value>, node: &WebTemplateNode, rm_type: &str) {
if !is_locatable(rm_type) {
return;
}
obj.entry("name".to_owned()).or_insert_with(|| {
let text = node
.name
.as_deref()
.or(node.node_id.as_deref())
.unwrap_or(rm_type);
name_value(text, node.name_coded.as_ref())
});
if let Some(nid) = &node.node_id {
obj.entry("archetype_node_id".to_owned())
.or_insert_with(|| json!(nid));
}
}
fn finish_identity(
obj: &mut Map<String, Value>,
node: &WebTemplateNode,
is_root: bool,
template_id: &str,
) {
let rm_type = concrete_type(&node.rm_type);
if fill_non_locatable(obj, rm_type) {
return;
}
stamp_locatable_identity(obj, node, rm_type);
fill_time_anchor(obj, rm_type);
stamp_archetype_details(obj, node, is_root, template_id);
fill_mandatory_entry_attributes(obj, node, rm_type);
}
fn fill_non_locatable(obj: &mut Map<String, Value>, rm_type: &str) -> bool {
if rm_type.starts_with("DV_") || rm_type == "CODE_PHRASE" {
return true; }
if rm_type == "EVENT_CONTEXT" {
return true; }
if rm_type == "PARTICIPATION" {
obj.entry("performer".to_owned())
.or_insert_with(|| json!({"_type": "PARTY_SELF"}));
return true;
}
if matches!(rm_type, "PARTY_SELF" | "PARTY_IDENTIFIED" | "PARTY_RELATED") {
if rm_type != "PARTY_SELF"
&& !obj.contains_key("name")
&& !obj.contains_key("identifiers")
&& !obj.contains_key("external_ref")
{
obj.insert("name".to_owned(), json!("Example party"));
}
return true;
}
false
}
fn fill_time_anchor(obj: &mut Map<String, Value>, rm_type: &str) {
match rm_type {
"POINT_EVENT" | "INTERVAL_EVENT" => {
obj.entry("time".to_owned())
.or_insert_with(|| dv_date_time(DEFAULT_TIME));
}
"HISTORY" => {
obj.entry("origin".to_owned())
.or_insert_with(|| dv_date_time(DEFAULT_TIME));
}
_ => {}
}
}
fn stamp_archetype_details(
obj: &mut Map<String, Value>,
node: &WebTemplateNode,
is_root: bool,
template_id: &str,
) {
let is_root_arch = node
.node_id
.as_deref()
.is_some_and(openehr_rm::v1_2::paths::is_archetype_root_node_id);
if !is_root_arch {
return;
}
obj.entry("archetype_details".to_owned())
.or_insert_with(|| {
let mut a = Map::new();
a.insert("_type".into(), json!("ARCHETYPED"));
a.insert(
"archetype_id".into(),
json!({"_type": "ARCHETYPE_ID", "value": node.node_id}),
);
if is_root && !template_id.is_empty() {
a.insert(
"template_id".into(),
json!({"_type": "TEMPLATE_ID", "value": template_id}),
);
}
a.insert("rm_version".into(), json!(RM_VERSION));
Value::Object(a)
});
}
fn fill_mandatory_entry_attributes(
obj: &mut Map<String, Value>,
node: &WebTemplateNode,
rm_type: &str,
) {
if matches!(
rm_type,
"OBSERVATION" | "EVALUATION" | "INSTRUCTION" | "ACTION" | "ADMIN_ENTRY" | "GENERIC_ENTRY"
) {
obj.entry("subject".to_owned())
.or_insert_with(|| json!({"_type": "PARTY_SELF"}));
}
match rm_type {
"OBSERVATION" => {
obj.entry("data".to_owned())
.or_insert_with(|| structural_from_stub(node, "data", "HISTORY", "History"));
}
"EVALUATION" | "ADMIN_ENTRY" => {
obj.entry("data".to_owned())
.or_insert_with(|| structural_from_stub(node, "data", "ITEM_TREE", "Tree"));
}
"INSTRUCTION" => {
obj.entry("narrative".to_owned())
.or_insert_with(|| json!({"_type": "DV_TEXT", "value": "<narrative>"}));
}
"ACTION" => {
obj.entry("time".to_owned())
.or_insert_with(|| dv_date_time(DEFAULT_TIME));
obj.entry("description".to_owned())
.or_insert_with(|| structural_from_stub(node, "description", "ITEM_TREE", "Tree"));
obj.entry("ism_transition".to_owned()).or_insert_with(|| {
json!({"_type": "ISM_TRANSITION",
"current_state": dv_coded_text("initial", "openehr", "524")})
});
}
_ => {}
}
}
fn structural_from_stub(
node: &WebTemplateNode,
attr: &str,
default_rm_type: &str,
default_name: &str,
) -> Value {
match node.structural_stubs.iter().find(|s| s.attr == attr) {
Some(stub) => {
let name = stub.name.as_deref().unwrap_or(default_name);
structural_container(concrete_structural(&stub.rm_type), &stub.node_id, name)
}
None => structural_container(default_rm_type, "at0001", default_name),
}
}
fn concrete_structural(rm_type: &str) -> &str {
match rm_type {
"ITEM_STRUCTURE" => "ITEM_TREE",
other => other,
}
}
fn structural_container(rm_type: &str, node_id: &str, name: &str) -> Value {
let mut o = Map::new();
o.insert("_type".into(), json!(rm_type));
o.insert("archetype_node_id".into(), json!(node_id));
o.insert("name".into(), json!({"_type": "DV_TEXT", "value": name}));
match rm_type {
"HISTORY" => {
o.insert("origin".into(), dv_date_time(DEFAULT_TIME));
o.insert("events".into(), json!([]));
}
_ => {
o.insert("items".into(), json!([]));
}
}
Value::Object(o)
}
fn ensure_category(comp: &mut Map<String, Value>) {
comp.entry("category".to_owned())
.or_insert_with(|| dv_coded_text("event", "openehr", "433"));
}
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 apply_composition_ctx(
comp: &mut Map<String, Value>,
defaults: &ctx::CtxDefaults,
explicit_event_context: bool,
) {
if let Some(language) = defaults.language_code_phrase() {
comp.entry("language".to_owned()).or_insert(language);
}
if let Some(territory) = defaults.territory_code_phrase() {
comp.entry("territory".to_owned()).or_insert(territory);
}
if let Some(composer) = &defaults.composer {
comp.entry("composer".to_owned())
.or_insert_with(|| composer.clone());
}
if !explicit_event_context && !comp.contains_key("context") {
return;
}
let context = comp
.entry("context".to_owned())
.or_insert_with(|| json!({"_type": "EVENT_CONTEXT"}));
let Value::Object(context) = context else {
return;
};
context
.entry("start_time".to_owned())
.or_insert_with(|| dv_date_time(&defaults.time));
context
.entry("setting".to_owned())
.or_insert_with(|| defaults.setting.clone());
if let Some(end_time) = &defaults.end_time {
context
.entry("end_time".to_owned())
.or_insert_with(|| dv_date_time(end_time));
}
if let Some(location) = &defaults.location {
context
.entry("location".to_owned())
.or_insert_with(|| json!(location));
}
if let Some(facility) = &defaults.health_care_facility {
context
.entry("health_care_facility".to_owned())
.or_insert_with(|| facility.clone());
}
if !defaults.participations.is_empty() {
context
.entry("participations".to_owned())
.or_insert_with(|| json!(defaults.participations));
}
}
fn apply_entry_defaults(value: &mut Value, defaults: &ctx::CtxDefaults) {
if !defaults.links.is_empty()
&& let Value::Object(root) = value
{
root.entry("links".to_owned())
.or_insert_with(|| json!(defaults.links));
}
walk_entry_defaults(value, defaults);
}
fn walk_entry_defaults(value: &mut Value, defaults: &ctx::CtxDefaults) {
match value {
Value::Array(items) => {
for item in items {
walk_entry_defaults(item, defaults);
}
}
Value::Object(obj) => {
let rm_type = obj
.get("_type")
.and_then(Value::as_str)
.unwrap_or_default()
.to_owned();
apply_common_entry_defaults(obj, &rm_type, defaults);
match rm_type.as_str() {
"OBSERVATION" => apply_observation_defaults(obj, defaults),
"ACTION" => apply_action_defaults(obj, defaults),
"INSTRUCTION" => {
if let Some(narrative) = &defaults.instruction_narrative {
obj.entry("narrative".to_owned())
.or_insert_with(|| json!({"_type": "DV_TEXT", "value": narrative}));
}
}
"ACTIVITY" => {
if let Some(timing) = &defaults.activity_timing {
obj.entry("timing".to_owned()).or_insert_with(|| {
json!({"_type": "DV_PARSABLE", "value": timing, "formalism": "timing"})
});
}
}
_ => {}
}
for child in obj.values_mut() {
walk_entry_defaults(child, defaults);
}
}
_ => {}
}
}
fn apply_common_entry_defaults(
obj: &mut Map<String, Value>,
rm_type: &str,
defaults: &ctx::CtxDefaults,
) {
if !matches!(
rm_type,
"OBSERVATION" | "EVALUATION" | "INSTRUCTION" | "ACTION" | "ADMIN_ENTRY" | "GENERIC_ENTRY"
) {
return;
}
if let Some(language) = defaults.language_code_phrase() {
obj.entry("language".to_owned()).or_insert(language);
}
obj.entry("encoding".to_owned())
.or_insert_with(|| code_phrase("IANA_character-sets", "UTF-8"));
if let Some(provider) = &defaults.provider {
obj.entry("provider".to_owned())
.or_insert_with(|| provider.clone());
}
if let Some(wf) = &defaults.work_flow_id {
obj.entry("workflow_id".to_owned())
.or_insert_with(|| wf.clone());
}
}
fn apply_observation_defaults(obj: &mut Map<String, Value>, defaults: &ctx::CtxDefaults) {
let origin = defaults
.history_origin
.clone()
.unwrap_or_else(|| defaults.time.clone());
let Some(Value::Object(history)) = obj.get_mut("data") else {
return;
};
let origin_value = history
.entry("origin".to_owned())
.or_insert_with(|| dv_date_time(&origin));
if origin_value.get("value").and_then(Value::as_str) == Some(DEFAULT_TIME) {
*origin_value = dv_date_time(&origin);
}
let origin_now = origin_value.clone();
let Some(Value::Array(events)) = history.get_mut("events") else {
return;
};
for event in events {
if let Value::Object(ev) = event {
let time = ev
.entry("time".to_owned())
.or_insert_with(|| origin_now.clone());
if time.get("value").and_then(Value::as_str) == Some(DEFAULT_TIME) {
*time = origin_now.clone();
}
}
}
}
fn apply_action_defaults(obj: &mut Map<String, Value>, defaults: &ctx::CtxDefaults) {
let time = defaults
.action_time
.clone()
.unwrap_or_else(|| defaults.time.clone());
let entry = obj
.entry("time".to_owned())
.or_insert_with(|| dv_date_time(&time));
if entry.get("value").and_then(Value::as_str) == Some(DEFAULT_TIME) {
*entry = dv_date_time(&time);
}
let Some(state) = &defaults.action_ism_current_state else {
return;
};
let ism = obj
.entry("ism_transition".to_owned())
.or_insert_with(|| json!({"_type": "ISM_TRANSITION"}));
if let Value::Object(ism) = ism {
ism.entry("current_state".to_owned())
.or_insert_with(|| state.clone());
}
}
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) => {
for e in a.iter_mut() {
complete_tree(e);
}
}
_ => {}
}
}
fn fill_structural_mandatory(obj: &mut Map<String, Value>, rm_type: &str) {
match rm_type {
"HISTORY" => {
obj.entry("origin".to_owned())
.or_insert_with(|| dv_date_time(DEFAULT_TIME));
obj.entry("events".to_owned()).or_insert_with(|| json!([]));
}
"POINT_EVENT" | "EVENT" => {
obj.entry("time".to_owned())
.or_insert_with(|| dv_date_time(DEFAULT_TIME));
obj.entry("data".to_owned()).or_insert_with(empty_item_tree);
}
"INTERVAL_EVENT" => {
obj.entry("time".to_owned())
.or_insert_with(|| dv_date_time(DEFAULT_TIME));
obj.entry("data".to_owned()).or_insert_with(empty_item_tree);
obj.entry("width".to_owned())
.or_insert_with(|| json!({"_type": "DV_DURATION", "value": "P0D"}));
obj.entry("math_function".to_owned())
.or_insert_with(|| dv_coded_text("mean", "openehr", "146"));
}
"ITEM_TREE" | "ITEM_LIST" | "CLUSTER" => {
obj.entry("items".to_owned()).or_insert_with(|| json!([]));
}
"ITEM_SINGLE" => {
obj.entry("item".to_owned()).or_insert_with(|| {
json!({
"_type": "ELEMENT",
"archetype_node_id": "at0001",
"name": {"_type": "DV_TEXT", "value": "Element"},
"null_flavour": dv_coded_text("unknown", "openehr", "253"),
})
});
}
"ITEM_TABLE" => {
obj.entry("rows".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_item_tree);
}
"ISM_TRANSITION" => {
obj.entry("current_state".to_owned())
.or_insert_with(|| dv_coded_text("initial", "openehr", "524"));
}
_ => {}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn name_value_stamps_coded_and_plain_names() {
let coded = CodedName {
terminology: "local".to_owned(),
code: "at0007".to_owned(),
incoherent: false,
};
let v = name_value("Exclusion", Some(&coded));
assert_eq!(v["_type"], "DV_CODED_TEXT");
assert_eq!(v["defining_code"]["code_string"], "at0007");
assert_eq!(
name_value("Systolic", None),
json!({"_type": "DV_TEXT", "value": "Systolic"})
);
}
#[test]
fn a_party_leaf_consumes_its_relationship_and_identifier_families() {
for base in [
"PARTY_PROXY",
"PARTY_SELF",
"PARTY_IDENTIFIED",
"PARTY_RELATED",
] {
assert!(leaf_consumes_segment(base, "_relationship"), "{base}");
assert!(leaf_consumes_segment(base, "_identifier"), "{base}");
assert!(!leaf_consumes_segment(base, "_feeder_audit"), "{base}");
}
assert!(!leaf_consumes_segment("DV_TEXT", "_identifier"));
assert!(!leaf_consumes_segment("ELEMENT", "_relationship"));
}
#[test]
fn infer_type_matches_level_removal_owners() {
assert_eq!(infer_type("data", Some("events")), "HISTORY");
assert_eq!(infer_type("events", None), "POINT_EVENT");
assert_eq!(infer_type("items", None), "ELEMENT");
assert_eq!(infer_type("activities", None), "ACTIVITY");
assert_eq!(infer_type("state", Some("items")), "ITEM_TREE");
}
}