#![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::{BTreeMap, BTreeSet, HashSet};
use openehr_rm::v1_2::paths::{PathSegment, RmPath};
use serde_json::{Map, Value, json};
use crate::flat::build::{BuildOptions, structural_from};
use crate::flat::convert::composition_from_flat;
use crate::flat::error::FlatError;
use crate::flat::identity::{recorded_archetype_id, stamp_archetype_details};
use crate::flat::uid::deterministic_uuid;
use crate::flat::webtemplate::model::{
WebTemplate, WebTemplateInput, WebTemplateInputType, WebTemplateNode, WebTemplateRange,
WebTemplateStructuralStub,
};
const EXAMPLE_DATE_TIME: &str = "2022-02-03T04:05:06Z";
const EXAMPLE_DURATION: &str = "PT1H";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DetailLevel {
Required,
Medium,
Complete,
}
impl DetailLevel {
pub fn from_query(value: Option<&str>) -> Result<Self, String> {
match value {
None | Some("required") => Ok(Self::Required),
Some("medium") => Ok(Self::Medium),
Some("complete") => Ok(Self::Complete),
Some(other) => Err(format!(
"unsupported detail_level '{other}' (expected one of required, medium, complete)"
)),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExampleType {
Input,
Output,
}
impl ExampleType {
pub fn from_query(value: Option<&str>) -> Result<Self, String> {
match value {
None | Some("input") => Ok(Self::Input),
Some("output") => Ok(Self::Output),
Some(other) => Err(format!(
"unsupported type '{other}' (expected one of input, output)"
)),
}
}
}
pub fn example_composition(wt: &WebTemplate, level: DetailLevel) -> Result<Value, FlatError> {
if wt.tree.rm_type != "COMPOSITION" {
return Err(FlatError::NoExampleForRoot {
root: wt.tree.rm_type.clone(),
});
}
let mut flat: Map<String, Value> = Map::new();
flat.insert("ctx/language".to_owned(), json!(wt.default_language));
flat.insert("ctx/territory".to_owned(), json!("US"));
flat.insert("ctx/composer_name".to_owned(), json!("Example composer"));
flat.insert("ctx/time".to_owned(), json!(EXAMPLE_DATE_TIME));
walk(&wt.tree, &wt.tree.id, 0, level, false, &mut flat);
let mut value = composition_from_flat(&flat, wt, EXAMPLE_DATE_TIME)?;
fill_mandatory_structures(&mut value, &wt.tree);
let rm_version = BuildOptions::for_template(wt).rm_version;
fill_content_less_sections(&mut value, &wt.tree, rm_version);
Ok(value)
}
pub fn example_root_instance(wt: &WebTemplate, level: DetailLevel) -> Result<Value, FlatError> {
if wt.tree.rm_type == "COMPOSITION" {
return Err(FlatError::Conversion(
"a COMPOSITION-rooted template's example is example_composition".to_owned(),
));
}
let mut flat: Map<String, Value> = Map::new();
walk(&wt.tree, &wt.tree.id, 0, level, false, &mut flat);
let doc = crate::flat::sim::flat::parse_flat(&flat)?;
let mut value = crate::flat::build::build_root(&doc, wt, &BuildOptions::for_template(wt))?;
fill_mandatory_structures(&mut value, &wt.tree);
Ok(value)
}
pub fn example_party(wt: &WebTemplate, level: DetailLevel) -> Result<Value, FlatError> {
let class = party_class(&wt.tree.rm_type).ok_or_else(|| FlatError::NotAPartyTemplate {
root: wt.tree.rm_type.clone(),
})?;
let mut flat: Map<String, Value> = Map::new();
walk(&wt.tree, &wt.tree.id, 0, level, false, &mut flat);
if class == "ROLE" {
let root = &wt.tree.id;
flat.insert(
format!("{root}/performer|id"),
json!(deterministic_uuid(&format!("{root}/performer"))),
);
flat.insert(format!("{root}/performer|namespace"), json!("demographic"));
flat.insert(format!("{root}/performer|type"), json!("PERSON"));
}
let doc = crate::flat::sim::flat::parse_flat(&flat)?;
let mut value = crate::flat::build::build_root(&doc, wt, &BuildOptions::for_template(wt))?;
fill_mandatory_structures(&mut value, &wt.tree);
crate::flat::build::check_party_decodes(&value, class)?;
Ok(value)
}
pub fn example_instance(wt: &WebTemplate, level: DetailLevel) -> Result<Value, FlatError> {
if party_class(&wt.tree.rm_type).is_some() {
example_party(wt, level)
} else {
example_composition(wt, level)
}
}
fn party_class(rm_type: &str) -> Option<&'static str> {
crate::flat::build::PARTY_CLASSES
.into_iter()
.find(|class| *class == rm_type)
}
pub fn apply_output_uid(composition: &mut Value, template_id: &str) {
if let Value::Object(map) = composition {
map.insert(
"uid".to_owned(),
json!({
"_type": "OBJECT_VERSION_ID",
"value": format!("{}::example.server::1", deterministic_uuid(template_id)),
}),
);
}
}
fn walk(
node: &WebTemplateNode,
prefix: &str,
opt_depth: usize,
level: DetailLevel,
force: bool,
out: &mut Map<String, Value>,
) -> bool {
if node.has_input() {
return emit_leaf(node, prefix, out);
}
let (groups, null_branch) = split_null_branch(node);
let cx = WalkCtx {
prefix,
opt_depth,
level,
};
let (mut emitted, mut included) = walk_children(node, &groups, &cx, out);
emitted |= satisfy_cardinalities(node, &groups, &mut included, &cx, out);
emitted |= satisfy_existence(node, &groups, &mut included, &cx, out);
if force
&& !emitted
&& let Some(child) = groups.first()
{
let child_prefix = format!("{prefix}/{}", seg_for(child));
emitted |= walk(child, &child_prefix, opt_depth, level, true, out);
}
if !null_branch.is_empty() && (emitted || force) && !value_emitted(&groups, prefix, out) {
emitted |= walk_null_flavour(&null_branch, &cx, out);
}
emitted
}
struct WalkCtx<'a> {
prefix: &'a str,
opt_depth: usize,
level: DetailLevel,
}
fn walk_children<'a>(
node: &'a WebTemplateNode,
groups: &[&'a WebTemplateNode],
cx: &WalkCtx<'_>,
out: &mut Map<String, Value>,
) -> (bool, Vec<&'a str>) {
let mut emitted = false;
let mut included: Vec<&str> = Vec::new();
let mut card_counts = vec![0usize; node.cardinalities.len()];
for child in groups {
if !rm_declares(&child.aql_path) {
continue;
}
let child_opt = cx.opt_depth + usize::from(is_optional(child));
let include = match cx.level {
DetailLevel::Required => child_opt == 0,
DetailLevel::Medium | DetailLevel::Complete => true,
};
if !include || skips_optional(child) {
continue;
}
let ci = cardinality_index(node, child);
if is_optional(child) && cardinality_full(node, ci, &card_counts) {
continue;
}
included.push(child.aql_path.as_str());
emitted |= walk_child(node, child, cx, &mut card_counts, out);
}
(emitted, included)
}
fn skips_optional(child: &WebTemplateNode) -> bool {
is_optional(child)
&& (child.name_coded.as_ref().is_some_and(|cn| cn.incoherent)
|| is_unsatisfiable_leaf(child))
}
fn cardinality_index(node: &WebTemplateNode, child: &WebTemplateNode) -> Option<usize> {
node.cardinalities
.iter()
.position(|c| c.max != -1 && child.aql_path.starts_with(c.path.as_str()))
}
fn cardinality_full(node: &WebTemplateNode, index: Option<usize>, counts: &[usize]) -> bool {
index.is_some_and(|i| {
let max = node
.cardinalities
.get(i)
.map_or(usize::MAX, |c| usize::try_from(c.max).unwrap_or(usize::MAX));
counts.get(i).is_some_and(|&n| n >= max)
})
}
fn walk_child(
node: &WebTemplateNode,
child: &WebTemplateNode,
cx: &WalkCtx<'_>,
card_counts: &mut [usize],
out: &mut Map<String, Value>,
) -> bool {
let ci = cardinality_index(node, child);
let child_opt = cx.opt_depth + usize::from(is_optional(child));
let child_prefix = format!("{}/{}", cx.prefix, seg_for(child));
let child_force = !is_optional(child);
let source = interval_part_source(node, child);
let mut emitted = walk(source, &child_prefix, child_opt, cx.level, child_force, out);
if emitted && let Some(slot) = ci.and_then(|i| card_counts.get_mut(i)) {
*slot += 1;
}
if cx.level == DetailLevel::Complete
&& emitted
&& is_repeating(child)
&& !cardinality_full(node, ci, card_counts)
{
let second_prefix = format!("{}/{}:1", cx.prefix, child.id);
let second_emitted = walk(child, &second_prefix, child_opt, cx.level, false, out);
emitted |= second_emitted;
if second_emitted && let Some(slot) = ci.and_then(|i| card_counts.get_mut(i)) {
*slot += 1;
}
}
emitted
}
fn interval_part_source<'a>(
node: &'a WebTemplateNode,
child: &'a WebTemplateNode,
) -> &'a WebTemplateNode {
let open = child.implicit && node.rm_type.starts_with("DV_INTERVAL<");
let sibling = || {
node.children
.iter()
.find(|s| !s.implicit && s.rm_type == child.rm_type)
};
open.then(sibling).flatten().unwrap_or(child)
}
fn satisfy_cardinalities<'a>(
node: &WebTemplateNode,
groups: &[&'a WebTemplateNode],
included: &mut Vec<&'a str>,
cx: &WalkCtx<'_>,
out: &mut Map<String, Value>,
) -> bool {
let mut emitted = false;
for card in &node.cardinalities {
let min = card.min.and_then(|m| usize::try_from(m).ok()).unwrap_or(0);
let mut count = included
.iter()
.filter(|p| p.starts_with(card.path.as_str()))
.count();
for child in groups.iter().filter(|c| c.aql_path.starts_with(&card.path)) {
if count >= min {
break;
}
if included.contains(&child.aql_path.as_str()) {
continue;
}
let child_prefix = format!("{}/{}", cx.prefix, seg_for(child));
included.push(child.aql_path.as_str());
emitted |= walk(child, &child_prefix, cx.opt_depth, cx.level, true, out);
count += 1;
}
}
emitted
}
fn satisfy_existence<'a>(
node: &WebTemplateNode,
groups: &[&'a WebTemplateNode],
included: &mut Vec<&'a str>,
cx: &WalkCtx<'_>,
out: &mut Map<String, Value>,
) -> bool {
let mut emitted = false;
for ex in &node.existence {
if ex.min < 1 || included.iter().any(|p| under_attribute(p, &ex.path)) {
continue;
}
if let Some(child) = groups.iter().find(|c| {
under_attribute(&c.aql_path, &ex.path) && rm_declares(&c.aql_path) && !skips_optional(c)
}) {
let child_prefix = format!("{}/{}", cx.prefix, seg_for(child));
included.push(child.aql_path.as_str());
emitted |= walk(child, &child_prefix, cx.opt_depth, cx.level, true, out);
}
}
emitted
}
fn under_attribute(path: &str, attribute_path: &str) -> bool {
path.strip_prefix(attribute_path)
.is_some_and(|rest| rest.is_empty() || rest.starts_with(['[', '/']))
}
fn tail_attribute(aql_path: &str) -> &str {
let tail = aql_path.rsplit('/').next().unwrap_or(aql_path);
tail.split('[').next().unwrap_or(tail)
}
const NULL_BRANCH: [&str; 2] = ["null_flavour", "null_reason"];
fn split_null_branch(node: &WebTemplateNode) -> (Vec<&WebTemplateNode>, Vec<&WebTemplateNode>) {
let groups = child_groups(node);
if node.rm_type != "ELEMENT" {
return (groups, Vec::new());
}
groups
.into_iter()
.partition(|c| !NULL_BRANCH.contains(&tail_attribute(&c.aql_path)))
}
fn value_emitted(groups: &[&WebTemplateNode], prefix: &str, out: &Map<String, Value>) -> bool {
groups.iter().any(|c| {
if tail_attribute(&c.aql_path) != "value" {
return false;
}
let base = format!("{prefix}/{}", seg_for(c));
out.keys().any(|k| {
k.strip_prefix(base.as_str())
.is_some_and(|rest| rest.is_empty() || rest.starts_with(['|', '/', ':']))
})
})
}
fn walk_null_flavour(
null_branch: &[&WebTemplateNode],
cx: &WalkCtx<'_>,
out: &mut Map<String, Value>,
) -> bool {
null_branch
.iter()
.find(|c| tail_attribute(&c.aql_path) == "null_flavour")
.is_some_and(|child| {
let child_prefix = format!("{}/{}", cx.prefix, seg_for(child));
walk(child, &child_prefix, cx.opt_depth, cx.level, true, out)
})
}
fn fill_mandatory_structures(composition: &mut Value, tree: &WebTemplateNode) {
let mut mandatory: BTreeSet<&str> = BTreeSet::new();
let mut stubs: BTreeMap<&str, &WebTemplateStructuralStub> = BTreeMap::new();
collect_mandatory_structures(tree, &mut mandatory, &mut stubs);
for path in mandatory {
let Some(stub) = stubs.get(path) else {
continue;
};
let Some(owner) = path
.strip_suffix(stub.attr.as_str())
.and_then(|p| p.strip_suffix('/'))
else {
continue;
};
let segments = if owner.is_empty() {
Vec::new()
} else {
match owner.parse::<RmPath>() {
Ok(p) => p.segments,
Err(_) => continue,
}
};
let default_name = if stub.rm_type == "HISTORY" {
"History"
} else {
"Tree"
};
for_each_at_mut(composition, &segments, &mut |obj| {
if let Value::Object(m) = obj
&& !m.contains_key(&stub.attr)
{
m.insert(stub.attr.clone(), structural_from(stub, default_name));
}
});
}
}
fn fill_content_less_sections(composition: &mut Value, node: &WebTemplateNode, rm_version: &str) {
for child in &node.children {
if child.rm_type == "SECTION" && child.children.is_empty() {
insert_missing_occurrences(composition, child, rm_version);
}
fill_content_less_sections(composition, child, rm_version);
}
}
fn insert_missing_occurrences(composition: &mut Value, node: &WebTemplateNode, rm_version: &str) {
let Some(min) = node
.min
.and_then(|m| usize::try_from(m).ok())
.filter(|m| *m >= 1)
else {
return;
};
let Ok(path) = node.aql_path.parse::<RmPath>() else {
return;
};
let Some((last, owner)) = path.segments.split_last() else {
return;
};
let name = last
.predicate
.name_value
.clone()
.or_else(|| node.localized_name.clone())
.or_else(|| node.name.clone())
.unwrap_or_else(|| node.id.clone());
let mut instance = Map::new();
instance.insert("_type".into(), json!(node.rm_type));
instance.insert("name".into(), json!({"_type": "DV_TEXT", "value": name}));
if let Some(id) = recorded_archetype_id(node) {
instance.insert("archetype_node_id".into(), json!(id));
}
stamp_archetype_details(&mut instance, node, false, "", rm_version);
let instance = Value::Object(instance);
for_each_at_mut(composition, owner, &mut |obj| {
let Value::Object(m) = obj else {
return;
};
let Value::Array(items) = m
.entry(last.attribute.clone())
.or_insert_with(|| Value::Array(Vec::new()))
else {
return;
};
let present = items
.iter()
.filter(|item| last.predicate.matches(item))
.count();
for _ in present..min {
items.push(instance.clone());
}
});
}
fn collect_mandatory_structures<'a>(
node: &'a WebTemplateNode,
mandatory: &mut BTreeSet<&'a str>,
stubs: &mut BTreeMap<&'a str, &'a WebTemplateStructuralStub>,
) {
for ex in node.existence.iter().filter(|ex| ex.min >= 1) {
mandatory.insert(ex.path.as_str());
}
for stub in &node.structural_stubs {
stubs.entry(stub.path.as_str()).or_insert(stub);
}
for child in &node.children {
collect_mandatory_structures(child, mandatory, stubs);
}
}
fn for_each_at_mut(node: &mut Value, segments: &[PathSegment], f: &mut dyn FnMut(&mut Value)) {
let Some((segment, rest)) = segments.split_first() else {
f(node);
return;
};
let Some(child) = node.get_mut(&segment.attribute) else {
return;
};
match child {
Value::Array(items) => {
for item in items {
if segment.predicate.matches(item) {
for_each_at_mut(item, rest, f);
}
}
}
single => {
if segment.predicate.matches(single) {
for_each_at_mut(single, rest, f);
}
}
}
}
fn child_groups(node: &WebTemplateNode) -> Vec<&WebTemplateNode> {
let mut seen: HashSet<&str> = HashSet::new();
let context = node.rm_type == "EVENT_CONTEXT";
node.children
.iter()
.filter(|c| !context || c.aql_path.contains("/other_context"))
.filter(|c| seen.insert(c.aql_path.as_str()))
.collect()
}
fn is_optional(node: &WebTemplateNode) -> bool {
node.min.unwrap_or(0) < 1
}
fn is_repeating(node: &WebTemplateNode) -> bool {
node.max == -1 || node.max > 1
}
fn rm_declares(aql_path: &str) -> bool {
static DECLARED: std::sync::LazyLock<BTreeSet<&str>> = std::sync::LazyLock::new(|| {
openehr_rm::v1_2::model::classes()
.flat_map(|c| c.attributes.iter().map(|a| a.name))
.collect()
});
let attr = tail_attribute(aql_path);
attr.is_empty() || DECLARED.contains(attr)
}
fn seg_for(node: &WebTemplateNode) -> String {
if is_repeating(node) {
format!("{}:0", node.id)
} else {
node.id.clone()
}
}
fn emit_leaf(node: &WebTemplateNode, base: &str, out: &mut Map<String, Value>) -> bool {
let rm = node.rm_type.split('<').next().unwrap_or(&node.rm_type);
match rm {
"DV_TEXT" | "DV_PARAGRAPH" => {
put(out, base, "", json!(example_text(node)));
}
"DV_URI" => {
let value = first_input(node)
.and_then(|i| i.validation.as_ref())
.and_then(|v| v.pattern.as_deref())
.and_then(example_for_pattern)
.or_else(|| list_value(first_input(node)))
.unwrap_or_else(|| "http://example.org/resource".to_owned());
put(out, base, "", json!(value));
}
"DV_EHR_URI" => {
put(out, base, "", json!(ehr_uri_example(node)));
}
"DV_CODED_TEXT" => emit_coded_text(node, base, out),
"CODE_PHRASE" => emit_code_phrase(node, base, out),
"DV_QUANTITY" => emit_quantity(node, base, out),
"DV_COUNT" => {
let range = first_input(node).and_then(input_range);
put(out, base, "", json!(pick_count(range)));
}
"DV_PROPORTION" => emit_proportion(node, base, out),
"DV_ORDINAL" => emit_ordinal(node, base, out, false),
"DV_SCALE" => emit_ordinal(node, base, out, true),
"DV_BOOLEAN" => put(out, base, "", json!(example_boolean(node))),
"DV_DATE_TIME" | "DV_DATE" | "DV_TIME" => {
put(out, base, "", json!(example_temporal(node, rm)));
}
"DV_DURATION" => put(out, base, "", json!(example_duration(node))),
"DV_IDENTIFIER" => {
let id = input_with_suffix(node, "id")
.and_then(constrained_string_example)
.unwrap_or_else(|| "example-id".to_owned());
put(out, base, "id", json!(id));
for part in ["issuer", "assigner", "type"] {
if let Some(value) =
input_with_suffix(node, part).and_then(constrained_string_example)
{
put(out, base, part, json!(value));
}
}
}
"DV_MULTIMEDIA" => {
put(out, base, "", json!("http://example.org/media"));
let media = node
.code_lists
.iter()
.find(|cl| cl.attr == "media_type")
.and_then(|cl| cl.codes.first())
.map_or("text/plain", String::as_str);
put(out, base, "mediatype", json!(media));
put(out, base, "size", json!(1024));
}
"DV_PARSABLE" => {
let value = input_with_suffix(node, "value")
.or_else(|| first_input(node))
.and_then(|i| i.validation.as_ref())
.and_then(|v| v.pattern.as_deref())
.and_then(example_for_pattern)
.unwrap_or_else(|| "example".to_owned());
let formalism = input_with_suffix(node, "formalism")
.filter(|i| i.list_open != Some(true))
.and_then(|i| i.list.first())
.map_or_else(|| "text".to_owned(), |cv| cv.value.clone());
put(out, base, "", json!(value));
put(out, base, "formalism", json!(formalism));
}
"PARTY_IDENTIFIED" => put(out, base, "name", json!("Example party")),
"PARTY_RELATED" => {
put(out, base, "name", json!("Example party"));
let rel_base = format!("{base}/relationship");
if let Some(rel) = node
.children
.iter()
.find(|c| c.id == "relationship" || c.aql_path.ends_with("/relationship"))
{
emit_coded_text(rel, &rel_base, out);
} else {
put(out, &rel_base, "code", json!("253"));
put(out, &rel_base, "value", json!("unknown"));
put(out, &rel_base, "terminology", json!("openehr"));
}
}
_ => return false,
}
true
}
fn put(out: &mut Map<String, Value>, base: &str, suffix: &str, value: Value) {
let key = if suffix.is_empty() {
base.to_owned()
} else {
format!("{base}|{suffix}")
};
out.insert(key, value);
}
fn example_text(node: &WebTemplateNode) -> String {
if let Some(input) = first_input(node)
&& input.input_type == WebTemplateInputType::Text
&& input.list_open != Some(true)
&& let Some(cv) = input.list.first()
{
return cv.value.clone();
}
if let Some(value) = first_input(node)
.and_then(|i| i.validation.as_ref())
.and_then(|v| v.pattern.as_deref())
.and_then(example_for_pattern)
{
return value;
}
node.name
.clone()
.unwrap_or_else(|| "Example text".to_owned())
}
fn example_for_pattern(pattern: &str) -> Option<String> {
const META: &[char] = &[
'.', '^', '$', '*', '+', '?', '(', ')', '[', ']', '{', '}', '|', '\\',
];
let body = pattern
.strip_prefix('/')
.and_then(|p| p.strip_suffix('/'))
.unwrap_or(pattern);
if body.is_empty() || body.chars().any(|c| META.contains(&c)) {
return None;
}
if pattern_matches(body, body) {
Some(body.to_owned())
} else {
None
}
}
fn constrained_string_example(input: &WebTemplateInput) -> Option<String> {
if input.list_open != Some(true)
&& let Some(cv) = input.list.first()
{
return Some(cv.value.clone());
}
let pattern = input.validation.as_ref()?.pattern.as_deref()?;
example_for_pattern(pattern).or_else(|| {
let body = pattern
.strip_prefix('/')
.and_then(|p| p.strip_suffix('/'))
.unwrap_or(pattern);
(!body.is_empty() && pattern_matches(pattern, body)).then(|| body.to_owned())
})
}
fn pattern_matches(pattern: &str, value: &str) -> bool {
let body = pattern
.strip_prefix('/')
.and_then(|p| p.strip_suffix('/'))
.unwrap_or(pattern);
match regex::Regex::new(&format!("^(?:{body})$")) {
Ok(re) => re.is_match(value),
Err(_) => true,
}
}
fn value_pattern(node: &WebTemplateNode) -> Option<&str> {
node.inputs
.first()
.and_then(|i| i.validation.as_ref())
.and_then(|v| v.pattern.as_deref())
}
const EHR_URI_DEFAULT: &str = "ehr://example.org/composition";
fn ehr_uri_example(node: &WebTemplateNode) -> String {
let Some(pattern) = value_pattern(node) else {
return EHR_URI_DEFAULT.to_owned();
};
if pattern_matches(pattern, EHR_URI_DEFAULT) {
return EHR_URI_DEFAULT.to_owned();
}
match example_for_pattern(pattern) {
Some(lit) if lit.starts_with("ehr:") => lit,
_ => EHR_URI_DEFAULT.to_owned(),
}
}
fn is_unsatisfiable_leaf(node: &WebTemplateNode) -> bool {
let rm = node.rm_type.split('<').next().unwrap_or(&node.rm_type);
if rm != "DV_EHR_URI" {
return false;
}
value_pattern(node).is_some_and(|p| !pattern_matches(p, &ehr_uri_example(node)))
}
fn emit_coded_text(node: &WebTemplateNode, base: &str, out: &mut Map<String, Value>) {
let input = input_with_suffix(node, "code").or_else(|| first_input(node));
let (code, value, terminology) = coded_example(input);
put(out, base, "code", json!(code));
put(out, base, "value", json!(value));
put(out, base, "terminology", json!(terminology));
}
fn emit_code_phrase(node: &WebTemplateNode, base: &str, out: &mut Map<String, Value>) {
let input = input_with_suffix(node, "code").or_else(|| first_input(node));
let (code, _value, terminology) = coded_example(input);
put(out, base, "code", json!(code));
put(out, base, "terminology", json!(terminology));
}
fn coded_example(input: Option<&WebTemplateInput>) -> (String, String, String) {
match input {
Some(i) if !i.list.is_empty() => {
let Some(cv) = i.list.first() else {
return (String::new(), String::new(), "local".to_owned());
};
let terminology = i.terminology.clone().unwrap_or_else(|| "local".to_owned());
let value = cv.label.clone().unwrap_or_else(|| cv.value.clone());
(cv.value.clone(), value, terminology)
}
Some(i) => (
"at0000".to_owned(),
"Example".to_owned(),
i.terminology.clone().unwrap_or_else(|| "local".to_owned()),
),
None => (
"at0000".to_owned(),
"Example".to_owned(),
"local".to_owned(),
),
}
}
fn emit_quantity(node: &WebTemplateNode, base: &str, out: &mut Map<String, Value>) {
let unit_input = input_with_suffix(node, "unit");
let unit = unit_input
.and_then(|i| i.list.first())
.map(|cv| cv.value.clone())
.or_else(|| {
node.quantity_property.as_deref().and_then(|p| {
let units = openehr_term::bundle::openehr().units_for_property(p);
units
.iter()
.find(|u| u.primary)
.or_else(|| units.first())
.map(|u| u.text.clone())
})
})
.unwrap_or_else(|| "1".to_owned());
let range = input_with_suffix(node, "magnitude")
.and_then(input_range)
.or_else(|| {
unit_input
.and_then(|i| i.list.first())
.and_then(|cv| cv.validation.as_ref())
.and_then(|v| v.range.as_ref())
});
put(out, base, "magnitude", json!(pick_decimal(range)));
put(out, base, "unit", json!(unit));
}
fn emit_proportion(node: &WebTemplateNode, base: &str, out: &mut Map<String, Value>) {
let type_code = match node.proportion_types.first().map(String::as_str) {
Some("unitary") => 1,
Some("percent") => 2,
Some("fraction") => 3,
Some("integer_fraction") => 4,
_ => 0,
};
let num_input = input_with_suffix(node, "numerator");
let den_input = input_with_suffix(node, "denominator");
let integral = matches!(type_code, 3 | 4)
|| num_input.is_some_and(|i| i.input_type == WebTemplateInputType::Integer)
|| den_input.is_some_and(|i| i.input_type == WebTemplateInputType::Integer);
let numerator = pick_proportion_part(num_input.and_then(input_range), integral, false);
let denominator = match type_code {
1 => 1.0, 2 => 100.0, _ => pick_proportion_part(den_input.and_then(input_range), integral, true),
};
put(out, base, "numerator", json!(numerator));
put(out, base, "denominator", json!(denominator));
put(out, base, "type", json!(type_code));
}
fn pick_proportion_part(range: Option<&WebTemplateRange>, integral: bool, avoid_zero: bool) -> f64 {
let v = pick_decimal(range);
if !integral {
return v;
}
for cand in [v.round(), v.floor(), v.ceil()] {
if in_decimal_range(cand, range) && !(avoid_zero && cand == 0.0) {
return cand;
}
}
v
}
fn in_decimal_range(v: f64, range: Option<&WebTemplateRange>) -> bool {
let Some(r) = range else { return true };
if let Some(min) = r.min.as_ref().and_then(Value::as_f64) {
let ok = if r.min_op.as_deref() == Some(">") {
v > min
} else {
v >= min
};
if !ok {
return false;
}
}
if let Some(max) = r.max.as_ref().and_then(Value::as_f64) {
let ok = if r.max_op.as_deref() == Some("<") {
v < max
} else {
v <= max
};
if !ok {
return false;
}
}
true
}
fn emit_ordinal(node: &WebTemplateNode, base: &str, out: &mut Map<String, Value>, scale: bool) {
let numeric_suffix = if scale { "scale" } else { "ordinal" };
if let Some(cv) = first_input(node).and_then(|i| i.list.first()) {
let numeric = if scale {
json!(cv.scale.unwrap_or(0.0))
} else {
json!(cv.ordinal.unwrap_or(0))
};
put(out, base, numeric_suffix, numeric);
put(out, base, "code", json!(cv.value));
put(
out,
base,
"value",
json!(cv.label.clone().unwrap_or_else(|| cv.value.clone())),
);
} else {
put(
out,
base,
numeric_suffix,
if scale { json!(0.0) } else { json!(0) },
);
put(out, base, "code", json!("at0000"));
put(out, base, "value", json!("Example"));
}
}
fn example_temporal(node: &WebTemplateNode, rm: &str) -> String {
let pattern = node
.inputs
.first()
.and_then(|i| i.validation.as_ref())
.and_then(|v| v.pattern.as_deref());
let (pat_date, pat_time) = match pattern {
Some(p) if p.contains('T') => {
let (d, t) = p.split_once('T').unwrap_or((p, ""));
(d, t)
}
Some(p) if p.contains(':') => ("", p),
Some(p) => (p, ""),
None => ("", ""),
};
let tz = if node.tz_validity == Some(1003) {
""
} else {
"Z"
};
let date = temporal_segments(pat_date, DATE_SEGMENTS, '-');
match rm {
"DV_DATE" => date,
"DV_TIME" => format!("{}{tz}", temporal_segments(pat_time, TIME_SEGMENTS, ':')),
_ if pat_time.starts_with("XX") => date,
_ => format!(
"{date}T{}{tz}",
temporal_segments(pat_time, TIME_SEGMENTS, ':')
),
}
}
const DATE_SEGMENTS: [&str; 3] = ["2022", "02", "03"];
const TIME_SEGMENTS: [&str; 3] = ["04", "05", "06"];
fn temporal_segments(pattern: &str, full: [&str; 3], separator: char) -> String {
let segments: Vec<&str> = if pattern.is_empty() {
Vec::new()
} else {
pattern.splitn(3, separator).collect()
};
let mut out = String::from(full[0]);
for (i, part) in full.iter().enumerate().skip(1) {
if segments.get(i).is_some_and(|s| *s == "XX") {
break;
}
out.push(separator);
out.push_str(part);
}
out
}
fn example_duration(node: &WebTemplateNode) -> String {
const ISO: &[(&str, bool, char, f64)] = &[
("year", false, 'Y', 31_557_600.0),
("month", false, 'M', 2_629_800.0),
("week", false, 'W', 604_800.0),
("day", false, 'D', 86_400.0),
("hour", true, 'H', 3_600.0),
("minute", true, 'M', 60.0),
("second", true, 'S', 1.0),
];
let allowed: Vec<&str> = node
.inputs
.iter()
.filter_map(|i| i.suffix.as_deref())
.collect();
let usable: Vec<&(&str, bool, char, f64)> = ISO
.iter()
.filter(|f| allowed.is_empty() || allowed.len() >= 7 || allowed.contains(&f.0))
.collect();
let fmt = |field: &(&str, bool, char, f64), n: f64| -> String {
let (_, is_time, letter, _) = *field;
if is_time {
format!("PT{n:.0}{letter}")
} else {
format!("P{n:.0}{letter}")
}
};
let Some(range) = node.duration_range.as_ref() else {
return usable
.first()
.map_or_else(|| EXAMPLE_DURATION.to_owned(), |field| fmt(field, 1.0));
};
let field = usable
.last()
.copied()
.unwrap_or(&("hour", true, 'H', 3_600.0));
let unit = field.3;
let min_secs = range.min.as_ref().and_then(Value::as_str).map(iso_seconds);
let max_secs = range.max.as_ref().and_then(Value::as_str).map(iso_seconds);
let min_excl = range.min_op.as_deref() == Some(">");
let max_excl = range.max_op.as_deref() == Some("<");
let n_lo = match min_secs {
Some(m) if min_excl => (m / unit).floor() + 1.0,
Some(m) => (m / unit).ceil(),
None => 0.0,
}
.max(0.0);
let n_hi = match max_secs {
Some(m) if max_excl => (m / unit).ceil() - 1.0,
Some(m) => (m / unit).floor(),
None => f64::INFINITY,
};
if n_lo > n_hi {
return range
.min
.as_ref()
.and_then(Value::as_str)
.map_or_else(|| fmt(field, 1.0), str::to_owned);
}
let target = match max_secs {
Some(mx) => f64::midpoint(min_secs.unwrap_or(0.0), mx),
None => min_secs.unwrap_or(0.0) + unit,
};
let n = (target / unit).round().max(n_lo).min(n_hi);
fmt(field, n)
}
fn iso_seconds(value: &str) -> f64 {
let rest = value.strip_prefix('-').unwrap_or(value);
let Some(rest) = rest.strip_prefix('P') else {
return 0.0;
};
let (date_part, time_part) = rest.split_once('T').unwrap_or((rest, ""));
iso_part_seconds(date_part, false) + iso_part_seconds(time_part, true)
}
fn iso_part_seconds(part: &str, in_time: bool) -> f64 {
let mut total = 0.0;
let mut num = String::new();
for ch in part.chars() {
if ch.is_ascii_digit() || ch == '.' || ch == ',' {
num.push(if ch == ',' { '.' } else { ch });
continue;
}
let n: f64 = num.parse().unwrap_or(0.0);
num.clear();
total += n * iso_designator_seconds(ch, in_time);
}
total
}
fn iso_designator_seconds(designator: char, in_time: bool) -> f64 {
match (designator, in_time) {
('Y', false) => 31_557_600.0,
('M', false) => 2_629_800.0,
('W', false) => 604_800.0,
('D', false) => 86_400.0,
('H', true) => 3_600.0,
('M', true) => 60.0,
('S', true) => 1.0,
_ => 0.0,
}
}
fn example_boolean(node: &WebTemplateNode) -> bool {
let only_false = first_input(node).is_some_and(|i| {
i.list.iter().any(|c| c.value == "false") && !i.list.iter().any(|c| c.value == "true")
});
!only_false
}
fn first_input(node: &WebTemplateNode) -> Option<&WebTemplateInput> {
node.inputs.first()
}
fn input_with_suffix<'a>(node: &'a WebTemplateNode, suffix: &str) -> Option<&'a WebTemplateInput> {
node.inputs
.iter()
.find(|i| i.suffix.as_deref() == Some(suffix))
}
fn list_value(input: Option<&WebTemplateInput>) -> Option<String> {
let input = input?;
if input.list_open == Some(true) {
return None;
}
input.list.first().map(|cv| cv.value.clone())
}
fn input_range(input: &WebTemplateInput) -> Option<&WebTemplateRange> {
input.validation.as_ref().and_then(|v| v.range.as_ref())
}
fn pick_decimal(range: Option<&WebTemplateRange>) -> f64 {
const DEFAULT_DECIMAL: f64 = 10.0;
const STEP: f64 = 1.0;
let Some(r) = range else {
return DEFAULT_DECIMAL;
};
let lo = r.min.as_ref().and_then(Value::as_f64).map(|min| {
if r.min_op.as_deref() == Some(">") {
min + STEP
} else {
min
}
});
let hi = r.max.as_ref().and_then(Value::as_f64).map(|max| {
if r.max_op.as_deref() == Some("<") {
max - STEP
} else {
max
}
});
match (lo, hi) {
(Some(lo), Some(hi)) => {
let mid = f64::midpoint(lo, hi);
if mid == 0.0 && hi > 0.0 {
hi / 2.0
} else {
mid
}
}
(Some(lo), None) => lo.max(DEFAULT_DECIMAL),
(None, Some(hi)) => {
let v = hi.min(DEFAULT_DECIMAL);
if v == 0.0 { hi - STEP } else { v }
}
(None, None) => DEFAULT_DECIMAL,
}
}
fn pick_count(range: Option<&WebTemplateRange>) -> i64 {
let Some(r) = range else { return 0 };
let mut v: i64 = 0;
if let Some(min) = r.min.as_ref().and_then(Value::as_i64) {
let lo = if r.min_op.as_deref() == Some(">") {
min + 1
} else {
min
};
if v < lo {
v = lo;
}
}
if let Some(max) = r.max.as_ref().and_then(Value::as_i64) {
let hi = if r.max_op.as_deref() == Some("<") {
max - 1
} else {
max
};
if v > hi {
v = hi;
}
}
v
}
#[cfg(test)]
mod tests {
use super::*;
use crate::flat::webtemplate::builder::build_web_template;
fn web_template(opt_rel: &str) -> WebTemplate {
let path = format!("{}/{opt_rel}", env!("CARGO_MANIFEST_DIR"));
let xml = std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("read {path}: {e}"));
let opt = openehr_its::opt14::from_xml(&xml).expect("parse OPT");
build_web_template(&opt).expect("build web template")
}
fn demo_vitals() -> WebTemplate {
web_template("tests/fixtures/better/Demo Vitals.opt")
}
fn leaf_count(comp: &Value) -> usize {
fn rec(v: &Value, n: &mut usize) {
match v {
Value::Object(m) => {
if m.get("_type")
.and_then(Value::as_str)
.is_some_and(|t| t.starts_with("DV_"))
{
*n += 1;
}
for val in m.values() {
rec(val, n);
}
}
Value::Array(a) => {
for e in a {
rec(e, n);
}
}
_ => {}
}
}
let mut n = 0;
rec(comp, &mut n);
n
}
#[test]
fn parses_detail_level() {
assert_eq!(DetailLevel::from_query(None), Ok(DetailLevel::Required));
assert_eq!(
DetailLevel::from_query(Some("required")),
Ok(DetailLevel::Required)
);
assert_eq!(
DetailLevel::from_query(Some("medium")),
Ok(DetailLevel::Medium)
);
assert_eq!(
DetailLevel::from_query(Some("complete")),
Ok(DetailLevel::Complete)
);
assert!(DetailLevel::from_query(Some("full")).is_err());
}
#[test]
fn parses_example_type() {
assert_eq!(ExampleType::from_query(None), Ok(ExampleType::Input));
assert_eq!(
ExampleType::from_query(Some("input")),
Ok(ExampleType::Input)
);
assert_eq!(
ExampleType::from_query(Some("output")),
Ok(ExampleType::Output)
);
assert!(ExampleType::from_query(Some("bogus")).is_err());
}
#[test]
fn refuses_present_out_of_enum_values() {
for bad in ["", " ", " required ", "REQUIRED", "required\n"] {
assert!(
DetailLevel::from_query(Some(bad)).is_err(),
"detail_level {bad:?} must refuse"
);
}
for bad in ["", " ", " input ", "INPUT", "output "] {
assert!(
ExampleType::from_query(Some(bad)).is_err(),
"type {bad:?} must refuse"
);
}
}
#[test]
fn is_deterministic() {
let wt = demo_vitals();
let a = example_composition(&wt, DetailLevel::Medium).expect("the example builds");
let b = example_composition(&wt, DetailLevel::Medium).expect("the example builds");
assert_eq!(a, b, "two calls produce identical JSON");
}
#[test]
fn is_a_composition_with_housekeeping() {
let wt = demo_vitals();
let comp = example_composition(&wt, DetailLevel::Required).expect("the example builds");
assert_eq!(
comp.get("_type").and_then(Value::as_str),
Some("COMPOSITION")
);
assert!(comp.get("language").is_some(), "mandatory language present");
assert!(
comp.get("territory").is_some(),
"mandatory territory present"
);
assert!(comp.get("composer").is_some(), "mandatory composer present");
assert!(comp.get("category").is_some(), "mandatory category present");
assert_eq!(
comp.pointer("/composer/name").and_then(Value::as_str),
Some("Example composer")
);
assert_eq!(
comp.pointer("/archetype_details/template_id/value")
.and_then(Value::as_str),
Some(wt.template_id.as_str()),
"self-describing template id"
);
}
#[test]
fn medium_populates_optional_content() {
let wt = demo_vitals();
let comp = example_composition(&wt, DetailLevel::Medium).expect("the example builds");
let content = comp.get("content").and_then(Value::as_array);
assert!(
content.is_some_and(|c| !c.is_empty()),
"medium example carries content entries"
);
assert!(
leaf_count(&comp)
> leaf_count(
&example_composition(&wt, DetailLevel::Required).expect("the example builds")
),
"medium populates strictly more leaves than required"
);
}
#[test]
fn complete_demonstrates_repetition() {
let wt = demo_vitals();
let medium = example_composition(&wt, DetailLevel::Medium).expect("the example builds");
let complete = example_composition(&wt, DetailLevel::Complete).expect("the example builds");
assert!(
leaf_count(&complete) > leaf_count(&medium),
"complete adds second occurrences of repeating nodes"
);
}
#[test]
fn levels_are_monotonic() {
let wt = demo_vitals();
let required = leaf_count(
&example_composition(&wt, DetailLevel::Required).expect("the example builds"),
);
let medium =
leaf_count(&example_composition(&wt, DetailLevel::Medium).expect("the example builds"));
let complete = leaf_count(
&example_composition(&wt, DetailLevel::Complete).expect("the example builds"),
);
assert!(
required <= medium && medium <= complete,
"coverage grows with detail: required={required}, medium={medium}, complete={complete}"
);
assert!(
complete > required,
"a rich template exposes more leaves at complete than required \
(required={required}, complete={complete})"
);
}
#[test]
fn output_populates_a_deterministic_uid() {
let wt = demo_vitals();
let mut input =
example_composition(&wt, DetailLevel::Required).expect("the example builds");
assert!(input.get("uid").is_none(), "input form has no uid");
let mut output = input.clone();
apply_output_uid(&mut output, &wt.template_id);
let uid = output
.pointer("/uid/value")
.and_then(Value::as_str)
.expect("output uid");
assert!(
uid.ends_with("::example.server::1"),
"OBJECT_VERSION_ID form: {uid}"
);
assert_eq!(
output.pointer("/uid/_type").and_then(Value::as_str),
Some("OBJECT_VERSION_ID")
);
apply_output_uid(&mut input, &wt.template_id);
assert_eq!(input.get("uid"), output.get("uid"));
}
fn range(
min: Option<f64>,
min_op: Option<&str>,
max: Option<f64>,
max_op: Option<&str>,
) -> WebTemplateRange {
WebTemplateRange {
min_op: min_op.map(str::to_owned),
min: min.map(|m| json!(m)),
max_op: max_op.map(str::to_owned),
max: max.map(|m| json!(m)),
}
}
#[test]
fn pick_decimal_is_non_zero_and_in_range() {
assert_eq!(pick_decimal(None), 10.0);
assert_eq!(
pick_decimal(Some(&range(Some(0.0), None, Some(10.0), None))),
5.0
);
assert_eq!(
pick_decimal(Some(&range(Some(60.0), None, Some(80.0), None))),
70.0
);
let v = pick_decimal(Some(&range(Some(-5.0), None, Some(5.0), None)));
assert!(
v != 0.0 && (-5.0..=5.0).contains(&v),
"in range, non-zero: {v}"
);
assert_eq!(
pick_decimal(Some(&range(Some(50.0), None, None, None))),
50.0
);
assert_eq!(
pick_decimal(Some(&range(Some(0.0), None, None, None))),
10.0
);
assert_eq!(
pick_decimal(Some(&range(None, None, Some(100.0), None))),
10.0
);
assert_eq!(pick_decimal(Some(&range(None, None, Some(4.0), None))), 4.0);
assert_eq!(
pick_decimal(Some(&range(Some(0.0), Some(">"), None, None))),
10.0
);
let hi = pick_decimal(Some(&range(None, None, Some(1.0), Some("<"))));
assert!(hi < 1.0, "strictly below an open upper bound: {hi}");
}
#[test]
fn pick_decimal_populated_magnitudes_are_non_zero() {
fn collect(v: &Value, out: &mut Vec<f64>) {
match v {
Value::Object(m) => {
if m.get("_type").and_then(Value::as_str) == Some("DV_QUANTITY")
&& let Some(x) = m.get("magnitude").and_then(Value::as_f64)
{
out.push(x);
}
m.values().for_each(|c| collect(c, out));
}
Value::Array(a) => a.iter().for_each(|e| collect(e, out)),
_ => {}
}
}
let wt = demo_vitals();
let comp = example_composition(&wt, DetailLevel::Medium).expect("the example builds");
let mut mags = Vec::new();
collect(&comp, &mut mags);
assert!(!mags.is_empty(), "Demo Vitals exposes DV_QUANTITY leaves");
assert!(
mags.iter().all(|m| *m != 0.0),
"no example magnitude is zero: {mags:?}"
);
}
}