#![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 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::webtemplate::{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 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);
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::emit_leaf(rm, &node.rm_type, list_open, out);
map::emit_rm_attrs(rm, base_type(&node.rm_type), out);
return;
}
map::emit_rm_attrs(rm, base_type(&node.rm_type), out);
for child in &node.children {
if covered_by_ctx(node, child) {
continue;
}
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);
let occurrences = occurrences_of(child, rm, &rel, in_choice);
if occurrences.is_empty() {
continue;
}
let repeating = child.max == -1 || child.max > 1;
if repeating {
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::emit_rm_attrs(element, "ELEMENT", slot);
}
}
}
emit_direct_rm_paths(node, rm, out);
}
struct Occurrence<'a> {
value: Option<&'a Value>,
element: Option<&'a Value>,
}
fn occurrences_of<'a>(
child: &WebTemplateNode,
rm: &'a Value,
rel: &[openehr_rm::paths::PathSegment],
in_choice: bool,
) -> Vec<Occurrence<'a>> {
let Some(wrappers) = value_step_owners(child, rm, rel) else {
return rmpath::resolve(rm, rel)
.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::paths::PathSegment],
) -> Option<Vec<&'a Value>> {
if !child.has_input() {
return None;
}
let (last, parents) = rel.split_last()?;
(last.attribute == "value").then(|| rmpath::resolve(rm, parents))
}
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) {
let mut leaf = |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::emit_leaf(v, rm_type, None, out.occurrence_mut(name, None));
}
};
match base_type(&node.rm_type) {
"ACTION" => {
leaf("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));
}
}
"INSTRUCTION" => leaf("narrative", "DV_TEXT", rm.get("narrative")),
"OBSERVATION" => {
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());
}
}
"ACTIVITY" => {
leaf("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()));
}
}
"POINT_EVENT" | "EVENT" => leaf("time", "DV_DATE_TIME", rm.get("time")),
"INTERVAL_EVENT" => {
leaf("time", "DV_DATE_TIME", rm.get("time"));
leaf("width", "DV_DURATION", rm.get("width"));
leaf("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::emit_leaf(cs, "DV_CODED_TEXT", None, out.occurrence_mut(attr, None));
}
}
map::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"
)
)
}