#![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)"
)]
pub(crate) mod data_values;
pub(crate) mod parties;
pub(crate) mod structures;
use serde_json::{Value, json};
use crate::flat::error::FlatError;
use crate::flat::sim::{SimChild, SimNode};
use crate::flat::webtemplate::model::{WebTemplateCodedValue, WebTemplateNode};
pub(crate) fn build_leaf(
node: &SimNode,
rm_type: &str,
wt_node: Option<&WebTemplateNode>,
path: &str,
) -> Result<Value, FlatError> {
let base = base_type(rm_type);
if let Some(raw) = node.attrs.get("raw") {
if raw.get("_type").and_then(Value::as_str).is_some() {
return Ok(raw.clone());
}
return Err(FlatError::RawWithoutType {
path: path.to_owned(),
});
}
if base == "DV_STATE" {
return Err(FlatError::NoSimplifiedForm {
rm_type: base.to_owned(),
path: path.to_owned(),
});
}
reject_unknown_suffixes(node, base, path)?;
if let Some(other) = node.attrs.get("other") {
if ["code", "value", "terminology", "preferred_term"]
.iter()
.any(|s| node.attrs.contains_key(*s))
{
return Err(FlatError::OtherSuffixConflict(path.to_owned()));
}
if list_open_of(wt_node) == Some(false) {
return Err(FlatError::OtherOnClosedValueSet(path.to_owned()));
}
return Ok(json!({"_type": "DV_TEXT", "value": other.clone()}));
}
data_values::build_leaf(node, rm_type, wt_node).ok_or_else(|| FlatError::InvalidValue {
path: path.to_owned(),
reason: format!("the datum parts do not form a valid {base}"),
})
}
pub(crate) fn build_rm_attr(
seg: &str,
occurrences: &[SimNode],
host_rm_type: &str,
path: &str,
) -> Result<Option<(String, Value)>, FlatError> {
structures::build_rm_attr(seg, occurrences, base_type(host_rm_type), path)
}
pub(crate) fn raw_value(
node: &SimNode,
path: &str,
types: &[&str],
) -> Option<Result<Value, FlatError>> {
let raw = node.attrs.get("raw")?;
let invalid = |reason: String| FlatError::InvalidRaw {
path: path.to_owned(),
reason,
};
let Some(rm_type) = raw.get("_type").and_then(Value::as_str) else {
return Some(Err(FlatError::RawWithoutType {
path: path.to_owned(),
}));
};
if !types.contains(&rm_type) {
return Some(Err(invalid(format!(
"the |raw value is a {rm_type}, expected one of {}",
types.join(", ")
))));
}
if node.attrs.len() > 1 || !node.children.is_empty() {
return Some(Err(invalid(
"|raw carries the whole value, so no other suffix or sub-path may stand beside it"
.to_owned(),
)));
}
Some(Ok(raw.clone()))
}
pub(crate) const PARTY_PROXY_CLASSES: &[&str] =
&["PARTY_SELF", "PARTY_IDENTIFIED", "PARTY_RELATED"];
pub(super) fn base_type(rm_type: &str) -> &str {
rm_type.split('<').next().unwrap_or(rm_type)
}
pub(super) fn code_phrase(terminology: &str, code: &str) -> Value {
json!({
"_type": "CODE_PHRASE",
"terminology_id": {"_type": "TERMINOLOGY_ID", "value": terminology},
"code_string": code,
})
}
pub(super) fn code_phrase_obj(
code: Value,
terminology: &str,
preferred_term: Option<Value>,
) -> Value {
let mut o = serde_json::Map::new();
o.insert("_type".to_owned(), json!("CODE_PHRASE"));
o.insert(
"terminology_id".to_owned(),
json!({"_type": "TERMINOLOGY_ID", "value": terminology}),
);
o.insert("code_string".to_owned(), code);
if let Some(pt) = preferred_term {
o.insert("preferred_term".to_owned(), pt);
}
Value::Object(o)
}
pub(super) fn family<'a>(node: &'a SimNode, name: &str) -> Option<&'a SimChild> {
node.children.get(name)
}
pub(crate) fn coded_from_group(group_id: &str, token: &str) -> Value {
let term = openehr_term::bundle::openehr();
if term.is_valid_code(group_id, token) {
let value = term.rubric(group_id, token, "en").unwrap_or(token);
return coded_text("openehr", token, value);
}
if let Some(concept) = term
.concepts_in_group(group_id)
.iter()
.find(|c| c.rubric == token)
{
return coded_text("openehr", &concept.id, token);
}
coded_text("openehr", token, token)
}
pub(super) fn coded_text(terminology: &str, code: &str, value: &str) -> Value {
json!({
"_type": "DV_CODED_TEXT",
"value": value,
"defining_code": code_phrase(terminology, code),
})
}
fn list_open_of(wt_node: Option<&WebTemplateNode>) -> Option<bool> {
wt_node?.inputs.iter().find_map(|i| i.list_open)
}
pub(super) fn wt_terminology(wt_node: Option<&WebTemplateNode>) -> Option<&str> {
wt_node?
.inputs
.iter()
.find_map(|i| i.terminology.as_deref())
}
pub(super) fn wt_coded_value<'a>(
wt_node: Option<&'a WebTemplateNode>,
code: &str,
) -> Option<&'a WebTemplateCodedValue> {
wt_node?
.inputs
.iter()
.flat_map(|i| i.list.iter())
.find(|cv| cv.value == code)
}
fn is_known_suffix(base: &str, suffix: &str) -> bool {
const AMOUNT_EXTRAS: &[&str] = &[
"magnitude_status",
"normal_status",
"accuracy",
"accuracy_is_percent",
];
let base_ok: &[&str] = match base {
"DV_TEXT" | "DV_PARAGRAPH" => &["value", "formatting"],
"DV_CODED_TEXT" => &[
"code",
"value",
"terminology",
"preferred_term",
"formatting",
],
"CODE_PHRASE" => &["code", "terminology", "preferred_term"],
"DV_QUANTITY" => &[
"magnitude",
"unit",
"precision",
"magnitude_status",
"normal_status",
"accuracy",
"accuracy_is_percent",
],
"DV_COUNT" | "DV_DURATION" => AMOUNT_EXTRAS,
"DV_PROPORTION" => &[
"numerator",
"denominator",
"type",
"precision",
"magnitude_status",
"normal_status",
"accuracy",
"accuracy_is_percent",
],
"DV_ORDINAL" => &["code", "value", "ordinal", "terminology"],
"DV_SCALE" => &["code", "value", "scale", "terminology"],
"DV_DATE" | "DV_DATE_TIME" | "DV_TIME" => &["magnitude_status", "normal_status"],
"DV_IDENTIFIER" => &["id", "issuer", "assigner", "type"],
"PARTY_PROXY" | "PARTY_SELF" | "PARTY_IDENTIFIED" | "PARTY_RELATED" => {
&["name", "id", "id_scheme", "id_namespace", "_type"]
}
"DV_PARSABLE" => &["value", "formalism"],
"DV_MULTIMEDIA" => &[
"mediatype",
"size",
"alternatetext",
"compression_algorithm",
"integrity_check",
"integrity_check_algorithm",
"data",
],
"DV_INTERVAL" => &[
"lower_unbounded",
"upper_unbounded",
"lower_included",
"upper_included",
],
_ => &[],
};
base_ok.contains(&suffix)
}
fn reject_unknown_suffixes(node: &SimNode, base: &str, path: &str) -> Result<(), FlatError> {
for key in node.attrs.keys() {
if key.is_empty() || key == "raw" || key == "other" {
continue;
}
let head = key.split('|').next().unwrap_or(key);
if !is_known_suffix(base, head) {
return Err(FlatError::UnknownSuffix {
rm_type: base.to_owned(),
suffix: key.clone(),
path: path.to_owned(),
});
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn leaf(pairs: &[(&str, Value)]) -> SimNode {
let mut n = SimNode::default();
for (k, v) in pairs {
n.attrs.insert((*k).to_owned(), v.clone());
}
n
}
#[test]
fn raw_bypass_returns_verbatim() {
let node = leaf(&[(
"raw",
json!({"_type": "DV_QUANTITY", "magnitude": 120, "unit": "mm[Hg]"}),
)]);
let v = build_leaf(&node, "DV_TEXT", None, "p").unwrap();
assert_eq!(v["_type"], "DV_QUANTITY");
assert_eq!(v["magnitude"], 120);
}
#[test]
fn raw_without_type_is_rejected() {
let node = leaf(&[("raw", json!({"magnitude": 1}))]);
assert!(matches!(
build_leaf(&node, "DV_TEXT", None, "p"),
Err(FlatError::RawWithoutType { .. })
));
}
#[test]
fn other_conflicts_with_code() {
let node = leaf(&[("other", json!("free")), ("code", json!("at0001"))]);
assert!(matches!(
build_leaf(&node, "DV_CODED_TEXT", None, "p"),
Err(FlatError::OtherSuffixConflict(_))
));
}
#[test]
fn unknown_suffix_rejected() {
let node = leaf(&[("magnitude", json!(1)), ("frobnicate", json!(2))]);
assert!(matches!(
build_leaf(&node, "DV_QUANTITY", None, "p"),
Err(FlatError::UnknownSuffix { .. })
));
}
}