#![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::borrow::Cow;
use serde_json::Value;
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::validation::node_ids::as_template_node_ids;
use crate::flat::webtemplate::model::{WebTemplate, WebTemplateNode};
pub fn flatten_composition(
composition: &Value,
wt: &WebTemplate,
) -> Result<SimDocument, FlatError> {
if !composition.is_object() {
return Err(FlatError::Conversion(
"composition must be a JSON object".to_owned(),
));
}
let composition = &*as_template_node_ids(composition, wt);
let mut doc = SimNode::default();
ctx::emit(composition, doc.occurrence_mut("ctx", None));
let root = doc.occurrence_mut(&wt.tree.id, None);
walk(&wt.tree, composition, root);
if let Some(composer) = composition
.get("composer")
.filter(|c| !map::parties::ref_type_survives(c, "PERSON"))
{
root.occurrence_mut("composer", None)
.attrs
.insert("raw".to_owned(), composer.clone());
}
doc.prune_empty();
Ok(doc)
}
fn type_matches(rm: &Value, rm_type: &str) -> bool {
let want = rm_type.split('<').next().unwrap_or(rm_type);
match rm.get("_type").and_then(Value::as_str) {
Some(actual) => actual == want,
None => true,
}
}
fn covered_by_ctx(node: &WebTemplateNode, child: &WebTemplateNode) -> bool {
if node.rm_type == "COMPOSITION"
&& matches!(child.id.as_str(), "language" | "territory" | "composer")
{
return true;
}
if node.rm_type == "EVENT_CONTEXT" {
return !child.aql_path.contains("other_context");
}
false
}
fn is_default_entry_context(child: &WebTemplateNode, rm: &Value) -> bool {
if child.in_context != Some(true) {
return false;
}
match child.id.as_str() {
"subject" => {
rm.get("_type").and_then(Value::as_str) == Some("PARTY_SELF")
&& rm.get("external_ref").is_none()
}
"language" | "encoding" => true,
_ => false,
}
}
fn walk(node: &WebTemplateNode, rm: &Value, out: &mut SimNode) {
if node.has_input() {
if !leaf_type_conforms(rm, &node.rm_type) {
out.attrs.insert("raw".to_owned(), rm.clone());
return;
}
let list_open = node.inputs.iter().find_map(|i| i.list_open);
map::data_values::emit_leaf(rm, &node.rm_type, list_open, out);
map::structures::emit_rm_attrs(rm, base_type(&node.rm_type), out);
return;
}
map::structures::emit_rm_attrs(rm, base_type(&node.rm_type), out);
let names = name_fallback(node);
for child in &node.children {
let direct = node.rm_type == "ACTIVITY" && child.id == "action_archetype_id";
if !covered_by_ctx(node, child) && !direct {
walk_child(node, child, rm, out, &names);
}
}
emit_direct_rm_paths(node, rm, out);
}
fn name_fallback(node: &WebTemplateNode) -> Cow<'_, rmpath::NameFallback> {
match &node.walk {
Some(plan) => Cow::Borrowed(&plan.name_fallback),
None => Cow::Owned(rmpath::name_fallback_from_paths(
&node.aql_path,
node.children.iter().map(|c| c.aql_path.as_str()),
)),
}
}
fn walk_child(
node: &WebTemplateNode,
child: &WebTemplateNode,
rm: &Value,
out: &mut SimNode,
names: &rmpath::NameFallback,
) {
let rel = rmpath::relative(&node.aql_path, &child.aql_path);
let in_choice = child.alt_json_id.is_some()
|| node
.children
.iter()
.any(|c| c.id != child.id && c.aql_path == child.aql_path);
if in_choice && shadowed_by_earlier_alternative(node, child) {
return;
}
let occurrences = occurrences_of(child, rm, &rel, in_choice, names);
if occurrences.is_empty() {
return;
}
if child.max == -1 || child.max > 1 {
out.children.entry(child.id.clone()).or_default().indexed = true;
}
for (i, occurrence) in occurrences.iter().enumerate() {
if occurrence
.value
.is_some_and(|v| is_default_entry_context(child, v))
{
continue;
}
let slot = out.place_mut(&child.id, u32::try_from(i).unwrap_or(u32::MAX));
if let Some(value) = occurrence.value {
walk(child, value, slot);
}
if let Some(element) = occurrence.element {
map::structures::emit_rm_attrs(element, "ELEMENT", slot);
}
}
}
fn shadowed_by_earlier_alternative(node: &WebTemplateNode, child: &WebTemplateNode) -> bool {
node.children
.iter()
.take_while(|c| !std::ptr::eq(*c, child))
.any(|c| c.aql_path == child.aql_path && base_type(&c.rm_type) == base_type(&child.rm_type))
}
struct Occurrence<'a> {
value: Option<&'a Value>,
element: Option<&'a Value>,
}
fn occurrences_of<'a>(
child: &WebTemplateNode,
rm: &'a Value,
rel: &[openehr_rm::v1_2::paths::PathSegment],
in_choice: bool,
names: &rmpath::NameFallback,
) -> Vec<Occurrence<'a>> {
let Some(wrappers) = value_step_owners(child, rm, rel, names) else {
return rmpath::resolve_matched(rm, rel, names)
.into_iter()
.filter(|value| !in_choice || type_matches(value, &child.rm_type))
.map(|value| Occurrence {
value: Some(value),
element: None,
})
.collect();
};
wrappers
.into_iter()
.filter_map(|wrapper| {
let element = (wrapper.get("_type").and_then(Value::as_str) == Some("ELEMENT"))
.then_some(wrapper);
match wrapper.get("value").filter(|v| !v.is_null()) {
Some(value) if !in_choice || type_matches(value, &child.rm_type) => {
Some(Occurrence {
value: Some(value),
element,
})
}
Some(_) => None,
None => (element
.is_some_and(|e| e.get("null_flavour").is_some_and(|v| !v.is_null()))
&& (!in_choice || child.alt_json_id.is_none()))
.then_some(Occurrence {
value: None,
element,
}),
}
})
.collect()
}
fn value_step_owners<'a>(
child: &WebTemplateNode,
rm: &'a Value,
rel: &[openehr_rm::v1_2::paths::PathSegment],
names: &rmpath::NameFallback,
) -> Option<Vec<&'a Value>> {
if !child.has_input() {
return None;
}
let (last, parents) = rel.split_last()?;
(last.attribute == "value").then(|| rmpath::resolve_matched(rm, parents, names))
}
fn attr_emitted(node: &WebTemplateNode, out: &SimNode, attr: &str) -> bool {
node.children.iter().any(|c| {
out.children.contains_key(&c.id)
&& rmpath::relative(&node.aql_path, &c.aql_path)
.first()
.is_some_and(|s| s.attribute == attr)
})
}
fn emit_direct_rm_paths(node: &WebTemplateNode, rm: &Value, out: &mut SimNode) {
match base_type(&node.rm_type) {
"ACTION" => emit_action_paths(node, rm, out),
"ISM_TRANSITION" => {
for attr in ["current_state", "transition", "careflow_step"] {
emit_direct_leaf(node, out, attr, "DV_CODED_TEXT", rm.get(attr));
}
}
"INSTRUCTION" => {
emit_direct_leaf(node, out, "narrative", "DV_TEXT", rm.get("narrative"));
}
"OBSERVATION" => emit_history_origin(rm, out),
"ACTIVITY" => emit_activity_paths(node, rm, out),
"EVENT" if rm.get("_type").and_then(Value::as_str) == Some("INTERVAL_EVENT") => {
emit_interval_event_paths(node, rm, out);
}
"POINT_EVENT" | "EVENT" => {
emit_direct_leaf(node, out, "time", "DV_DATE_TIME", rm.get("time"));
}
"INTERVAL_EVENT" => emit_interval_event_paths(node, rm, out),
"DV_INTERVAL" => emit_interval_flags(rm, out),
_ => {}
}
}
fn emit_interval_flags(rm: &Value, out: &mut SimNode) {
for side in ["lower", "upper"] {
let has_bound = rm.get(side).is_some_and(|v| !v.is_null());
let unbounded = format!("{side}_unbounded");
let included = format!("{side}_included");
if let Some(flag) = rm.get(&unbounded).and_then(Value::as_bool)
&& (flag || flag == has_bound)
{
out.attrs.insert(unbounded, Value::Bool(flag));
}
if let Some(flag) = rm.get(&included).and_then(Value::as_bool)
&& (!flag || flag != has_bound)
{
out.attrs.insert(included, Value::Bool(flag));
}
}
}
fn emit_direct_leaf(
node: &WebTemplateNode,
out: &mut SimNode,
name: &str,
rm_type: &str,
value: Option<&Value>,
) {
if attr_emitted(node, out, name) {
return;
}
if let Some(v) = value.filter(|v| !v.is_null()) {
map::data_values::emit_leaf(v, rm_type, None, out.occurrence_mut(name, None));
}
}
fn emit_action_paths(node: &WebTemplateNode, rm: &Value, out: &mut SimNode) {
emit_direct_leaf(node, out, "time", "DV_DATE_TIME", rm.get("time"));
if !attr_emitted(node, out, "ism_transition")
&& let Some(ism) = rm.get("ism_transition").filter(|v| !v.is_null())
{
emit_ism_transition(ism, out.occurrence_mut("ism_transition", None));
}
}
fn emit_history_origin(rm: &Value, out: &mut SimNode) {
if !out.children.contains_key("history_origin")
&& let Some(origin) = rm.pointer("/data/origin/value")
{
out.occurrence_mut("history_origin", None)
.attrs
.insert(String::new(), origin.clone());
}
}
fn emit_activity_paths(node: &WebTemplateNode, rm: &Value, out: &mut SimNode) {
emit_direct_leaf(node, out, "timing", "DV_PARSABLE", rm.get("timing"));
if !attr_emitted(node, out, "action_archetype_id")
&& let Some(aid) = rm.get("action_archetype_id").and_then(Value::as_str)
&& aid != "/.*/"
{
out.occurrence_mut("action_archetype_id", None)
.attrs
.insert(String::new(), Value::String(aid.to_owned()));
}
}
fn emit_interval_event_paths(node: &WebTemplateNode, rm: &Value, out: &mut SimNode) {
emit_direct_leaf(node, out, "time", "DV_DATE_TIME", rm.get("time"));
emit_direct_leaf(node, out, "width", "DV_DURATION", rm.get("width"));
emit_direct_leaf(
node,
out,
"math_function",
"DV_CODED_TEXT",
rm.get("math_function"),
);
if let Some(n) = rm.get("sample_count").filter(|v| !v.is_null()) {
out.attrs.insert("sample_count".to_owned(), n.clone());
}
}
fn emit_ism_transition(ism: &Value, out: &mut SimNode) {
for attr in ["current_state", "transition", "careflow_step"] {
if let Some(cs) = ism.get(attr).filter(|v| !v.is_null()) {
map::data_values::emit_leaf(cs, "DV_CODED_TEXT", None, out.occurrence_mut(attr, None));
}
}
map::structures::emit_rm_attrs(ism, "ISM_TRANSITION", out);
}
fn base_type(rm_type: &str) -> &str {
rm_type.split('<').next().unwrap_or(rm_type)
}
fn leaf_type_conforms(rm: &Value, declared: &str) -> bool {
let Some(actual) = rm.get("_type").and_then(Value::as_str) else {
return true;
};
let declared = base_type(declared);
if actual == declared {
return true;
}
matches!(
(declared, actual),
("DV_TEXT", "DV_CODED_TEXT")
| ("DV_CODED_TEXT", "DV_TEXT")
| (
"PARTY_PROXY",
"PARTY_SELF" | "PARTY_IDENTIFIED" | "PARTY_RELATED"
)
)
}