#![allow(
dead_code,
reason = "consumed by the RM ⇄ sim walkers landing in this same rewrite; drop this allow with their arrival"
)]
#![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, RmPath, select_children};
use serde_json::Value;
pub(crate) fn parse(rel: &str) -> Vec<PathSegment> {
rel.parse::<RmPath>()
.map(|p| p.segments)
.unwrap_or_default()
}
pub(crate) fn relative(parent_aql: &str, child_aql: &str) -> Vec<PathSegment> {
parse(child_aql.strip_prefix(parent_aql).unwrap_or(child_aql))
}
pub(crate) fn navigate<'a>(roots: &[&'a Value], segs: &[PathSegment]) -> Vec<&'a Value> {
let mut current: Vec<&Value> = roots.to_vec();
for seg in segs {
current = current
.iter()
.flat_map(|n| select_children(n, seg))
.collect();
}
current
}
pub(crate) fn select_children_matched<'a>(
container: &'a Value,
seg: &PathSegment,
allow_name_fallback: bool,
) -> Vec<&'a Value> {
let strict = select_children(container, seg);
if strict.is_empty() && allow_name_fallback && seg.predicate.name_value.is_some() {
let mut id_only = seg.clone();
id_only.predicate.name_value = None;
return select_children(container, &id_only);
}
strict
}
pub(crate) fn select_children_excluding_names<'a>(
container: &'a Value,
seg: &PathSegment,
excluded_names: &[String],
) -> Vec<&'a Value> {
select_children(container, seg)
.into_iter()
.filter(|node| {
node.get("name")
.and_then(|n| n.get("value"))
.and_then(Value::as_str)
.is_none_or(|name| !excluded_names.iter().any(|e| e == name))
})
.collect()
}
pub(crate) type NameFallback = std::collections::HashMap<(String, String), Option<String>>;
pub(crate) fn name_fallback_from_segments<'a>(
paths: impl Iterator<Item = &'a [PathSegment]>,
) -> NameFallback {
let mut out: NameFallback = std::collections::HashMap::new();
for segs in paths {
for seg in segs {
let Some(id) = &seg.predicate.archetype_node_id else {
continue;
};
let key = (seg.attribute.clone(), id.clone());
let name = seg.predicate.name_value.clone();
match out.entry(key) {
std::collections::hash_map::Entry::Vacant(v) => {
v.insert(name);
}
std::collections::hash_map::Entry::Occupied(mut o) => {
if *o.get() != name {
o.insert(None);
}
}
}
}
}
out
}
pub(crate) fn name_fallback_from_paths<'a>(
parent_aql: &str,
paths: impl Iterator<Item = &'a str>,
) -> NameFallback {
let parsed: Vec<Vec<PathSegment>> = paths.map(|p| relative(parent_aql, p)).collect();
name_fallback_from_segments(parsed.iter().map(Vec::as_slice))
}
fn fallback_allowed(table: &NameFallback, seg: &PathSegment) -> bool {
let Some(id) = &seg.predicate.archetype_node_id else {
return false;
};
table
.get(&(seg.attribute.clone(), id.clone()))
.is_some_and(|uniform| uniform.as_ref() == seg.predicate.name_value.as_ref())
}
pub(crate) fn navigate_matched<'a>(
roots: &[&'a Value],
segs: &[PathSegment],
table: &NameFallback,
) -> Vec<&'a Value> {
let mut current: Vec<&Value> = roots.to_vec();
for seg in segs {
let allow = fallback_allowed(table, seg);
current = current
.iter()
.flat_map(|n| select_children_matched(n, seg, allow))
.collect();
}
current
}
pub(crate) fn resolve<'a>(rm: &'a Value, segs: &[PathSegment]) -> Vec<&'a Value> {
navigate(&[rm], segs)
}
pub(crate) fn resolve_matched<'a>(
rm: &'a Value,
segs: &[PathSegment],
table: &NameFallback,
) -> Vec<&'a Value> {
navigate_matched(&[rm], segs, table)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_segments() {
let segs = parse(
"/content[openEHR-EHR-SECTION.ispek_dialog.v1,'Vitals']/items[openEHR-EHR-OBSERVATION.lab_test-hba1c.v1]/data[at0001]/value",
);
assert_eq!(segs.len(), 4);
assert_eq!(segs[0].attribute, "content");
assert_eq!(
segs[0].predicate.archetype_node_id.as_deref(),
Some("openEHR-EHR-SECTION.ispek_dialog.v1")
);
assert_eq!(segs[0].predicate.name_value.as_deref(), Some("Vitals"));
assert_eq!(segs[2].attribute, "data");
assert_eq!(
segs[2].predicate.archetype_node_id.as_deref(),
Some("at0001")
);
assert_eq!(segs[3].attribute, "value");
assert!(segs[3].predicate.is_empty());
}
#[test]
fn relative_strips_parent() {
let rel = relative(
"/content[openEHR-EHR-OBSERVATION.x.v1]",
"/content[openEHR-EHR-OBSERVATION.x.v1]/data[at0001]/events[at0002]",
);
assert_eq!(rel.len(), 2);
assert_eq!(rel[0].attribute, "data");
assert_eq!(rel[1].attribute, "events");
}
#[test]
fn matched_step_falls_back_to_id_only_for_renamed_instances() {
let rm = serde_json::json!({
"content": [
{"archetype_node_id": "openEHR-EHR-EVALUATION.a.v1",
"name": {"value": "Runtime Name"},
"data": {"archetype_node_id": "at0001", "_type": "ITEM_LIST"}}
]
});
let segs = parse("/content[openEHR-EHR-EVALUATION.a.v1,'Template Name']/data[at0001]");
let step = select_children_matched(&rm, &segs[0], true);
assert_eq!(step.len(), 1);
assert_eq!(step[0]["name"]["value"], "Runtime Name");
assert_eq!(
select_children_matched(&rm, &segs[0], false),
Vec::<&Value>::new()
);
}
#[test]
fn matched_step_still_disambiguates_matching_siblings() {
let rm = serde_json::json!({
"content": [
{"archetype_node_id": "openEHR-EHR-OBSERVATION.x.v1",
"name": {"value": "First"}, "tag": 1},
{"archetype_node_id": "openEHR-EHR-OBSERVATION.x.v1",
"name": {"value": "Second"}, "tag": 2}
]
});
let segs = parse("/content[openEHR-EHR-OBSERVATION.x.v1,'Second']");
let found = select_children_matched(&rm, &segs[0], true);
assert_eq!(found.len(), 1);
assert_eq!(found[0]["tag"], 2);
}
#[test]
fn a_redundant_name_conjunct_permits_the_id_only_fallback() {
let table = name_fallback_from_paths(
"/content[openEHR-EHR-OBSERVATION.x.v1]",
[
"/content[openEHR-EHR-OBSERVATION.x.v1]/data[at0001]/events[at0002,'Point in time']/data[at0003]/items[at0004]/value",
"/content[openEHR-EHR-OBSERVATION.x.v1]/language",
]
.into_iter(),
);
let rm = serde_json::json!({
"data": {"archetype_node_id": "at0001", "events": [{
"archetype_node_id": "at0002",
"name": {"value": "at0002"},
"data": {"archetype_node_id": "at0003", "items": [{
"archetype_node_id": "at0004",
"value": {"_type": "DV_TEXT", "value": "hit"}
}]}
}]}
});
let segs =
parse("/data[at0001]/events[at0002,'Point in time']/data[at0003]/items[at0004]/value");
assert!(
resolve(&rm, &segs).is_empty(),
"the strict match locates nothing"
);
let found = resolve_matched(&rm, &segs, &table);
assert_eq!(found.len(), 1);
assert_eq!(found[0]["value"], "hit");
}
#[test]
fn a_discriminating_name_conjunct_refuses_the_id_only_fallback() {
let table = name_fallback_from_paths(
"",
[
"/content[openEHR-EHR-SECTION.adhoc.v1,'Reporting']/items[at0001]/value",
"/content[openEHR-EHR-SECTION.adhoc.v1,'Outcome']/items[at0001]/value",
]
.into_iter(),
);
let rm = serde_json::json!({
"content": [{
"archetype_node_id": "openEHR-EHR-SECTION.adhoc.v1",
"name": {"value": "Neither"},
"items": [{"archetype_node_id": "at0001", "value": {"_type": "DV_TEXT", "value": "x"}}]
}]
});
let segs = parse("/content[openEHR-EHR-SECTION.adhoc.v1,'Reporting']/items[at0001]/value");
assert!(
resolve_matched(&rm, &segs, &table).is_empty(),
"a name-differentiated sibling never widens to the bare node id"
);
}
#[test]
fn an_unqualified_sibling_sharing_the_identity_refuses_the_fallback() {
let table = name_fallback_from_paths(
"",
["/items[at0001,'Named']/value", "/items[at0001]/value"].into_iter(),
);
let segs = parse("/items[at0001,'Named']/value");
let rm = serde_json::json!({
"items": [{"archetype_node_id": "at0001", "name": {"value": "Other"},
"value": {"_type": "DV_TEXT", "value": "x"}}]
});
assert_eq!(resolve_matched(&rm, &segs, &table), Vec::<&Value>::new());
}
#[test]
fn resolves_through_arrays_and_objects() {
let rm = serde_json::json!({
"data": {
"events": [
{"archetype_node_id": "at0002", "value": {"_type": "DV_TEXT", "value": "a"}},
{"archetype_node_id": "at0009", "value": {"_type": "DV_TEXT", "value": "b"}}
]
}
});
let segs = parse("/data/events[at0002]/value");
let found = resolve(&rm, &segs);
assert_eq!(found.len(), 1);
assert_eq!(found[0]["value"], "a");
}
}