#![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::HashSet;
use serde_json::{Map, Value, json};
use crate::flat::convert::composition_from_flat;
use crate::flat::webtemplate::{
WebTemplate, WebTemplateInput, WebTemplateInputType, WebTemplateNode, WebTemplateRange,
};
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.map(str::trim) {
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.map(str::trim) {
None | Some("" | "input") => Ok(Self::Input),
Some("output") => Ok(Self::Output),
Some(other) => Err(format!(
"unsupported type '{other}' (expected one of input, output)"
)),
}
}
}
#[must_use]
pub fn example_composition(wt: &WebTemplate, level: DetailLevel) -> Value {
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);
match composition_from_flat(&flat, wt, EXAMPLE_DATE_TIME) {
Ok(value) => value,
Err(_) => Value::Object(Map::new()),
}
}
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 = child_groups(node);
let mut emitted = false;
let mut included: Vec<&str> = Vec::new();
let mut card_counts = vec![0usize; node.cardinalities.len()];
let card_idx = |child: &WebTemplateNode| {
node.cardinalities
.iter()
.position(|c| c.max != -1 && child.aql_path.starts_with(c.path.as_str()))
};
for child in &groups {
if !rm_declares(&child.aql_path) {
continue;
}
let child_opt = opt_depth + usize::from(is_optional(child));
let include = match level {
DetailLevel::Required => child_opt == 0,
DetailLevel::Medium | DetailLevel::Complete => true,
};
if include {
if is_optional(child)
&& (child.name_coded.as_ref().is_some_and(|cn| cn.incoherent)
|| is_unsatisfiable_leaf(child))
{
continue;
}
let ci = card_idx(child);
let card_full = |counts: &[usize]| {
ci.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)
})
};
if is_optional(child) && card_full(&card_counts) {
continue;
}
included.push(child.aql_path.as_str());
let child_prefix = format!("{prefix}/{}", seg_for(child));
let child_force = !is_optional(child);
let child_emitted = walk(child, &child_prefix, child_opt, level, child_force, out);
emitted |= child_emitted;
if child_emitted && let Some(slot) = ci.and_then(|i| card_counts.get_mut(i)) {
*slot += 1;
}
if level == DetailLevel::Complete
&& child_emitted
&& is_repeating(child)
&& !card_full(&card_counts)
{
let second_prefix = format!("{prefix}/{}:1", child.id);
let second_emitted = walk(child, &second_prefix, child_opt, level, false, out);
emitted |= second_emitted;
if second_emitted && let Some(slot) = ci.and_then(|i| card_counts.get_mut(i)) {
*slot += 1;
}
}
}
}
for card in &node.cardinalities {
if card.min.unwrap_or(0) < 1 {
continue;
}
let satisfied = included.iter().any(|p| p.starts_with(card.path.as_str()));
if !satisfied
&& let Some(child) = groups.iter().find(|c| c.aql_path.starts_with(&card.path))
{
let child_prefix = format!("{prefix}/{}", seg_for(child));
emitted |= walk(child, &child_prefix, opt_depth, level, true, 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);
}
emitted
}
fn child_groups(node: &WebTemplateNode) -> Vec<&WebTemplateNode> {
let mut seen: HashSet<&str> = HashSet::new();
node.children
.iter()
.filter(|c| c.rm_type != "EVENT_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<std::collections::BTreeSet<&'static str>> =
std::sync::LazyLock::new(|| {
openehr_rm::model::classes()
.flat_map(|c| c.attributes.iter().map(|a| a.name))
.collect()
});
let tail = aql_path.rsplit('/').next().unwrap_or(aql_path);
let attr = tail.split('[').next().unwrap_or(tail);
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" | "DV_STATE" => 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 date_full = ["2022", "02", "03"];
let date = |pat: &str| -> String {
let segs: Vec<&str> = if pat.is_empty() {
Vec::new()
} else {
pat.splitn(3, '-').collect()
};
let mut out = String::from(date_full[0]);
for (i, part) in date_full.iter().enumerate().skip(1) {
if segs.get(i).is_some_and(|s| *s == "XX") {
break;
}
out.push('-');
out.push_str(part);
}
out
};
let time_full = ["04", "05", "06"];
let time = |pat: &str| -> String {
let segs: Vec<&str> = if pat.is_empty() {
Vec::new()
} else {
pat.splitn(3, ':').collect()
};
let mut out = String::from(time_full[0]);
for (i, part) in time_full.iter().enumerate().skip(1) {
if segs.get(i).is_some_and(|s| *s == "XX") {
break;
}
out.push(':');
out.push_str(part);
}
out
};
let tz = if node.tz_validity == Some(1003) {
""
} else {
"Z"
};
match rm {
"DV_DATE" => date(pat_date),
"DV_TIME" => format!("{}{tz}", time(pat_time)),
_ => {
if pat_time.starts_with("XX") {
date(pat_date)
} else {
format!("{}T{}{tz}", date(pat_date), time(pat_time))
}
}
}
}
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, ""));
let mut total = 0.0;
let mut accumulate = |part: &str, in_time: bool| {
let mut num = String::new();
for ch in part.chars() {
if ch.is_ascii_digit() || ch == '.' || ch == ',' {
num.push(if ch == ',' { '.' } else { ch });
} else {
let n: f64 = num.parse().unwrap_or(0.0);
num.clear();
let secs = match (ch, 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,
};
total += n * secs;
}
}
};
accumulate(date_part, false);
accumulate(time_part, true);
total
}
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
}
fn deterministic_uuid(seed: &str) -> String {
let h1 = fnv1a64(seed.as_bytes(), 0xcbf2_9ce4_8422_2325);
let h2 = fnv1a64(seed.as_bytes(), 0x8422_2325_cbf2_9ce4);
let mut b = [0u8; 16];
b[..8].copy_from_slice(&h1.to_be_bytes());
b[8..].copy_from_slice(&h2.to_be_bytes());
b[6] = (b[6] & 0x0f) | 0x50; b[8] = (b[8] & 0x3f) | 0x80; format!(
"{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
b[0],
b[1],
b[2],
b[3],
b[4],
b[5],
b[6],
b[7],
b[8],
b[9],
b[10],
b[11],
b[12],
b[13],
b[14],
b[15],
)
}
fn fnv1a64(data: &[u8], basis: u64) -> u64 {
let mut hash = basis;
for &byte in data {
hash ^= u64::from(byte);
hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
}
hash
}
#[cfg(test)]
mod tests {
use super::*;
use crate::flat::webtemplate::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 = crate::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("output")),
Ok(ExampleType::Output)
);
assert!(ExampleType::from_query(Some("bogus")).is_err());
}
#[test]
fn is_deterministic() {
let wt = demo_vitals();
let a = example_composition(&wt, DetailLevel::Medium);
let b = example_composition(&wt, DetailLevel::Medium);
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);
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);
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)),
"medium populates strictly more leaves than required"
);
}
#[test]
fn complete_demonstrates_repetition() {
let wt = demo_vitals();
let medium = example_composition(&wt, DetailLevel::Medium);
let complete = example_composition(&wt, DetailLevel::Complete);
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));
let medium = leaf_count(&example_composition(&wt, DetailLevel::Medium));
let complete = leaf_count(&example_composition(&wt, DetailLevel::Complete));
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);
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]
#[expect(
clippy::float_cmp,
reason = "the example picker returns exactly representable values, so bit-equality is the intended 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);
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:?}"
);
}
}