use openehr::am::{
AM_RELEASE, Archetype, ArchetypeSlot, ArchetypeTerminology, CArchetypeRoot, CAttribute,
CComplexObject, CObject, CPrimitive, CPrimitiveObject, Cardinality, MultiplicityInterval,
TermDefinition,
};
use openehr::base::Interval;
use std::collections::{BTreeMap, BTreeSet};
fn terms(codes: &[&str]) -> BTreeMap<String, TermDefinition> {
codes
.iter()
.map(|code| {
(
(*code).to_owned(),
TermDefinition::new(format!("term {code}"), None).unwrap(),
)
})
.collect()
}
fn blood_pressure() -> Archetype {
let systolic = CObject::Complex(
CComplexObject::new(
"ELEMENT",
Some("at0004".to_owned()),
MultiplicityInterval::MANDATORY,
vec![
CAttribute::single(
"value",
MultiplicityInterval::MANDATORY,
vec![CObject::Primitive(CPrimitiveObject::new(
"DV_QUANTITY",
MultiplicityInterval::MANDATORY,
CPrimitive::Real {
list: Vec::new(),
range: Some(Interval::closed("0".parse().unwrap(), "1000.0".parse().unwrap()).unwrap()),
},
))],
)
.unwrap(),
],
)
.unwrap(),
);
let position = CObject::Primitive(CPrimitiveObject::new(
"DV_CODED_TEXT",
MultiplicityInterval::OPTIONAL,
CPrimitive::TerminologyCode {
constraint: Some("ac0001".to_owned()),
code_list: Vec::new(),
},
));
let slot = CObject::Slot(
ArchetypeSlot::new("CLUSTER", "at0007", MultiplicityInterval::new(0, None).unwrap())
.unwrap()
.including("archetype_id/value matches {/openEHR-EHR-CLUSTER\\..*/}"),
);
let items = CAttribute::container(
"items",
MultiplicityInterval::MANDATORY,
Cardinality::new(MultiplicityInterval::at_least(1).unwrap()).ordered(),
vec![systolic, position, slot],
)
.unwrap();
let definition = CComplexObject::new(
"OBSERVATION",
Some("id1".to_owned()),
MultiplicityInterval::MANDATORY,
vec![CAttribute::single("data", MultiplicityInterval::MANDATORY, vec![CObject::Complex(
CComplexObject::new(
"ITEM_TREE",
Some("at0003".to_owned()),
MultiplicityInterval::MANDATORY,
vec![items],
)
.unwrap(),
)])
.unwrap()],
)
.unwrap();
let terminology = ArchetypeTerminology::new(
"en",
terms(&["id1", "at0003", "at0004", "at0007", "at0010"]),
)
.unwrap()
.with_value_set("ac0001", BTreeSet::from(["at0010".to_owned()]))
.unwrap()
.with_binding("SNOMED-CT", "at0004", "271649006");
Archetype::new(
"openEHR-EHR-OBSERVATION.blood_pressure.v2".parse().unwrap(),
definition,
terminology,
)
.unwrap()
}
#[test]
fn an_archetype_is_constructible_without_a_parser() {
let archetype = blood_pressure();
assert_eq!(archetype.rm_type_name(), "OBSERVATION");
assert_eq!(
archetype.node_ids(),
["id1", "at0003", "at0004", "at0007"],
"document order, root first"
);
assert!(archetype.terminology().defines("at0004"));
}
#[test]
fn an_archetype_round_trips_through_json_unchanged() {
let archetype = blood_pressure();
let json = serde_json::to_string(&archetype).unwrap();
let back: Archetype = serde_json::from_str(&json).unwrap();
assert_eq!(back, archetype);
assert_eq!(serde_json::to_string(&back).unwrap(), json);
back.check().unwrap();
}
#[test]
fn a_constraint_this_crate_cannot_model_survives_rather_than_disappearing() {
let exotic = CPrimitive::Unsupported {
rm_type_name: "C_DURATION".to_owned(),
source: "P0Y..P100Y".to_owned(),
};
let node = CObject::Primitive(CPrimitiveObject::new(
"DV_DURATION",
MultiplicityInterval::MANDATORY,
exotic.clone(),
));
let json = serde_json::to_string(&node).unwrap();
assert!(json.contains("C_UNSUPPORTED"), "the kind is named, not erased");
let CObject::Primitive(back) = serde_json::from_str::<CObject>(&json).unwrap() else {
panic!("a primitive node deserialized as something else");
};
assert_eq!(back.constraint(), &exotic);
}
#[test]
fn the_targeted_archetype_model_release_is_named() {
assert_eq!(AM_RELEASE, "2.3.0");
let declared = blood_pressure().with_versions(Some("2.4.0".to_owned()), Some("1.0.4".to_owned()));
let json = serde_json::to_string(&declared).unwrap();
assert!(json.contains("2.4.0") && json.contains("1.0.4"));
serde_json::from_str::<Archetype>(&json).unwrap().check().unwrap();
}
#[test]
fn an_artefact_that_arrived_as_json_is_checkable_by_the_same_rules() {
let mut json: serde_json::Value = serde_json::to_value(blood_pressure()).unwrap();
json["definition"]["rm_type_name"] = serde_json::Value::String("EVALUATION".to_owned());
let smuggled: Archetype = serde_json::from_value(json).unwrap();
assert_eq!(smuggled.rm_type_name(), "EVALUATION");
assert_eq!(smuggled.check().unwrap_err().reason, "VARDT");
}
#[test]
fn every_accessor_returns_what_was_constructed() {
let bounded = MultiplicityInterval::new(0, Some(4)).unwrap();
assert_eq!(bounded.lower(), 0);
assert_eq!(bounded.upper(), Some(4));
assert!(!bounded.is_open());
assert!(!bounded.is_mandatory());
let open = MultiplicityInterval::at_least(2).unwrap();
assert_eq!(open.lower(), 2);
assert_eq!(open.upper(), None);
assert!(open.is_open());
assert!(open.is_mandatory());
assert!(MultiplicityInterval::MANDATORY.is_mandatory());
assert!(!MultiplicityInterval::OPTIONAL.is_mandatory());
let plain = Cardinality::new(bounded.clone());
assert_eq!(plain.interval(), &bounded);
assert!(!plain.is_ordered());
assert!(!plain.is_unique());
assert!(Cardinality::new(bounded.clone()).ordered().is_ordered());
assert!(Cardinality::new(bounded.clone()).unique().is_unique());
let with_description =
TermDefinition::new("Systolic", Some("Peak pressure".to_owned())).unwrap();
assert_eq!(with_description.text(), "Systolic");
assert_eq!(with_description.description(), Some("Peak pressure"));
assert_eq!(TermDefinition::new("Diastolic", None).unwrap().description(), None);
let terminology = ArchetypeTerminology::new("en", terms(&["id1", "at0004"])).unwrap();
assert_eq!(terminology.original_language(), "en");
assert_eq!(terminology.definition("at0004").unwrap().text(), "term at0004");
assert_eq!(terminology.definition("at9999"), None);
let mut codes: Vec<&str> = terminology.codes().collect();
codes.sort_unstable();
assert_eq!(codes, ["at0004", "id1"]);
let leaf = CObject::Primitive(CPrimitiveObject::new(
"DV_TEXT",
MultiplicityInterval::MANDATORY,
CPrimitive::String { list: Vec::new(), pattern: None },
));
let single = CAttribute::single("value", MultiplicityInterval::MANDATORY, vec![leaf.clone()])
.unwrap();
assert_eq!(single.rm_attribute_name(), "value");
assert_eq!(single.cardinality(), None);
assert_eq!(single.existence(), &MultiplicityInterval::MANDATORY);
let container = CAttribute::container(
"items",
MultiplicityInterval::MANDATORY,
Cardinality::new(MultiplicityInterval::at_least(1).unwrap()).ordered(),
vec![leaf.clone()],
)
.unwrap();
assert_eq!(container.rm_attribute_name(), "items");
assert!(container.cardinality().unwrap().is_ordered());
assert_eq!(container.children().len(), 1);
assert_eq!(leaf.rm_type_name(), "DV_TEXT");
let CObject::Primitive(primitive) = &leaf else {
panic!("built a primitive and got something else");
};
assert_eq!(primitive.rm_type_name(), "DV_TEXT");
let slot = ArchetypeSlot::new("CLUSTER", "at0007", MultiplicityInterval::OPTIONAL)
.unwrap()
.including("archetype_id/value matches {/.*/}")
.excluding("archetype_id/value matches {/nothing/}");
assert_eq!(slot.node_id(), "at0007");
assert_eq!(slot.includes(), ["archetype_id/value matches {/.*/}"]);
assert_eq!(slot.excludes(), ["archetype_id/value matches {/nothing/}"]);
let slot_object = CObject::Slot(slot);
assert_eq!(slot_object.rm_type_name(), "CLUSTER");
assert_eq!(slot_object.node_id(), Some("at0007"));
let root = CArchetypeRoot::new(
"CLUSTER",
"openEHR-EHR-CLUSTER.device.v1",
MultiplicityInterval::MANDATORY,
)
.unwrap();
assert_eq!(root.archetype_ref(), "openEHR-EHR-CLUSTER.device.v1");
assert_eq!(CObject::ArchetypeRoot(root).rm_type_name(), "CLUSTER");
let plain_archetype = blood_pressure();
assert_eq!(plain_archetype.parent_archetype_id(), None);
assert!(!plain_archetype.is_template());
let parent = "openEHR-EHR-OBSERVATION.blood_pressure.v2".parse().unwrap();
let specialised = blood_pressure().specialising(parent);
assert_eq!(
specialised.parent_archetype_id().map(ToString::to_string),
Some("openEHR-EHR-OBSERVATION.blood_pressure.v2".to_owned())
);
assert!(blood_pressure().as_template().is_template());
}
#[test]
fn the_node_id_syntaxes_are_distinguished_at_their_boundaries() {
use openehr::am::NodeIdSyntax;
assert_eq!(NodeIdSyntax::of("at0"), Some(NodeIdSyntax::Adl2));
assert_eq!(NodeIdSyntax::of("id1"), Some(NodeIdSyntax::Adl2));
assert_eq!(NodeIdSyntax::of("at10"), Some(NodeIdSyntax::Adl2));
assert_eq!(NodeIdSyntax::of("at0004"), Some(NodeIdSyntax::Adl14));
assert_eq!(NodeIdSyntax::of("ac0001"), Some(NodeIdSyntax::Adl14));
assert_eq!(NodeIdSyntax::of("id1."), None);
assert_eq!(NodeIdSyntax::of("id1..2"), None);
assert_eq!(NodeIdSyntax::of("id1.x"), None);
assert_eq!(NodeIdSyntax::of("id"), None);
}
#[test]
fn children_may_fill_a_container_exactly() {
let element = |code: &str| {
CObject::Complex(
CComplexObject::new(
"ELEMENT",
Some(code.to_owned()),
MultiplicityInterval::MANDATORY,
Vec::new(),
)
.unwrap(),
)
};
assert!(
CAttribute::container(
"items",
MultiplicityInterval::MANDATORY,
Cardinality::new(MultiplicityInterval::new(0, Some(2)).unwrap()),
vec![element("at0004"), element("at0005")],
)
.is_ok()
);
assert!(
CAttribute::container(
"items",
MultiplicityInterval::MANDATORY,
Cardinality::new(MultiplicityInterval::new(0, Some(2)).unwrap()),
vec![element("at0004"), element("at0005"), element("at0006")],
)
.is_err()
);
}