#![expect(
clippy::disallowed_types,
reason = "dev/verification tooling over JSON artifacts (the catalogue, results, wire \
exchanges), whose shapes belong to the artifacts and the SUT"
)]
use std::path::Path;
use serde_json::Value;
use crate::model::corpus::CorpusManifest;
use crate::perf::JourneyCatalogue;
use crate::perf_run::jitter::LeafConstraints;
const STAFF: [&str; 8] = [
"Nurse Amara Okafor",
"Dr. Ingrid Larsen",
"Nurse Tomas Novak",
"Dr. Priya Sharma",
"Nurse Lucia Romero",
"Dr. Sean Murphy",
"Nurse Mei Chen",
"Dr. Kwame Mensah",
];
#[derive(Debug, Clone)]
pub struct PackTemplate {
pub key: String,
pub template_id: String,
pub opt_xml: String,
pub skeleton: Value,
pub constraints: LeafConstraints,
}
#[derive(Debug, Clone)]
pub struct FlatPayload {
pub template_id: String,
pub opt_xml: String,
pub body: Value,
}
#[derive(Debug, Clone)]
pub struct TddPayload {
pub opt_xml: String,
pub document: String,
}
#[derive(Debug, Clone, Default)]
pub struct AuxPayloads {
pub flat: Option<FlatPayload>,
pub tdd: Option<TddPayload>,
pub person: Option<Value>,
pub person_amended: Option<Value>,
pub party_relationship: Option<Value>,
}
pub const FLAT_OPT_KEY: &str = "cnf.opt.minimal_action";
pub const FLAT_BODY_KEY: &str = "cnf.flat.vitals.minimal_ctx";
pub const PERSON_KEY: &str = "cnf.demographic.person.v1";
pub const PERSON_AMENDED_KEY: &str = "cnf.demographic.person.v2";
pub const PARTY_RELATIONSHIP_KEY: &str = "cnf.demographic.party_relationship.v1";
pub const TDD_OPT_KEY: &str = "cnf.opt.nested";
pub const TDD_BODY_KEY: &str = "cnf.messaging.tdd.nested";
#[derive(Debug, Clone)]
pub struct JourneyPack {
pub templates: Vec<PackTemplate>,
pub aux: AuxPayloads,
}
impl JourneyPack {
pub fn load(
corpus_dir: &Path,
manifest: &CorpusManifest,
catalogue: &JourneyCatalogue,
) -> Result<Self, String> {
let mut keys: Vec<String> = Vec::new();
for (_, journey) in &catalogue.0 {
for stage in &journey.stages {
if let Some(template) = &stage.template
&& !keys.contains(template)
{
keys.push(template.clone());
}
}
}
keys.sort();
let entry = |k: &str| {
crate::ids::CorpusKey::parse(k)
.ok()
.and_then(|parsed| manifest.get(&parsed).cloned())
.ok_or_else(|| format!("corpus manifest has no entry {k}"))
};
let read = |source: Option<&String>, what: &str| {
let source = source.ok_or_else(|| format!("{what} entry has no source"))?;
std::fs::read_to_string(corpus_dir.join(source))
.map_err(|e| format!("cannot read {source}: {e}"))
};
let read_json = |key: &str| -> Result<Value, String> {
let e = entry(key)?;
serde_json::from_str(&read(e.source.as_ref(), key)?)
.map_err(|error| format!("corpus fixture {key}: {error}"))
};
let mut templates = Vec::with_capacity(keys.len());
for key in keys {
let opt_entry = entry(&key)?;
let example_entry = entry(&format!("{key}.example"))?;
let template_id = opt_entry
.template_id
.clone()
.ok_or_else(|| format!("manifest entry {key} carries no template_id"))?;
let opt_xml = read(opt_entry.source.as_ref(), &key)?;
let skeleton: Value = serde_json::from_str(&read(example_entry.source.as_ref(), &key)?)
.map_err(|e| format!("example skeleton {key}: {e}"))?;
let constraints = LeafConstraints::from_opt(&opt_xml)
.map_err(|e| format!("operational template {key}: {e}"))?;
templates.push(PackTemplate {
key,
template_id,
opt_xml,
skeleton,
constraints,
});
}
if templates.is_empty() {
return Err("the journey catalogue names no templates".to_owned());
}
let mut needed: Vec<crate::perf::AuxPayloadKind> = Vec::new();
for (_, journey) in &catalogue.0 {
for stage in &journey.stages {
if let Some(kind) = crate::perf::PerfOp::parse(&stage.op)
.ok()
.and_then(crate::perf::PerfOp::aux_payload)
&& !needed.contains(&kind)
{
needed.push(kind);
}
}
}
let mut aux = AuxPayloads::default();
for kind in needed {
match kind {
crate::perf::AuxPayloadKind::Flat => {
let opt_entry = entry(FLAT_OPT_KEY)?;
aux.flat = Some(FlatPayload {
template_id: opt_entry.template_id.clone().ok_or_else(|| {
format!("manifest entry {FLAT_OPT_KEY} carries no template_id")
})?,
opt_xml: read(opt_entry.source.as_ref(), FLAT_OPT_KEY)?,
body: read_json(FLAT_BODY_KEY)?,
});
}
crate::perf::AuxPayloadKind::Person => {
aux.person = Some(read_json(PERSON_KEY)?);
aux.person_amended = Some(read_json(PERSON_AMENDED_KEY)?);
}
crate::perf::AuxPayloadKind::PartyRelationship => {
aux.party_relationship = Some(read_json(PARTY_RELATIONSHIP_KEY)?);
}
crate::perf::AuxPayloadKind::Tdd => {
let opt_entry = entry(TDD_OPT_KEY)?;
let body_entry = entry(TDD_BODY_KEY)?;
aux.tdd = Some(TddPayload {
opt_xml: read(opt_entry.source.as_ref(), TDD_OPT_KEY)?,
document: read(body_entry.source.as_ref(), TDD_BODY_KEY)?,
});
}
}
}
Ok(Self { templates, aux })
}
#[must_use]
pub fn index_of(&self, key: &str) -> Option<usize> {
self.templates.iter().position(|t| t.key == key)
}
#[must_use]
pub fn get(&self, index: usize) -> Option<&PackTemplate> {
self.templates.get(index)
}
}
#[expect(
clippy::integer_division,
reason = "whole hours/minutes/days of the simulated clock: exact integer split, \
which is what makes the rendered timestamp byte-identical across runs"
)]
pub(crate) fn sim_time(offset_s: u64) -> String {
let day_s = offset_s % 86_400;
let (h, m, s) = (day_s / 3600, (day_s % 3600) / 60, day_s % 60);
let day = 1 + (offset_s / 86_400) % 27;
format!("2024-06-{day:02}T{h:02}:{m:02}:{s:02}Z")
}
fn staff(arrival: u64) -> &'static str {
let index = usize::try_from(arrival % 8).unwrap_or(0);
STAFF.get(index).copied().unwrap_or(STAFF[0])
}
fn stamped(template: &PackTemplate, offset_s: u64, arrival: u64) -> Value {
let mut body = template.skeleton.clone();
template
.constraints
.apply(&mut body, &template.key, arrival);
let time = sim_time(offset_s);
if let Some(context) = body.get_mut("context") {
for field in ["start_time", "end_time"] {
if let Some(Value::String(value)) =
context.get_mut(field).and_then(|t| t.get_mut("value"))
{
value.clone_from(&time);
}
}
}
if let Some(Value::String(name)) = body.get_mut("composer").and_then(|c| c.get_mut("name")) {
staff(arrival).clone_into(name);
}
body
}
pub(crate) fn composition_body(
template: &PackTemplate,
offset_s: u64,
arrival: u64,
) -> Result<Vec<u8>, String> {
serde_json::to_vec(&stamped(template, offset_s, arrival)).map_err(|e| e.to_string())
}
pub(crate) fn contribution_body(
template: &PackTemplate,
offset_s: u64,
arrival: u64,
) -> Result<Vec<u8>, String> {
let audit = |change: &str, code: &str| {
serde_json::json!({
"_type": "AUDIT_DETAILS",
"system_id": "veredictum",
"committer": { "_type": "PARTY_IDENTIFIED", "name": staff(arrival) },
"change_type": { "_type": "DV_CODED_TEXT", "value": change,
"defining_code": { "_type": "CODE_PHRASE",
"terminology_id": { "_type": "TERMINOLOGY_ID", "value": "openehr" },
"code_string": code } }
})
};
let envelope = serde_json::json!({
"_type": "CONTRIBUTION",
"versions": [{
"_type": "ORIGINAL_VERSION",
"lifecycle_state": {
"_type": "DV_CODED_TEXT",
"value": "complete",
"defining_code": { "_type": "CODE_PHRASE",
"terminology_id": { "_type": "TERMINOLOGY_ID", "value": "openehr" },
"code_string": "532" }
},
"commit_audit": audit("creation", "249"),
"data": stamped(template, offset_s, arrival)
}],
"audit": audit("creation", "249")
});
serde_json::to_vec(&envelope).map_err(|e| e.to_string())
}
pub(crate) fn ehr_status_body(offset_s: u64) -> Vec<u8> {
let body = serde_json::json!({
"_type": "EHR_STATUS",
"name": { "_type": "DV_TEXT", "value": "EHR Status" },
"archetype_node_id": "openEHR-EHR-EHR_STATUS.generic.v1",
"archetype_details": {
"_type": "ARCHETYPED",
"archetype_id": { "_type": "ARCHETYPE_ID",
"value": "openEHR-EHR-EHR_STATUS.generic.v1" },
"rm_version": "1.1.0"
},
"subject": { "_type": "PARTY_SELF" },
"is_queryable": true,
"is_modifiable": true,
"other_details": {
"_type": "ITEM_TREE",
"name": { "_type": "DV_TEXT", "value": "status" },
"archetype_node_id": "at0001",
"items": [{
"_type": "ELEMENT",
"name": { "_type": "DV_TEXT", "value": "last ADT touch" },
"archetype_node_id": "at0002",
"value": { "_type": "DV_DATE_TIME", "value": sim_time(offset_s) }
}]
}
});
serde_json::to_vec(&body).unwrap_or_default()
}
pub(crate) fn folder_body(closed: bool) -> Vec<u8> {
let mut folders = vec![serde_json::json!({
"_type": "FOLDER",
"archetype_node_id": "openEHR-EHR-FOLDER.generic.v1",
"name": { "_type": "DV_TEXT", "value": "episodes" }
})];
if closed {
folders.push(serde_json::json!({
"_type": "FOLDER",
"archetype_node_id": "openEHR-EHR-FOLDER.generic.v1",
"name": { "_type": "DV_TEXT", "value": "closed" }
}));
}
let body = serde_json::json!({
"_type": "FOLDER",
"name": { "_type": "DV_TEXT", "value": "root" },
"archetype_node_id": "openEHR-EHR-FOLDER.generic.v1",
"folders": folders
});
serde_json::to_vec(&body).unwrap_or_default()
}
pub(crate) fn person_body(person: &Value, arrival: u64) -> Result<Vec<u8>, String> {
let mut body = person.clone();
if let Some(Value::String(name)) = body
.get_mut("identities")
.and_then(|i| i.get_mut(0))
.and_then(|identity| identity.get_mut("details"))
.and_then(|details| details.get_mut("items"))
.and_then(|items| items.get_mut(0))
.and_then(|item| item.get_mut("value"))
.and_then(|value| value.get_mut("value"))
{
*name = format!("{} (registration {arrival})", staff(arrival));
}
serde_json::to_vec(&body).map_err(|e| e.to_string())
}
pub(crate) fn party_relationship_body(
relationship: &Value,
source_uid: &str,
) -> Result<Vec<u8>, String> {
let mut body = relationship.clone();
if let Some(Value::String(id)) = body
.get_mut("source")
.and_then(|source| source.get_mut("id"))
.and_then(|id| id.get_mut("value"))
{
source_uid.clone_into(id);
}
serde_json::to_vec(&body).map_err(|e| e.to_string())
}
pub(crate) fn flat_body(payload: &FlatPayload) -> Result<Vec<u8>, String> {
serde_json::to_vec(&payload.body).map_err(|e| e.to_string())
}
pub(crate) fn tags_body(offset_s: u64) -> Vec<u8> {
let body = serde_json::json!([
{ "key": "cnf.workflow", "value": "ward-round" },
{ "key": "cnf.touched", "value": sim_time(offset_s) }
]);
serde_json::to_vec(&body).unwrap_or_default()
}
#[cfg(test)]
mod tests {
use super::*;
const CELSIUS_OPT: &str = "<template xmlns=\"http://schemas.openehr.org/v1\" \
xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">\
<children xsi:type=\"C_DV_QUANTITY\"><rm_type_name>DV_QUANTITY</rm_type_name>\
<list><magnitude><lower_included>true</lower_included>\
<upper_included>false</upper_included><lower_unbounded>false</lower_unbounded>\
<upper_unbounded>false</upper_unbounded><lower>0</lower><upper>100</upper>\
</magnitude><units>Cel</units></list></children></template>";
fn template() -> PackTemplate {
PackTemplate {
key: "cnf.ckm.vital_signs".to_owned(),
template_id: "Vital signs".to_owned(),
opt_xml: "<template/>".to_owned(),
skeleton: serde_json::json!({
"_type": "COMPOSITION",
"context": {
"_type": "EVENT_CONTEXT",
"start_time": { "_type": "DV_DATE_TIME", "value": "2020-01-01T00:00:00Z" }
},
"composer": { "_type": "PARTY_IDENTIFIED", "name": "original" }
}),
constraints: LeafConstraints::default(),
}
}
fn measuring_template() -> PackTemplate {
let mut template = template();
template.opt_xml = CELSIUS_OPT.to_owned();
template.constraints = LeafConstraints::from_opt(CELSIUS_OPT).unwrap();
template.skeleton = serde_json::json!({
"_type": "COMPOSITION",
"context": {
"_type": "EVENT_CONTEXT",
"start_time": { "_type": "DV_DATE_TIME", "value": "2020-01-01T00:00:00Z" }
},
"composer": { "_type": "PARTY_IDENTIFIED", "name": "original" },
"content": [{
"_type": "ELEMENT",
"name": { "_type": "DV_TEXT", "value": "Temperature" },
"value": { "_type": "DV_QUANTITY", "magnitude": 49.5, "units": "Cel" }
}]
});
template
}
#[test]
fn stamping_is_deterministic_and_touches_only_time_and_composer() {
let t = template();
let a = composition_body(&t, 3661, 5).unwrap();
let b = composition_body(&t, 3661, 5).unwrap();
assert_eq!(a, b);
let value: Value = serde_json::from_slice(&a).unwrap();
assert_eq!(
value["context"]["start_time"]["value"],
"2024-06-01T01:01:01Z"
);
assert_eq!(value["composer"]["name"], STAFF[5]);
let c = composition_body(&t, 7200, 5).unwrap();
assert_ne!(a, c);
}
fn population(template: &PackTemplate, count: u64) -> Vec<Vec<u8>> {
(0..count)
.map(|arrival| composition_body(template, arrival * 60, arrival).unwrap())
.collect()
}
#[test]
fn the_same_seed_renders_the_same_population_byte_for_byte() {
let t = measuring_template();
assert_eq!(population(&t, 64), population(&t, 64));
}
#[test]
fn a_population_no_longer_carries_one_leaf_value_in_every_composition() {
let t = measuring_template();
let magnitudes: std::collections::BTreeSet<String> = population(&t, 64)
.iter()
.map(|body| {
let value: Value = serde_json::from_slice(body).unwrap();
value["content"][0]["value"]["magnitude"].to_string()
})
.collect();
assert!(
magnitudes.len() > 32,
"64 compositions carried only {} distinct temperatures",
magnitudes.len()
);
}
#[test]
fn every_redrawn_leaf_stays_inside_the_range_the_template_declares() {
let t = measuring_template();
for body in population(&t, 512) {
let value: Value = serde_json::from_slice(&body).unwrap();
let magnitude = value["content"][0]["value"]["magnitude"].as_f64().unwrap();
assert!(
(0.0..100.0).contains(&magnitude),
"redrawn magnitude {magnitude} is outside the declared [0, 100)"
);
assert_eq!(value["content"][0]["value"]["units"], "Cel");
}
}
#[test]
fn the_jitter_touches_the_numeric_leaf_and_nothing_else_structural() {
let t = measuring_template();
let stamped_body = composition_body(&t, 0, 3).unwrap();
let value: Value = serde_json::from_slice(&stamped_body).unwrap();
assert_eq!(value["_type"], "COMPOSITION");
assert_eq!(value["content"][0]["_type"], "ELEMENT");
assert_eq!(value["content"][0]["name"]["value"], "Temperature");
assert_eq!(value["content"][0]["value"]["_type"], "DV_QUANTITY");
let keys: Vec<&String> = value["content"][0]["value"]
.as_object()
.unwrap()
.keys()
.collect();
assert_eq!(keys, vec!["_type", "magnitude", "units"]);
}
#[test]
fn the_contribution_envelope_wraps_one_original_version() {
let t = template();
let bytes = contribution_body(&t, 60, 1).unwrap();
let value: Value = serde_json::from_slice(&bytes).unwrap();
assert_eq!(value["_type"], "CONTRIBUTION");
let versions = value["versions"].as_array().unwrap();
assert_eq!(versions.len(), 1);
assert_eq!(versions[0]["_type"], "ORIGINAL_VERSION");
assert_eq!(
versions[0]["lifecycle_state"]["defining_code"]["code_string"],
"532"
);
assert_eq!(versions[0]["data"]["_type"], "COMPOSITION");
}
fn ckm_dir() -> std::path::PathBuf {
Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/../.."))
.join("artifacts/corpus/templates/ckm")
}
fn vendored(stem: &str, key: &str, template_id: &str) -> PackTemplate {
let dir = ckm_dir();
let opt_xml = std::fs::read_to_string(dir.join(format!("{stem}.opt"))).unwrap();
let skeleton: Value = serde_json::from_str(
&std::fs::read_to_string(dir.join(format!("{stem}.example.json"))).unwrap(),
)
.unwrap();
let constraints = LeafConstraints::from_opt(&opt_xml).unwrap();
PackTemplate {
key: key.to_owned(),
template_id: template_id.to_owned(),
opt_xml,
skeleton,
constraints,
}
}
fn flatten(value: &Value, at: &str, into: &mut std::collections::BTreeMap<String, String>) {
match value {
Value::Object(map) => {
for (key, child) in map {
flatten(child, &format!("{at}/{key}"), into);
}
}
Value::Array(items) => {
for (index, item) in items.iter().enumerate() {
flatten(item, &format!("{at}/{index}"), into);
}
}
other => {
into.insert(at.to_owned(), other.to_string());
}
}
}
fn quantities(value: &Value, into: &mut Vec<(String, f64)>) {
match value {
Value::Object(map) => {
if map.get("_type").and_then(Value::as_str) == Some("DV_QUANTITY")
&& let (Some(units), Some(magnitude)) = (
map.get("units").and_then(Value::as_str),
map.get("magnitude").and_then(Value::as_f64),
)
{
into.push((units.to_owned(), magnitude));
}
for (_, child) in map {
quantities(child, into);
}
}
Value::Array(items) => {
for item in items {
quantities(item, into);
}
}
_ => {}
}
}
const VITAL_SIGNS_RANGES: [(&str, f64, f64, bool); 7] = [
("Cel", 0.0, 100.0, false),
("mm[Hg]", 0.0, 1000.0, false),
("kg/m2", 0.0, 1000.0, false),
("cm", 0.0, 1000.0, true),
("kg", 0.0, 1000.0, true),
("g", 0.0, 1_000_000.0, true),
("/min", 0.0, 200.0, true),
];
#[test]
fn the_vendored_vital_signs_population_reproduces_byte_for_byte() {
let t = vendored("vital-signs", "cnf.ckm.vital_signs", "Vital signs");
assert_eq!(population(&t, 48), population(&t, 48));
}
#[test]
fn two_compositions_of_one_vendored_population_differ_in_their_leaves() {
let t = vendored("vital-signs", "cnf.ckm.vital_signs", "Vital signs");
let of = |arrival: u64| {
let body = composition_body(&t, arrival * 60, arrival).unwrap();
let value: Value = serde_json::from_slice(&body).unwrap();
let mut found = Vec::new();
quantities(&value, &mut found);
found
};
let (first, second) = (of(0), of(1));
assert_eq!(
first.len(),
8,
"the committed skeleton carries 8 quantities"
);
assert_ne!(
first, second,
"two arrivals of one population carried identical quantity leaves"
);
}
#[test]
fn every_redrawn_vital_sign_is_inside_the_range_the_vendored_opt_declares() {
let t = vendored("vital-signs", "cnf.ckm.vital_signs", "Vital signs");
for arrival in 0..256_u64 {
let body = composition_body(&t, arrival * 60, arrival).unwrap();
let value: Value = serde_json::from_slice(&body).unwrap();
let mut found = Vec::new();
quantities(&value, &mut found);
for (units, magnitude) in found {
let declared = VITAL_SIGNS_RANGES
.iter()
.find(|(declared_units, _, _, _)| *declared_units == units);
let Some(&(_, lower, upper, upper_included)) = declared else {
panic!("arrival {arrival} carried an undeclared unit {units}");
};
let inside = magnitude >= lower
&& (if upper_included {
magnitude <= upper
} else {
magnitude < upper
});
assert!(
inside,
"arrival {arrival}: {magnitude} {units} is outside the declared range"
);
}
}
}
#[test]
fn the_jitter_moves_magnitudes_and_leaves_every_other_leaf_alone() {
let t = vendored("vital-signs", "cnf.ckm.vital_signs", "Vital signs");
let mut before = std::collections::BTreeMap::new();
flatten(&t.skeleton, "", &mut before);
let body = composition_body(&t, 600, 7).unwrap();
let stamped_value: Value = serde_json::from_slice(&body).unwrap();
let mut after = std::collections::BTreeMap::new();
flatten(&stamped_value, "", &mut after);
assert_eq!(
before.keys().collect::<Vec<_>>(),
after.keys().collect::<Vec<_>>(),
"stamping added or removed a leaf"
);
let moved: Vec<&String> = before
.iter()
.filter(|(pointer, value)| after.get(*pointer) != Some(*value))
.map(|(pointer, _)| pointer)
.collect();
assert!(!moved.is_empty(), "stamping changed nothing at all");
for pointer in moved {
let expected = pointer.ends_with("/magnitude")
|| pointer == "/context/start_time/value"
|| pointer == "/context/end_time/value"
|| pointer == "/composer/name";
assert!(
expected,
"stamping moved {pointer}, which it must not touch"
);
}
}
#[test]
fn a_template_that_declares_no_readable_range_keeps_its_committed_leaf() {
let t = vendored(
"generic-lab-test-result",
"cnf.ckm.lab_result",
"Generic lab test result example simple",
);
for arrival in 0..16_u64 {
let body = composition_body(&t, arrival * 60, arrival).unwrap();
let value: Value = serde_json::from_slice(&body).unwrap();
let mut found = Vec::new();
quantities(&value, &mut found);
assert_eq!(found, vec![("mg/L".to_owned(), 10.0)]);
}
}
#[test]
fn every_vendored_pack_template_reads_its_constraints() {
let mut read = 0_usize;
for entry in std::fs::read_dir(ckm_dir()).unwrap() {
let path = entry.unwrap().path();
if path.extension().is_none_or(|e| e != "opt") {
continue;
}
let opt_xml = std::fs::read_to_string(&path).unwrap();
assert!(
LeafConstraints::from_opt(&opt_xml).is_ok(),
"{} is not readable as an operational template",
path.display()
);
read += 1;
}
assert!(read >= 16, "the pack shrank to {read} templates");
}
#[test]
fn constructed_bodies_parse_and_carry_their_shape() {
let status: Value = serde_json::from_slice(&ehr_status_body(0)).unwrap();
assert_eq!(status["_type"], "EHR_STATUS");
assert_eq!(status["subject"]["_type"], "PARTY_SELF");
let folder: Value = serde_json::from_slice(&folder_body(true)).unwrap();
assert_eq!(folder["folders"].as_array().unwrap().len(), 2);
assert!(folder["archetype_node_id"].is_string());
for sub in folder["folders"].as_array().unwrap() {
assert!(
sub["archetype_node_id"].is_string(),
"subfolder without archetype_node_id"
);
}
let tags: Value = serde_json::from_slice(&tags_body(0)).unwrap();
assert_eq!(tags.as_array().unwrap().len(), 2);
assert!(sim_time(90_061).starts_with("2024-06-02T01:01:01"));
}
}