#![expect(
clippy::disallowed_types,
reason = "dev/verification tooling over JSON artifacts (the catalogue, results, wire \
exchanges) — not the application (#1694)"
)]
use std::path::Path;
use serde_json::Value;
use crate::model::corpus::CorpusManifest;
use crate::perf::JourneyCatalogue;
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,
}
#[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}"))?;
templates.push(PackTemplate {
key,
template_id,
opt_xml,
skeleton,
});
}
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();
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::*;
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" }
}),
}
}
#[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);
}
#[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");
}
#[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"));
}
}