use crate::am::{ArchetypeTerminology, CComplexObject, CObject, MultiplicityInterval, NodeIdSyntax};
use crate::base::ArchetypeId;
use crate::error::ParseError;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[allow(clippy::struct_field_names)]
pub struct Archetype {
archetype_id: ArchetypeId,
parent_archetype_id: Option<ArchetypeId>,
adl_version: Option<String>,
rm_release: Option<String>,
is_template: bool,
definition: CComplexObject,
terminology: ArchetypeTerminology,
}
impl Archetype {
pub fn new(
archetype_id: ArchetypeId,
definition: CComplexObject,
terminology: ArchetypeTerminology,
) -> Result<Self, ParseError> {
let archetype = Self {
archetype_id,
parent_archetype_id: None,
adl_version: None,
rm_release: None,
is_template: false,
definition,
terminology,
};
archetype.check()?;
Ok(archetype)
}
#[must_use]
pub fn specialising(mut self, parent: ArchetypeId) -> Self {
self.parent_archetype_id = Some(parent);
self
}
#[must_use]
pub fn with_versions(mut self, adl_version: Option<String>, rm_release: Option<String>) -> Self {
self.adl_version = adl_version;
self.rm_release = rm_release;
self
}
#[must_use]
pub const fn as_template(mut self) -> Self {
self.is_template = true;
self
}
#[must_use]
pub const fn archetype_id(&self) -> &ArchetypeId {
&self.archetype_id
}
#[must_use]
pub const fn parent_archetype_id(&self) -> Option<&ArchetypeId> {
self.parent_archetype_id.as_ref()
}
#[must_use]
pub fn rm_type_name(&self) -> &str {
self.definition.rm_type_name()
}
#[must_use]
pub const fn definition(&self) -> &CComplexObject {
&self.definition
}
#[must_use]
pub const fn terminology(&self) -> &ArchetypeTerminology {
&self.terminology
}
#[must_use]
pub const fn is_template(&self) -> bool {
self.is_template
}
#[must_use]
pub fn specialisation_depth(&self) -> usize {
self.archetype_id.specialisations().count()
}
#[must_use]
pub fn node_ids(&self) -> Vec<&str> {
let mut out = Vec::new();
if let Some(id) = self.definition.node_id() {
out.push(id);
}
collect_from_attributes(self.definition.attributes(), &mut out);
out
}
pub fn check(&self) -> Result<(), ParseError> {
if self.definition.rm_type_name() != self.archetype_id.rm_entity() {
return Err(ParseError::invariant("ARCHETYPE", "VARDT"));
}
let depth = self.specialisation_depth();
for code in self.node_ids() {
if !self.terminology.defines(code) {
return Err(ParseError::new("ARCHETYPE", "VATDF", code));
}
if NodeIdSyntax::specialisation_depth(code) > depth {
return Err(ParseError::new("ARCHETYPE", "VATCD", code));
}
}
for ac_code in terminology_constraints(self.definition.attributes()) {
if self.terminology.value_set(&ac_code).is_none() {
return Err(ParseError::new("ARCHETYPE", "VACDF", &ac_code));
}
}
Ok(())
}
}
fn collect_from_attributes<'a>(attributes: &'a [crate::am::CAttribute], out: &mut Vec<&'a str>) {
for attribute in attributes {
for child in attribute.children() {
if let Some(id) = child.node_id() {
out.push(id);
}
collect_from_attributes(child.attributes(), out);
}
}
}
fn terminology_constraints(attributes: &[crate::am::CAttribute]) -> Vec<String> {
let mut out = Vec::new();
for attribute in attributes {
for child in attribute.children() {
if let CObject::Primitive(primitive) = child
&& let crate::am::CPrimitive::TerminologyCode {
constraint: Some(ac_code),
..
} = primitive.constraint()
{
out.push(ac_code.clone());
}
out.extend(terminology_constraints(child.attributes()));
}
}
out
}
pub const ROOT_OCCURRENCES: MultiplicityInterval = MultiplicityInterval::MANDATORY;
#[cfg(test)]
mod tests {
use super::*;
use crate::am::{CAttribute, CPrimitive, CPrimitiveObject, TermDefinition};
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 observation(node_ids: &[&str], children: Vec<CObject>) -> CComplexObject {
let _ = node_ids;
CComplexObject::new(
"OBSERVATION",
Some("id1".to_owned()),
ROOT_OCCURRENCES,
vec![CAttribute::single("data", MultiplicityInterval::MANDATORY, children).unwrap()],
)
.unwrap()
}
fn element(node_id: &str) -> CObject {
CObject::Complex(
CComplexObject::new(
"ELEMENT",
Some(node_id.to_owned()),
MultiplicityInterval::MANDATORY,
Vec::new(),
)
.unwrap(),
)
}
#[test]
fn a_definition_constraining_the_wrong_rm_class_is_refused() {
let definition = CComplexObject::new(
"EVALUATION",
Some("id1".to_owned()),
ROOT_OCCURRENCES,
Vec::new(),
)
.unwrap();
let err = Archetype::new(
"openEHR-EHR-OBSERVATION.blood_pressure.v2".parse().unwrap(),
definition,
ArchetypeTerminology::new("en", terms(&["id1"])).unwrap(),
)
.unwrap_err();
assert_eq!(err.reason, "VARDT");
}
#[test]
fn a_node_the_terminology_does_not_define_is_refused() {
let err = Archetype::new(
"openEHR-EHR-OBSERVATION.blood_pressure.v2".parse().unwrap(),
observation(&[], vec![element("at0004")]),
ArchetypeTerminology::new("en", terms(&["id1"])).unwrap(),
)
.unwrap_err();
assert_eq!(err.reason, "VATDF");
assert_eq!(err.input, "at0004");
}
#[test]
fn a_code_specialised_deeper_than_its_archetype_is_refused() {
let err = Archetype::new(
"openEHR-EHR-OBSERVATION.blood_pressure.v2".parse().unwrap(),
observation(&[], vec![element("id2.1")]),
ArchetypeTerminology::new("en", terms(&["id1", "id2.1"])).unwrap(),
)
.unwrap_err();
assert_eq!(err.reason, "VATCD");
assert!(
Archetype::new(
"openEHR-EHR-OBSERVATION.blood_pressure-ambulatory.v2"
.parse()
.unwrap(),
observation(&[], vec![element("id2.1")]),
ArchetypeTerminology::new("en", terms(&["id1", "id2.1"])).unwrap(),
)
.is_ok()
);
}
#[test]
fn a_terminology_constraint_naming_no_value_set_is_refused() {
let coded = CObject::Primitive(CPrimitiveObject::new(
"DV_CODED_TEXT",
MultiplicityInterval::MANDATORY,
CPrimitive::TerminologyCode {
constraint: Some("ac0001".to_owned()),
code_list: Vec::new(),
},
));
let terminology = ArchetypeTerminology::new("en", terms(&["id1"])).unwrap();
let err = Archetype::new(
"openEHR-EHR-OBSERVATION.blood_pressure.v2".parse().unwrap(),
observation(&[], vec![coded.clone()]),
terminology,
)
.unwrap_err();
assert_eq!(err.reason, "VACDF");
let with_set = ArchetypeTerminology::new("en", terms(&["id1", "at0010"]))
.unwrap()
.with_value_set("ac0001", BTreeSet::from(["at0010".to_owned()]))
.unwrap();
assert!(
Archetype::new(
"openEHR-EHR-OBSERVATION.blood_pressure.v2".parse().unwrap(),
observation(&[], vec![coded]),
with_set,
)
.is_ok()
);
}
#[test]
fn deserialization_bypasses_the_constructor_and_check_catches_it() {
let archetype = Archetype::new(
"openEHR-EHR-OBSERVATION.blood_pressure.v2".parse().unwrap(),
observation(&[], vec![element("at0004")]),
ArchetypeTerminology::new("en", terms(&["id1", "at0004"])).unwrap(),
)
.unwrap();
let mut json: serde_json::Value = serde_json::to_value(&archetype).unwrap();
json["terminology"]["term_definitions"]["en"]
.as_object_mut()
.unwrap()
.remove("at0004");
let smuggled: Archetype = serde_json::from_value(json).unwrap();
assert_eq!(smuggled.check().unwrap_err().reason, "VATDF");
}
}