#![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;
use std::sync::LazyLock;
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::identity::{
default_name, name_value, recorded_archetype_id, stamp_archetype_details,
stamp_locatable_identity,
};
use crate::flat::map;
use crate::flat::rmpath;
use crate::flat::sim::{SimDocument, SimNode};
use crate::flat::webtemplate::model::{
CodedName, WebTemplate, WebTemplateNode, WebTemplateStructuralStub,
};
pub const RM_VERSION: &str = "1.2.0";
pub(crate) const DEFAULT_TIME: &str = "1970-01-01T00:00:00Z";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct BuildOptions<'a> {
pub rm_version: &'a str,
}
impl Default for BuildOptions<'_> {
fn default() -> Self {
Self {
rm_version: RM_VERSION,
}
}
}
impl<'a> BuildOptions<'a> {
#[must_use]
pub fn with_rm_version(mut self, rm_version: &'a str) -> Self {
self.rm_version = rm_version;
self
}
#[must_use]
pub fn for_template(wt: &'a WebTemplate) -> Self {
Self::default().with_rm_version(wt.tree.rm_release.as_deref().unwrap_or(RM_VERSION))
}
}
pub fn build_composition(
doc: &SimDocument,
wt: &WebTemplate,
now: &str,
) -> Result<Value, FlatError> {
build_composition_with(doc, wt, now, &BuildOptions::for_template(wt))
}
pub fn build_composition_with(
doc: &SimDocument,
wt: &WebTemplate,
now: &str,
options: &BuildOptions<'_>,
) -> 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 mut defaults = ctx::resolve(merged_ctx.as_ref(), now)?;
apply_root_raw(&mut defaults, data_root, root_id)?;
let build_ctx = BuildCtx::new(wt, options);
let mut comp = match build(&wt.tree, data_root, root_id, &build_ctx)? {
Some(Value::Object(m)) => m,
_ => Map::new(),
};
finish_identity(
&mut comp,
&wt.tree,
true,
&wt.template_id,
options.rm_version,
);
ensure_template_id(&mut comp, &wt.tree, &wt.template_id, options.rm_version);
ensure_category(&mut comp, &wt.tree)?;
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)
}
pub(crate) const PARTY_CLASSES: [&str; 5] = ["PERSON", "ORGANISATION", "GROUP", "AGENT", "ROLE"];
pub fn build_party(
doc: &SimDocument,
wt: &WebTemplate,
options: &BuildOptions<'_>,
) -> Result<Value, FlatError> {
let class = concrete_type(&wt.tree.rm_type);
if !PARTY_CLASSES.contains(&class) {
return Err(FlatError::NotAPartyTemplate {
root: wt.tree.rm_type.clone(),
});
}
let value = build_root(doc, wt, options)?;
check_party_decodes(&value, class)?;
Ok(value)
}
pub fn build_root(
doc: &SimDocument,
wt: &WebTemplate,
options: &BuildOptions<'_>,
) -> Result<Value, FlatError> {
let root_id = &wt.tree.id;
let mut data_root = None;
for (name, child) in &doc.children {
match name.as_str() {
"ctx" => return Err(FlatError::ContextOnParty),
n if n == root_id => data_root = child.occurrences.first(),
other => return Err(FlatError::UnknownPath(other.to_owned())),
}
}
let Some(data_root) = data_root else {
return Err(FlatError::Conversion(
"empty simplified document: no data root".to_owned(),
));
};
let build_ctx = BuildCtx::new(wt, options);
let mut root = match build(&wt.tree, data_root, root_id, &build_ctx)? {
Some(Value::Object(m)) => m,
_ => Map::new(),
};
finish_identity(
&mut root,
&wt.tree,
true,
&wt.template_id,
options.rm_version,
);
ensure_template_id(&mut root, &wt.tree, &wt.template_id, options.rm_version);
let mut value = Value::Object(root);
complete_tree(&mut value);
Ok(value)
}
pub(crate) fn check_party_decodes(value: &Value, class: &str) -> Result<(), FlatError> {
use openehr_its::json::from_canonical_value;
use openehr_rm::v1_2::demographic;
let decoded = match class {
"PERSON" => from_canonical_value::<demographic::person::Person>(value).map(drop),
"ORGANISATION" => {
from_canonical_value::<demographic::organisation::Organisation>(value).map(drop)
}
"GROUP" => from_canonical_value::<demographic::group::Group>(value).map(drop),
"AGENT" => from_canonical_value::<demographic::agent::Agent>(value).map(drop),
_ => from_canonical_value::<demographic::role::Role>(value).map(drop),
};
decoded.map_err(|source| FlatError::PartyDecode {
class: class.to_owned(),
source,
})
}
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"));
let context = data_root.child("context");
let context_attr = |name: &str, suffix: &str| {
context
.and_then(|c| c.child(name))
.and_then(|n| n.attrs.get(suffix))
};
fill("time", context_attr("start_time", ""));
fill("end_time", context_attr("_end_time", ""));
fill("location", context_attr("_location", ""));
fill(
"setting",
context_attr("setting", "code").or_else(|| context_attr("setting", "value")),
);
if merged.is_empty() {
None
} else {
Some(merged)
}
}
fn apply_root_raw(
defaults: &mut ctx::CtxDefaults,
data_root: &SimNode,
root_id: &str,
) -> Result<(), FlatError> {
let raw = |seg: &str, types: &[&str]| {
data_root
.child(seg)
.and_then(|node| map::raw_value(node, &format!("{root_id}/{seg}"), types))
.transpose()
};
if let Some(phrase) = raw("language", &["CODE_PHRASE"])?
&& defaults.language.is_none()
{
defaults.language_phrase = Some(phrase);
}
if let Some(phrase) = raw("territory", &["CODE_PHRASE"])?
&& defaults.territory.is_none()
{
defaults.territory_phrase = Some(phrase);
}
if let Some(composer) = raw("composer", map::PARTY_PROXY_CLASSES)?
&& defaults.composer.is_none()
{
defaults.composer = Some(composer);
}
Ok(())
}
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::RawWithoutType {
path: path.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"
)
}
struct SlotType {
path: Vec<PathSegment>,
rm_type: String,
name: Option<String>,
}
struct BuildCtx<'a> {
slots: Vec<SlotType>,
rm_version: &'a str,
}
impl<'a> BuildCtx<'a> {
fn new(wt: &WebTemplate, options: &BuildOptions<'a>) -> Self {
let mut slots = Vec::new();
collect_slot_types(&wt.tree, &mut slots);
Self {
slots,
rm_version: options.rm_version,
}
}
}
fn collect_slot_types(node: &WebTemplateNode, out: &mut Vec<SlotType>) {
for slot in &node.slots {
out.push(SlotType {
path: rmpath::parse(&slot.path),
rm_type: slot.rm_type.clone(),
name: slot.name.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
})
}
#[derive(Clone, Default)]
struct WrapperFacts {
rm_type: Option<String>,
name: Option<String>,
}
fn wrapper_types(child_aql: &str, rel_len: usize, slots: &[SlotType]) -> Vec<WrapperFacts> {
let child_abs = rmpath::parse(child_aql);
if child_abs.len() < rel_len {
return vec![WrapperFacts::default(); rel_len];
}
let base = child_abs.len() - rel_len;
(0..rel_len)
.map(|i| {
child_abs
.get(..=base + i)
.and_then(|abs| slots.iter().find(|slot| segments_match(&slot.path, abs)))
.map(|slot| WrapperFacts {
rm_type: Some(slot.rm_type.clone()),
name: slot.name.clone(),
})
.unwrap_or_default()
})
.collect()
}
fn build(
node: &WebTemplateNode,
sim: &SimNode,
path: &str,
ctx: &BuildCtx<'_>,
) -> Result<Option<Value>, FlatError> {
if node.has_input() {
return build_leaf(node, sim, path);
}
let interval;
let node = if is_interval_input(node, sim) {
interval = WebTemplateNode {
rm_type: "INTERVAL_EVENT".to_owned(),
..node.clone()
};
&interval
} else {
node
};
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, ctx, &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, "", ctx.rm_version);
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,
ctx: &BuildCtx<'_>,
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, ctx)?;
place(obj, &rel, child_val, child, occ, &child_path, ctx)?;
}
}
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;
}
if node.rm_type == "ACTIVITY" && child.id == "action_archetype_id" {
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,
Performer,
}
fn direct_rm_path(host: &str, seg: &str) -> Option<DirectPath> {
use DirectPath::{ActionArchetypeId, CodedGroup, HistoryOrigin, Ism, Leaf, Performer};
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",
},
("ROLE", "performer") => Performer,
_ => 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"),
("DV_INTERVAL", "lower_unbounded") => Some("lower_unbounded"),
("DV_INTERVAL", "upper_unbounded") => Some("upper_unbounded"),
("DV_INTERVAL", "lower_included") => Some("lower_included"),
("DV_INTERVAL", "upper_included") => Some("upper_included"),
_ => 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);
}
DirectPath::Performer => {
if let Some(node) = occ
&& !obj.contains_key("performer")
{
obj.insert("performer".to_owned(), build_party_ref(node, &sub_path)?);
}
}
}
Ok(true)
}
fn build_party_ref(node: &SimNode, path: &str) -> Result<Value, FlatError> {
if let Some(raw) = map::raw_value(node, path, &["PARTY_REF"]) {
return raw;
}
if let Some(suffix) = node
.attrs
.keys()
.find(|k| !matches!(k.as_str(), "id" | "namespace" | "type" | "id_scheme"))
{
return Err(FlatError::UnknownSuffix {
rm_type: "PARTY_REF".to_owned(),
suffix: suffix.clone(),
path: path.to_owned(),
});
}
let required = |suffix: &str| {
node.attrs
.get(suffix)
.and_then(Value::as_str)
.ok_or_else(|| FlatError::MissingRequiredSuffix {
key: format!("{path}|{suffix}"),
})
};
let id = required("id")?;
let namespace = required("namespace")?;
let party_type = required("type")?;
let id_obj = match node.attrs.get("id_scheme").and_then(Value::as_str) {
Some(scheme) => json!({"_type": "GENERIC_ID", "value": id, "scheme": scheme}),
None => json!({"_type": "HIER_OBJECT_ID", "value": id}),
};
Ok(json!({
"_type": "PARTY_REF",
"namespace": namespace,
"type": party_type,
"id": id_obj,
}))
}
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,
ctx: &BuildCtx<'_>,
) -> Result<(), FlatError> {
if rel.is_empty() {
return Ok(());
}
let types = wrapper_types(&child.aql_path, rel.len(), &ctx.slots);
let placement = Placement {
rel,
id_idx: rel.iter().rposition(|s| is_multiple(&s.attribute)),
child,
occ,
path,
types: &types,
ctx,
};
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 [WrapperFacts],
ctx: &'a BuildCtx<'a>,
}
impl Placement<'_> {
fn type_at(&self, i: usize) -> Option<&str> {
self.types.get(i).and_then(|facts| facts.rm_type.as_deref())
}
fn name_at(&self, i: usize) -> Option<&str> {
self.types.get(i).and_then(|facts| facts.name.as_deref())
}
}
#[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.type_at(i),
);
if let Value::Object(m) = &mut el {
place(
m,
&p.rel[i + 1..],
child_value,
p.child,
p.occ,
p.path,
p.ctx,
)?;
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 name = seg.predicate.name_value.as_deref();
let existing = arr.iter().position(|e| {
e.get("archetype_node_id").and_then(Value::as_str) == node_id
&& name.is_none_or(|n| e.pointer("/name/value").and_then(Value::as_str) == Some(n))
});
let idx = if let Some(pos) = existing {
pos
} else {
arr.push(new_struct(
seg,
p.rel.get(i + 1),
p.name_at(i),
None,
p.type_at(i),
));
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), p.name_at(i), None, p.type_at(i)),
);
}
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(())
}
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",
"PERSON",
"ORGANISATION",
"GROUP",
"AGENT",
"ROLE",
"PARTY_RELATIONSHIP",
];
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::is_primitive(attr.declared_type) {
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
});
fn is_multiple(attr: &str) -> bool {
MULTIVALUED_ATTRS.contains(attr)
}
fn is_interval_input(node: &WebTemplateNode, sim: &SimNode) -> bool {
base_type(&node.rm_type) == "EVENT"
&& (sim.children.contains_key("width")
|| sim.children.contains_key("math_function")
|| sim.attrs.contains_key("sample_count"))
}
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 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)
.map_or_else(|| default_name(rm_type), str::to_owned);
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));
}
"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 finish_identity(
obj: &mut Map<String, Value>,
node: &WebTemplateNode,
is_root: bool,
template_id: &str,
rm_version: &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, rm_version);
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 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"));
}
"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"));
}
_ => {}
}
}
fn structural_from_stub(
node: &WebTemplateNode,
attr: &str,
default_rm_type: &str,
default_name: &str,
) -> Value {
let path = format!("{}/{attr}", node.aql_path);
match node.structural_stubs.iter().find(|s| s.path == path) {
Some(stub) => structural_from(stub, default_name),
None => structural_container(default_rm_type, "at0001", default_name),
}
}
pub(crate) fn structural_from(stub: &WebTemplateStructuralStub, default_name: &str) -> Value {
let name = stub.name.as_deref().unwrap_or(default_name);
structural_container(concrete_structural(&stub.rm_type), &stub.node_id, 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("items".into(), json!([]));
}
}
Value::Object(o)
}
fn ensure_category(comp: &mut Map<String, Value>, root: &WebTemplateNode) -> Result<(), FlatError> {
if comp.contains_key("category") {
return Ok(());
}
let category =
template_category(root).map_err(|_admitted| FlatError::MissingRequiredSuffix {
key: format!("{}/category|code", root.id),
})?;
comp.insert("category".to_owned(), category);
Ok(())
}
pub(crate) fn template_category(root: &WebTemplateNode) -> Result<Value, Vec<String>> {
let input = root
.children
.iter()
.find(|child| child.aql_path == "/category")
.and_then(|node| {
node.inputs
.iter()
.find(|input| input.suffix.as_deref() == Some("code"))
});
let Some(input) = input.filter(|input| !input.list.is_empty()) else {
return Ok(dv_coded_text("event", "openehr", "433"));
};
let terminology = input.terminology.as_deref().unwrap_or("openehr");
let assumed = input.default_value.as_ref().and_then(Value::as_str);
let chosen = match (assumed, input.list.as_slice()) {
(Some(code), list) => list.iter().find(|entry| entry.value == code),
(None, [only]) => Some(only),
(None, _) => None,
};
match chosen {
Some(entry) => Ok(dv_coded_text(
entry.label.as_deref().unwrap_or(&entry.value),
terminology,
&entry.value,
)),
None => Err(input.list.iter().map(|entry| entry.value.clone()).collect()),
}
}
fn ensure_template_id(
comp: &mut Map<String, Value>,
root: &WebTemplateNode,
template_id: &str,
rm_version: &str,
) {
let ad = comp
.entry("archetype_details".to_owned())
.or_insert_with(|| {
json!({"_type": "ARCHETYPED",
"archetype_id": {"_type": "ARCHETYPE_ID", "value": recorded_archetype_id(root)},
"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 Some(Value::Object(history)) = obj.get_mut("data") else {
return;
};
let origin = defaults
.history_origin
.clone()
.or_else(|| earliest_event_time(history))
.unwrap_or_else(|| defaults.time.clone());
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 earliest_event_time(history: &Map<String, Value>) -> Option<String> {
let Some(Value::Array(events)) = history.get("events") else {
return None;
};
events
.iter()
.filter_map(|event| event.pointer("/time/value").and_then(Value::as_str))
.filter(|time| *time != DEFAULT_TIME)
.filter_map(|time| instant(time).map(|at| (at, time)))
.min_by_key(|(at, _)| *at)
.map(|(_, time)| time.to_owned())
}
fn instant(text: &str) -> Option<i128> {
let (date, rest) = text.split_once('T')?;
let mut ymd = date.splitn(3, '-');
let year: i64 = ymd.next()?.parse().ok()?;
let month: i64 = ymd.next()?.parse().ok()?;
let day: i64 = ymd.next()?.parse().ok()?;
let (clock, offset_minutes) = if let Some(clock) = rest.strip_suffix('Z') {
(clock, 0)
} else if let Some(at) = rest.rfind(['+', '-']) {
let (clock, offset) = rest.split_at(at);
let sign = if offset.starts_with('-') { -1 } else { 1 };
let (hours, minutes) = offset.get(1..)?.split_once(':')?;
let minutes = hours.parse::<i64>().ok()? * 60 + minutes.parse::<i64>().ok()?;
(clock, sign * minutes)
} else {
(rest, 0)
};
let mut hms = clock.splitn(3, ':');
let hour: i64 = hms.next()?.parse().ok()?;
let minute: i64 = hms.next()?.parse().ok()?;
let (second, nanos) = match hms.next() {
None => (0, 0),
Some(sec) => match sec.split_once(['.', ',']) {
None => (sec.parse::<i64>().ok()?, 0),
Some((whole, frac)) => {
let digits: String = frac.chars().chain("000000000".chars()).take(9).collect();
(whole.parse::<i64>().ok()?, digits.parse::<i64>().ok()?)
}
},
};
let y = if month <= 2 { year - 1 } else { year };
let era = y.div_euclid(400);
let yoe = y - era * 400;
let mp = (month + 9) % 12;
let doy = (153 * mp + 2).div_euclid(5) + day - 1;
let doe = yoe * 365 + yoe.div_euclid(4) - yoe.div_euclid(100) + doy;
let days = era * 146_097 + doe - 719_468;
let seconds = days * 86_400 + hour * 3_600 + minute * 60 + second - offset_minutes * 60;
Some(i128::from(seconds) * 1_000_000_000 + i128::from(nanos))
}
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));
}
"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"));
}
"INSTRUCTION" => {
obj.entry("narrative".to_owned())
.or_insert_with(|| json!({"_type": "DV_TEXT", "value": "<narrative>"}));
}
"ACTION" => {
obj.entry("ism_transition".to_owned())
.or_insert_with(|| json!({"_type": "ISM_TRANSITION"}));
}
_ => {}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_history_without_an_event_carries_no_events_member() {
let built = structural_container("HISTORY", "at0001", "History");
assert!(built.get("events").is_none(), "{built}");
let mut obj = Map::new();
fill_structural_mandatory(&mut obj, "HISTORY");
assert!(
obj.contains_key("origin") && !obj.contains_key("events"),
"{obj:?}"
);
}
#[test]
fn an_unset_history_origin_is_the_earliest_event_time() {
let mut obj = json!({
"_type": "OBSERVATION",
"data": { "_type": "HISTORY", "events": [
{ "_type": "POINT_EVENT", "time": dv_date_time("2026-03-02T10:00:00+01:00") },
{ "_type": "POINT_EVENT", "time": dv_date_time("2026-03-02T08:30:00Z") },
{ "_type": "POINT_EVENT" }
]}
});
let defaults = ctx::CtxDefaults {
time: "2026-03-05T12:00:00Z".to_owned(),
..ctx::CtxDefaults::default()
};
let Value::Object(map) = &mut obj else {
panic!("an object")
};
apply_observation_defaults(map, &defaults);
assert_eq!(obj["data"]["origin"]["value"], "2026-03-02T08:30:00Z");
assert_eq!(
obj["data"]["events"][2]["time"]["value"], "2026-03-02T08:30:00Z",
"an event without a time takes the origin"
);
let mut none = json!({"_type": "OBSERVATION", "data": {"_type": "HISTORY", "events": []}});
let Value::Object(map) = &mut none else {
panic!("an object")
};
apply_observation_defaults(map, &defaults);
assert_eq!(
none["data"]["origin"]["value"], "2026-03-05T12:00:00Z",
"with no event time the context time stands"
);
}
#[test]
fn instants_order_across_offsets() {
assert!(instant("2026-03-02T10:00:00+01:00") > instant("2026-03-02T08:30:00Z"));
assert_eq!(instant("1970-01-01T00:00:00Z"), Some(0));
assert_eq!(instant("1970-01-01T01:00:00.5+01:00"), Some(500_000_000));
assert_eq!(instant("not a time"), None);
}
#[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");
}
}
#[cfg(test)]
mod multiplicity_tests {
use super::is_multiple;
#[test]
fn covers_legacy_hardcoded_set() {
for attr in ["content", "items", "events", "activities"] {
assert!(
is_multiple(attr),
"model-driven multiplicity set must include the legacy member `{attr}`",
);
}
}
#[test]
fn data_stays_single() {
assert!(
!is_multiple("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("other_participations"));
}
#[test]
fn rejects_single_and_unknown() {
assert!(!is_multiple("value"));
assert!(!is_multiple("not_a_real_attribute"));
}
}