use crate::base::{
ArchetypeId, HierObjectId, Interval, LocatableRef, ObjectRef, ObjectVersionId, PartyRef,
TemplateId, UidBasedId,
};
use crate::error::ParseError;
use crate::rm::data_types::{
DataValue, DvCodedText, DvDate, DvDateTime, DvEhrUri, DvIdentifier, DvMultimedia, Text,
};
use crate::terminology;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Archetyped {
archetype_id: ArchetypeId,
#[serde(skip_serializing_if = "Option::is_none", default)]
template_id: Option<TemplateId>,
rm_version: String,
}
impl Archetyped {
pub fn new(archetype_id: &str, rm_version: impl Into<String>) -> Result<Self, ParseError> {
let rm_version = rm_version.into();
if rm_version.is_empty() {
return Err(ParseError::invariant("ARCHETYPED", "Rm_version_valid"));
}
Ok(Self {
archetype_id: archetype_id.parse()?,
template_id: None,
rm_version,
})
}
pub fn with_template(mut self, template_id: &str) -> Result<Self, ParseError> {
self.template_id = Some(template_id.parse()?);
Ok(self)
}
#[must_use]
pub fn archetype_id(&self) -> &ArchetypeId {
&self.archetype_id
}
#[must_use]
pub fn template_id(&self) -> Option<&TemplateId> {
self.template_id.as_ref()
}
#[must_use]
pub fn rm_version(&self) -> &str {
&self.rm_version
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[allow(clippy::struct_field_names)]
pub struct Link {
meaning: Text,
#[serde(rename = "type")]
link_type: Text,
target: DvEhrUri,
}
impl Link {
#[must_use]
pub fn new(meaning: Text, link_type: Text, target: DvEhrUri) -> Self {
Self {
meaning,
link_type,
target,
}
}
#[must_use]
pub fn meaning(&self) -> &Text {
&self.meaning
}
#[must_use]
pub fn link_type(&self) -> &Text {
&self.link_type
}
#[must_use]
pub fn target(&self) -> &DvEhrUri {
&self.target
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct FeederAuditDetails {
system_id: String,
#[serde(skip_serializing_if = "Option::is_none", default)]
location: Option<PartyIdentified>,
#[serde(skip_serializing_if = "Option::is_none", default)]
subject: Option<PartyProxy>,
#[serde(skip_serializing_if = "Option::is_none", default)]
provider: Option<PartyIdentified>,
#[serde(skip_serializing_if = "Option::is_none", default)]
time: Option<DvDateTime>,
#[serde(skip_serializing_if = "Option::is_none", default)]
version_id: Option<String>,
}
impl FeederAuditDetails {
pub fn new(system_id: impl Into<String>) -> Result<Self, ParseError> {
let system_id = system_id.into();
if system_id.is_empty() {
return Err(ParseError::invariant(
"FEEDER_AUDIT_DETAILS",
"System_id_valid",
));
}
Ok(Self {
system_id,
location: None,
subject: None,
provider: None,
time: None,
version_id: None,
})
}
#[must_use]
pub fn with_time(mut self, time: DvDateTime) -> Self {
self.time = Some(time);
self
}
#[must_use]
pub fn system_id(&self) -> &str {
&self.system_id
}
#[must_use]
pub fn time(&self) -> Option<&DvDateTime> {
self.time.as_ref()
}
#[must_use]
pub fn version_id(&self) -> Option<&str> {
self.version_id.as_deref()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct FeederAudit {
originating_system_audit: FeederAuditDetails,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
originating_system_item_ids: Vec<DvIdentifier>,
#[serde(skip_serializing_if = "Option::is_none", default)]
feeder_system_audit: Option<FeederAuditDetails>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
feeder_system_item_ids: Vec<DvIdentifier>,
#[serde(skip_serializing_if = "Option::is_none", default)]
original_content: Option<DataValue>,
}
impl FeederAudit {
#[must_use]
pub fn new(originating_system_audit: FeederAuditDetails) -> Self {
Self {
originating_system_audit,
originating_system_item_ids: Vec::new(),
feeder_system_audit: None,
feeder_system_item_ids: Vec::new(),
original_content: None,
}
}
pub fn with_original_content(mut self, content: DataValue) -> Result<Self, ParseError> {
if !matches!(content, DataValue::Parsable(_) | DataValue::Multimedia(_)) {
return Err(ParseError::invariant(
"FEEDER_AUDIT",
"Original_content_encapsulated",
));
}
self.original_content = Some(content);
Ok(self)
}
#[must_use]
pub fn with_originating_item_id(mut self, id: DvIdentifier) -> Self {
self.originating_system_item_ids.push(id);
self
}
#[must_use]
pub fn originating_system_audit(&self) -> &FeederAuditDetails {
&self.originating_system_audit
}
#[must_use]
pub fn originating_system_item_ids(&self) -> &[DvIdentifier] {
&self.originating_system_item_ids
}
#[must_use]
pub fn original_content(&self) -> Option<&DataValue> {
self.original_content.as_ref()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct LocatableAttrs {
name: Text,
archetype_node_id: String,
#[serde(skip_serializing_if = "Option::is_none", default)]
uid: Option<UidBasedId>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
links: Vec<Link>,
#[serde(skip_serializing_if = "Option::is_none", default)]
archetype_details: Option<Box<Archetyped>>,
#[serde(skip_serializing_if = "Option::is_none", default)]
feeder_audit: Option<Box<FeederAudit>>,
}
impl LocatableAttrs {
pub fn new(name: Text, archetype_node_id: impl Into<String>) -> Result<Self, ParseError> {
let archetype_node_id = archetype_node_id.into();
if archetype_node_id.is_empty() {
return Err(ParseError::invariant(
"LOCATABLE",
"Archetype_node_id_valid",
));
}
Ok(Self {
name,
archetype_node_id,
uid: None,
links: Vec::new(),
archetype_details: None,
feeder_audit: None,
})
}
pub fn named(name: &str, archetype_node_id: &str) -> Result<Self, ParseError> {
Self::new(Text::plain(name)?, archetype_node_id)
}
#[must_use]
pub fn with_archetype_details(mut self, details: Archetyped) -> Self {
self.archetype_details = Some(Box::new(details));
self
}
#[must_use]
pub fn with_uid(mut self, uid: UidBasedId) -> Self {
self.uid = Some(uid);
self
}
#[must_use]
pub fn with_link(mut self, link: Link) -> Self {
self.links.push(link);
self
}
#[must_use]
pub fn with_feeder_audit(mut self, audit: FeederAudit) -> Self {
self.feeder_audit = Some(Box::new(audit));
self
}
#[must_use]
pub fn has_uid(&self) -> bool {
self.uid.is_some()
}
#[must_use]
pub fn name(&self) -> &Text {
&self.name
}
#[must_use]
pub fn archetype_node_id(&self) -> &str {
&self.archetype_node_id
}
#[must_use]
pub fn uid(&self) -> Option<&UidBasedId> {
self.uid.as_ref()
}
}
pub trait Locatable {
fn locatable(&self) -> &LocatableAttrs;
fn rm_type_name(&self) -> &'static str;
fn name(&self) -> &Text {
&self.locatable().name
}
fn archetype_node_id(&self) -> &str {
&self.locatable().archetype_node_id
}
fn uid(&self) -> Option<&UidBasedId> {
self.locatable().uid.as_ref()
}
fn links(&self) -> &[Link] {
&self.locatable().links
}
fn archetype_details(&self) -> Option<&Archetyped> {
self.locatable().archetype_details.as_deref()
}
fn feeder_audit(&self) -> Option<&FeederAudit> {
self.locatable().feeder_audit.as_deref()
}
fn is_archetype_root(&self) -> bool {
self.locatable().archetype_details.is_some()
}
fn concept(&self) -> Option<&Text> {
self.is_archetype_root().then(|| self.name())
}
}
macro_rules! impl_locatable {
($ty:ty, $class:literal) => {
impl $crate::rm::common::Locatable for $ty {
fn locatable(&self) -> &$crate::rm::common::LocatableAttrs {
&self.locatable
}
fn rm_type_name(&self) -> &'static str {
$class
}
}
};
}
pub(crate) use impl_locatable;
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct PartySelf {
#[serde(skip_serializing_if = "Option::is_none", default)]
external_ref: Option<PartyRef>,
}
impl PartySelf {
#[must_use]
pub fn anonymous() -> Self {
Self::default()
}
#[must_use]
pub fn with_external_ref(external_ref: PartyRef) -> Self {
Self {
external_ref: Some(external_ref),
}
}
#[must_use]
pub fn external_ref(&self) -> Option<&PartyRef> {
self.external_ref.as_ref()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PartyIdentified {
#[serde(skip_serializing_if = "Option::is_none", default)]
external_ref: Option<PartyRef>,
#[serde(skip_serializing_if = "Option::is_none", default)]
name: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
identifiers: Vec<DvIdentifier>,
}
impl PartyIdentified {
pub fn new(
name: Option<String>,
identifiers: Vec<DvIdentifier>,
external_ref: Option<PartyRef>,
) -> Result<Self, ParseError> {
if name.as_ref().is_some_and(String::is_empty) {
return Err(ParseError::invariant("PARTY_IDENTIFIED", "Name_valid"));
}
if name.is_none() && identifiers.is_empty() && external_ref.is_none() {
return Err(ParseError::invariant("PARTY_IDENTIFIED", "Basic_validity"));
}
Ok(Self {
external_ref,
name,
identifiers,
})
}
pub fn named(name: impl Into<String>) -> Result<Self, ParseError> {
Self::new(Some(name.into()), Vec::new(), None)
}
#[must_use]
pub fn name(&self) -> Option<&str> {
self.name.as_deref()
}
#[must_use]
pub fn identifiers(&self) -> &[DvIdentifier] {
&self.identifiers
}
#[must_use]
pub fn external_ref(&self) -> Option<&PartyRef> {
self.external_ref.as_ref()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PartyRelated {
#[serde(flatten)]
identified: PartyIdentified,
relationship: DvCodedText,
}
impl PartyRelated {
pub fn new(identified: PartyIdentified, relationship_code: &str) -> Result<Self, ParseError> {
let relationship = terminology::subject_relationship::GROUP
.coded_text(relationship_code)
.ok_or_else(|| ParseError::invariant("PARTY_RELATED", "Relationship_valid"))?;
Ok(Self {
identified,
relationship,
})
}
#[must_use]
pub fn from_coded(identified: PartyIdentified, relationship: DvCodedText) -> Self {
Self {
identified,
relationship,
}
}
#[must_use]
pub fn relationship(&self) -> &DvCodedText {
&self.relationship
}
#[must_use]
pub fn as_identified(&self) -> &PartyIdentified {
&self.identified
}
#[must_use]
pub fn is_self(&self) -> bool {
self.relationship.defining_code().is_openehr()
&& self.relationship.defining_code().code_string()
== terminology::subject_relationship::SELF
}
}
#[derive(Debug, Clone, PartialEq)]
#[allow(clippy::large_enum_variant)]
pub enum PartyProxy {
SelfParty(PartySelf),
Identified(PartyIdentified),
Related(PartyRelated),
}
impl PartyProxy {
#[must_use]
pub fn external_ref(&self) -> Option<&PartyRef> {
match self {
Self::SelfParty(p) => p.external_ref(),
Self::Identified(p) => p.external_ref(),
Self::Related(p) => p.as_identified().external_ref(),
}
}
#[must_use]
pub fn identifiers(&self) -> &[DvIdentifier] {
match self {
Self::SelfParty(_) => &[],
Self::Identified(p) => p.identifiers(),
Self::Related(p) => p.as_identified().identifiers(),
}
}
#[must_use]
pub fn name(&self) -> Option<&str> {
match self {
Self::SelfParty(_) => None,
Self::Identified(p) => p.name(),
Self::Related(p) => p.as_identified().name(),
}
}
#[must_use]
pub fn type_name(&self) -> &'static str {
match self {
Self::SelfParty(_) => "PARTY_SELF",
Self::Identified(_) => "PARTY_IDENTIFIED",
Self::Related(_) => "PARTY_RELATED",
}
}
#[must_use]
pub fn is_subject(&self) -> bool {
match self {
Self::SelfParty(_) => true,
Self::Identified(_) => false,
Self::Related(p) => p.is_self(),
}
}
}
impl From<PartySelf> for PartyProxy {
fn from(v: PartySelf) -> Self {
Self::SelfParty(v)
}
}
impl From<PartyIdentified> for PartyProxy {
fn from(v: PartyIdentified) -> Self {
Self::Identified(v)
}
}
impl From<PartyRelated> for PartyProxy {
fn from(v: PartyRelated) -> Self {
Self::Related(v)
}
}
impl Serialize for PartyProxy {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
#[derive(Serialize)]
struct Tagged<'a, T: Serialize> {
#[serde(rename = "_type")]
ty: &'static str,
#[serde(flatten)]
inner: &'a T,
}
match self {
Self::SelfParty(p) => Tagged {
ty: "PARTY_SELF",
inner: p,
}
.serialize(s),
Self::Identified(p) => Tagged {
ty: "PARTY_IDENTIFIED",
inner: p,
}
.serialize(s),
Self::Related(p) => Tagged {
ty: "PARTY_RELATED",
inner: p,
}
.serialize(s),
}
}
}
impl<'de> Deserialize<'de> for PartyProxy {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
use serde::de::Error as _;
#[derive(Deserialize)]
struct Wire {
#[serde(rename = "_type", default)]
ty: Option<String>,
#[serde(default)]
external_ref: Option<PartyRef>,
#[serde(default)]
name: Option<String>,
#[serde(default)]
identifiers: Vec<DvIdentifier>,
#[serde(default)]
relationship: Option<DvCodedText>,
}
let wire = Wire::deserialize(d)?;
let kind = match wire.ty.as_deref() {
Some(known @ ("PARTY_SELF" | "PARTY_IDENTIFIED" | "PARTY_RELATED")) => known,
Some(other) => {
return Err(D::Error::custom(format!(
"{other} is not a PARTY_PROXY class"
)));
}
None if wire.relationship.is_some() => "PARTY_RELATED",
None if wire.name.is_some() || !wire.identifiers.is_empty() => "PARTY_IDENTIFIED",
None => "PARTY_SELF",
};
if kind == "PARTY_SELF" {
return Ok(Self::SelfParty(PartySelf {
external_ref: wire.external_ref,
}));
}
let identified = PartyIdentified {
external_ref: wire.external_ref,
name: wire.name,
identifiers: wire.identifiers,
};
if kind == "PARTY_IDENTIFIED" {
return Ok(Self::Identified(identified));
}
let relationship = wire
.relationship
.ok_or_else(|| D::Error::missing_field("relationship"))?;
Ok(Self::Related(PartyRelated {
identified,
relationship,
}))
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Participation {
function: Text,
#[serde(skip_serializing_if = "Option::is_none", default)]
mode: Option<DvCodedText>,
performer: PartyProxy,
#[serde(skip_serializing_if = "Option::is_none", default)]
time: Option<Interval<DvDateTime>>,
}
impl Participation {
#[must_use]
pub fn new(function: Text, performer: PartyProxy) -> Self {
Self {
function,
mode: None,
performer,
time: None,
}
}
#[must_use]
pub fn with_mode(mut self, mode: DvCodedText) -> Self {
self.mode = Some(mode);
self
}
#[must_use]
pub fn with_time(mut self, time: Interval<DvDateTime>) -> Self {
self.time = Some(time);
self
}
#[must_use]
pub fn function(&self) -> &Text {
&self.function
}
#[must_use]
pub fn mode(&self) -> Option<&DvCodedText> {
self.mode.as_ref()
}
#[must_use]
pub fn performer(&self) -> &PartyProxy {
&self.performer
}
#[must_use]
pub fn time(&self) -> Option<&Interval<DvDateTime>> {
self.time.as_ref()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AuditDetails {
system_id: String,
time_committed: DvDateTime,
change_type: DvCodedText,
#[serde(skip_serializing_if = "Option::is_none", default)]
description: Option<Text>,
committer: PartyProxy,
}
impl AuditDetails {
pub fn new(
system_id: impl Into<String>,
time_committed: DvDateTime,
change_type_code: &str,
committer: PartyProxy,
) -> Result<Self, ParseError> {
let system_id = system_id.into();
if system_id.is_empty() {
return Err(ParseError::invariant("AUDIT_DETAILS", "System_id_valid"));
}
let change_type = terminology::audit_change_type::GROUP
.coded_text(change_type_code)
.ok_or_else(|| ParseError::invariant("AUDIT_DETAILS", "Change_type_valid"))?;
Ok(Self {
system_id,
time_committed,
change_type,
description: None,
committer,
})
}
#[must_use]
pub fn with_description(mut self, description: Text) -> Self {
self.description = Some(description);
self
}
#[must_use]
pub fn system_id(&self) -> &str {
&self.system_id
}
#[must_use]
pub fn time_committed(&self) -> &DvDateTime {
&self.time_committed
}
#[must_use]
pub fn change_type(&self) -> &DvCodedText {
&self.change_type
}
#[must_use]
pub fn description(&self) -> Option<&Text> {
self.description.as_ref()
}
#[must_use]
pub fn committer(&self) -> &PartyProxy {
&self.committer
}
#[must_use]
pub fn change_type_code(&self) -> &str {
self.change_type.defining_code().code_string()
}
#[must_use]
pub fn is_creation(&self) -> bool {
self.change_type_code() == terminology::audit_change_type::CREATION
}
#[must_use]
pub fn is_deletion(&self) -> bool {
self.change_type_code() == terminology::audit_change_type::DELETED
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Attestation {
#[serde(flatten)]
audit: AuditDetails,
#[serde(skip_serializing_if = "Option::is_none", default)]
attested_view: Option<DvMultimedia>,
#[serde(skip_serializing_if = "Option::is_none", default)]
proof: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
items: Vec<DvEhrUri>,
reason: Text,
is_pending: bool,
}
impl Attestation {
#[must_use]
pub fn new(audit: AuditDetails, reason: Text, is_pending: bool) -> Self {
Self {
audit,
attested_view: None,
proof: None,
items: Vec::new(),
reason,
is_pending,
}
}
#[must_use]
pub fn with_attested_view(mut self, view: DvMultimedia) -> Self {
self.attested_view = Some(view);
self
}
#[must_use]
pub fn with_proof(mut self, proof: impl Into<String>) -> Self {
self.proof = Some(proof.into());
self
}
#[must_use]
pub fn with_item(mut self, item: DvEhrUri) -> Self {
self.items.push(item);
self
}
#[must_use]
pub fn audit(&self) -> &AuditDetails {
&self.audit
}
#[must_use]
pub fn reason(&self) -> &Text {
&self.reason
}
#[must_use]
pub fn is_pending(&self) -> bool {
self.is_pending
}
#[must_use]
pub fn attested_view(&self) -> Option<&DvMultimedia> {
self.attested_view.as_ref()
}
#[must_use]
pub fn proof(&self) -> Option<&str> {
self.proof.as_deref()
}
#[must_use]
pub fn items(&self) -> &[DvEhrUri] {
&self.items
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RevisionHistoryItem {
version_id: ObjectVersionId,
audits: Vec<AuditDetails>,
}
impl RevisionHistoryItem {
pub fn new(version_id: ObjectVersionId, audits: Vec<AuditDetails>) -> Result<Self, ParseError> {
if audits.is_empty() {
return Err(ParseError::invariant(
"REVISION_HISTORY_ITEM",
"Audit_valid",
));
}
Ok(Self { version_id, audits })
}
#[must_use]
pub fn version_id(&self) -> &ObjectVersionId {
&self.version_id
}
#[must_use]
pub fn audits(&self) -> &[AuditDetails] {
&self.audits
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RevisionHistory {
items: Vec<RevisionHistoryItem>,
}
impl RevisionHistory {
pub fn new(items: Vec<RevisionHistoryItem>) -> Result<Self, ParseError> {
if items.is_empty() {
return Err(ParseError::invariant("REVISION_HISTORY", "Items_valid"));
}
Ok(Self { items })
}
#[must_use]
pub fn items(&self) -> &[RevisionHistoryItem] {
&self.items
}
#[must_use]
pub fn most_recent_version(&self) -> &ObjectVersionId {
self.items
.last()
.map(RevisionHistoryItem::version_id)
.expect("constructor rejects an empty revision history")
}
#[must_use]
pub fn most_recent_version_time_committed(&self) -> &DvDateTime {
self.items
.last()
.map(|i| i.audits()[0].time_committed())
.expect("constructor rejects an empty revision history")
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(bound(serialize = "T: Serialize", deserialize = "T: Deserialize<'de>"))]
#[serde(tag = "_type")]
#[allow(clippy::large_enum_variant)]
pub enum Version<T> {
#[serde(rename = "ORIGINAL_VERSION")]
Original(OriginalVersion<T>),
#[serde(rename = "IMPORTED_VERSION")]
Imported(ImportedVersion<T>),
}
impl<T> Version<T> {
#[must_use]
pub fn uid(&self) -> &ObjectVersionId {
match self {
Self::Original(v) => &v.uid,
Self::Imported(v) => &v.item.uid,
}
}
#[must_use]
pub fn preceding_version_uid(&self) -> Option<&ObjectVersionId> {
match self {
Self::Original(v) => v.preceding_version_uid.as_ref(),
Self::Imported(v) => v.item.preceding_version_uid.as_ref(),
}
}
#[must_use]
pub fn attestations(&self) -> &[Attestation] {
match self {
Self::Original(v) => &v.attestations,
Self::Imported(v) => &v.item.attestations,
}
}
#[must_use]
pub fn other_input_version_uids(&self) -> &[ObjectVersionId] {
match self {
Self::Original(v) => &v.other_input_version_uids,
Self::Imported(v) => &v.item.other_input_version_uids,
}
}
#[must_use]
pub fn signature(&self) -> Option<&str> {
match self {
Self::Original(v) => v.signature.as_deref(),
Self::Imported(v) => v.signature.as_deref(),
}
}
#[must_use]
pub fn data(&self) -> Option<&T> {
match self {
Self::Original(v) => v.data.as_ref(),
Self::Imported(v) => v.item.data.as_ref(),
}
}
#[must_use]
pub fn commit_audit(&self) -> &AuditDetails {
match self {
Self::Original(v) => &v.commit_audit,
Self::Imported(v) => &v.commit_audit,
}
}
#[must_use]
pub fn lifecycle_state_code(&self) -> &str {
match self {
Self::Original(v) => v.lifecycle_state.defining_code().code_string(),
Self::Imported(v) => v.item.lifecycle_state.defining_code().code_string(),
}
}
#[must_use]
pub fn is_deleted(&self) -> bool {
self.lifecycle_state_code() == terminology::version_lifecycle_state::DELETED
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(bound(serialize = "T: Serialize", deserialize = "T: Deserialize<'de>"))]
pub struct OriginalVersion<T> {
uid: ObjectVersionId,
#[serde(skip_serializing_if = "Option::is_none", default)]
preceding_version_uid: Option<ObjectVersionId>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
other_input_version_uids: Vec<ObjectVersionId>,
lifecycle_state: DvCodedText,
#[serde(skip_serializing_if = "Option::is_none", default)]
data: Option<T>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
attestations: Vec<Attestation>,
commit_audit: AuditDetails,
contribution: ObjectRef,
#[serde(skip_serializing_if = "Option::is_none", default)]
signature: Option<String>,
}
impl<T> OriginalVersion<T> {
#[must_use]
pub fn with_signature(mut self, signature: impl Into<String>) -> Self {
self.signature = Some(signature.into());
self
}
#[must_use]
pub fn signature(&self) -> Option<&str> {
self.signature.as_deref()
}
pub fn new(
uid: ObjectVersionId,
preceding_version_uid: Option<ObjectVersionId>,
lifecycle_state_code: &str,
data: Option<T>,
commit_audit: AuditDetails,
contribution: ObjectRef,
) -> Result<Self, ParseError> {
let lifecycle_state = terminology::version_lifecycle_state::GROUP
.coded_text(lifecycle_state_code)
.ok_or_else(|| ParseError::invariant("ORIGINAL_VERSION", "Lifecycle_state_valid"))?;
if data.is_none() && lifecycle_state_code != terminology::version_lifecycle_state::DELETED {
return Err(ParseError::invariant("ORIGINAL_VERSION", "Data_valid"));
}
if uid.version_tree_id().is_first() == preceding_version_uid.is_some() {
return Err(ParseError::invariant(
"VERSION",
"Preceding_version_uid_validity",
));
}
Ok(Self {
uid,
preceding_version_uid,
other_input_version_uids: Vec::new(),
signature: None,
lifecycle_state,
data,
attestations: Vec::new(),
commit_audit,
contribution,
})
}
#[must_use]
pub fn with_attestation(mut self, attestation: Attestation) -> Self {
self.attestations.push(attestation);
self
}
#[must_use]
pub fn with_other_input_version_uid(mut self, uid: ObjectVersionId) -> Self {
self.other_input_version_uids.push(uid);
self
}
#[must_use]
pub fn uid(&self) -> &ObjectVersionId {
&self.uid
}
#[must_use]
pub fn data(&self) -> Option<&T> {
self.data.as_ref()
}
#[must_use]
pub fn commit_audit(&self) -> &AuditDetails {
&self.commit_audit
}
#[must_use]
pub fn contribution(&self) -> &ObjectRef {
&self.contribution
}
#[must_use]
pub fn attestations(&self) -> &[Attestation] {
&self.attestations
}
#[must_use]
pub fn is_merged(&self) -> bool {
!self.other_input_version_uids.is_empty()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(bound(serialize = "T: Serialize", deserialize = "T: Deserialize<'de>"))]
pub struct ImportedVersion<T> {
item: OriginalVersion<T>,
commit_audit: AuditDetails,
contribution: ObjectRef,
#[serde(skip_serializing_if = "Option::is_none", default)]
signature: Option<String>,
}
impl<T> ImportedVersion<T> {
#[must_use]
pub fn new(
item: OriginalVersion<T>,
commit_audit: AuditDetails,
contribution: ObjectRef,
) -> Self {
Self {
item,
commit_audit,
contribution,
signature: None,
}
}
#[must_use]
pub fn item(&self) -> &OriginalVersion<T> {
&self.item
}
#[must_use]
pub fn commit_audit(&self) -> &AuditDetails {
&self.commit_audit
}
}
impl<T> From<OriginalVersion<T>> for Version<T> {
fn from(v: OriginalVersion<T>) -> Self {
Self::Original(v)
}
}
impl<T> From<ImportedVersion<T>> for Version<T> {
fn from(v: ImportedVersion<T>) -> Self {
Self::Imported(v)
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum CommitError {
#[error("version belongs to a different versioned object")]
WrongObject,
#[error("a version with this id already exists")]
DuplicateVersion,
#[error("preceding_version_uid is absent on a successor, or present on the first version")]
PrecedingVersionMismatch,
#[error("preceding version is not the current latest (concurrent modification)")]
NotLatest,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(bound(serialize = "T: Serialize", deserialize = "T: Deserialize<'de>"))]
pub struct VersionedObject<T> {
uid: HierObjectId,
owner_id: ObjectRef,
time_created: DvDateTime,
#[serde(skip_serializing_if = "Vec::is_empty", default = "Vec::new")]
versions: Vec<Version<T>>,
}
impl<T> VersionedObject<T> {
#[must_use]
pub fn new(uid: HierObjectId, owner_id: ObjectRef, time_created: DvDateTime) -> Self {
Self {
uid,
owner_id,
time_created,
versions: Vec::new(),
}
}
#[must_use]
pub fn uid(&self) -> &HierObjectId {
&self.uid
}
#[must_use]
pub fn owner_id(&self) -> &ObjectRef {
&self.owner_id
}
#[must_use]
pub fn time_created(&self) -> &DvDateTime {
&self.time_created
}
#[must_use]
pub fn all_versions(&self) -> &[Version<T>] {
&self.versions
}
#[must_use]
pub fn version_count(&self) -> usize {
self.versions.len()
}
#[must_use]
pub fn latest_version(&self) -> Option<&Version<T>> {
self.versions.last()
}
#[must_use]
pub fn version_with_id(&self, uid: &ObjectVersionId) -> Option<&Version<T>> {
self.versions.iter().find(|v| v.uid() == uid)
}
#[must_use]
pub fn has_version_id(&self, uid: &ObjectVersionId) -> bool {
self.version_with_id(uid).is_some()
}
#[must_use]
pub fn version_at_time(&self, time: &DvDateTime) -> Option<&Version<T>> {
self.versions.iter().rfind(|v| {
matches!(
v.commit_audit().time_committed().partial_cmp(time),
Some(core::cmp::Ordering::Less | core::cmp::Ordering::Equal)
)
})
}
#[must_use]
pub fn has_version_at_time(&self, time: &DvDateTime) -> bool {
self.version_at_time(time).is_some()
}
pub fn commit(&mut self, version: Version<T>) -> Result<(), CommitError> {
if version.uid().object_id() != self.uid.root() {
return Err(CommitError::WrongObject);
}
if self.has_version_id(version.uid()) {
return Err(CommitError::DuplicateVersion);
}
match (self.latest_version(), version.preceding_version_uid()) {
(None, None) => {}
(None, Some(_)) | (Some(_), None) => {
return Err(CommitError::PrecedingVersionMismatch);
}
(Some(latest), Some(preceding)) => {
if latest.uid() != preceding {
return Err(CommitError::NotLatest);
}
}
}
self.versions.push(version);
Ok(())
}
#[must_use]
pub fn revision_history(&self) -> Option<RevisionHistory> {
let items: Vec<RevisionHistoryItem> = self
.versions
.iter()
.filter_map(|v| {
RevisionHistoryItem::new(v.uid().clone(), vec![v.commit_audit().clone()]).ok()
})
.collect();
RevisionHistory::new(items).ok()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Contribution {
uid: HierObjectId,
versions: Vec<ObjectVersionId>,
audit: AuditDetails,
}
impl Contribution {
pub const PERMITTED_CHANGE_TYPES: [&'static str; 3] = [
terminology::audit_change_type::CREATION,
terminology::audit_change_type::AMENDMENT,
terminology::audit_change_type::DELETED,
];
pub fn new(
uid: HierObjectId,
versions: Vec<ObjectVersionId>,
audit: AuditDetails,
) -> Result<Self, ParseError> {
if versions.is_empty() {
return Err(ParseError::invariant("CONTRIBUTION", "Versions_valid"));
}
if !Self::PERMITTED_CHANGE_TYPES.contains(&audit.change_type_code()) {
return Err(ParseError::invariant(
"CONTRIBUTION",
"Audit_change_type_valid",
));
}
Ok(Self {
uid,
versions,
audit,
})
}
#[must_use]
pub fn uid(&self) -> &HierObjectId {
&self.uid
}
#[must_use]
pub fn versions(&self) -> &[ObjectVersionId] {
&self.versions
}
#[must_use]
pub fn audit(&self) -> &AuditDetails {
&self.audit
}
}
pub type DateValidity = Interval<DvDate>;
pub type NodeRef = LocatableRef;
#[cfg(test)]
mod tests {
use super::*;
use crate::base::{HierObjectId, ObjectId};
use crate::rm::data_types::DvText;
fn audit(code: &str) -> AuditDetails {
AuditDetails::new(
"ehr1.example.org",
DvDateTime::new("2026-07-31T09:15:00Z").unwrap(),
code,
PartyIdentified::named("Dr A Nurse").unwrap().into(),
)
.unwrap()
}
fn versioned() -> (VersionedObject<String>, ObjectRef) {
let uid = HierObjectId::from_uid_str("87284370-2D4B-4E3D-A3F3-F303D2F4F34B").unwrap();
let owner = ObjectRef::new("local", "EHR", ObjectId::HierObjectId(uid.clone())).unwrap();
(
VersionedObject::new(
uid,
owner.clone(),
DvDateTime::new("2026-07-31T09:00:00Z").unwrap(),
),
owner,
)
}
fn version(n: u32, preceding: Option<u32>, owner: &ObjectRef, code: &str) -> Version<String> {
let id = |v: u32| -> ObjectVersionId {
format!("87284370-2D4B-4E3D-A3F3-F303D2F4F34B::ehr1.example.org::{v}")
.parse()
.unwrap()
};
OriginalVersion::new(
id(n),
preceding.map(id),
terminology::version_lifecycle_state::COMPLETE,
Some(format!("content {n}")),
audit(code),
owner.clone(),
)
.unwrap()
.into()
}
#[test]
fn a_well_formed_history_commits() {
let (mut vo, owner) = versioned();
vo.commit(version(
1,
None,
&owner,
terminology::audit_change_type::CREATION,
))
.unwrap();
vo.commit(version(
2,
Some(1),
&owner,
terminology::audit_change_type::AMENDMENT,
))
.unwrap();
assert_eq!(vo.version_count(), 2);
assert_eq!(
vo.latest_version()
.unwrap()
.uid()
.version_tree_id()
.trunk_version(),
2
);
}
#[test]
fn concurrent_writes_are_refused_rather_than_silently_ordered() {
let (mut vo, owner) = versioned();
vo.commit(version(
1,
None,
&owner,
terminology::audit_change_type::CREATION,
))
.unwrap();
vo.commit(version(
2,
Some(1),
&owner,
terminology::audit_change_type::AMENDMENT,
))
.unwrap();
let stale = version(
3,
Some(1),
&owner,
terminology::audit_change_type::AMENDMENT,
);
assert_eq!(vo.commit(stale), Err(CommitError::NotLatest));
}
#[test]
fn a_version_of_another_object_is_refused() {
let (mut vo, owner) = versioned();
let foreign = OriginalVersion::new(
"11111111-2222-3333-4444-555555555555::ehr1.example.org::1"
.parse()
.unwrap(),
None,
terminology::version_lifecycle_state::COMPLETE,
Some("x".to_string()),
audit(terminology::audit_change_type::CREATION),
owner,
)
.unwrap();
assert_eq!(vo.commit(foreign.into()), Err(CommitError::WrongObject));
}
#[test]
fn a_duplicate_version_id_is_refused() {
let (mut vo, owner) = versioned();
vo.commit(version(
1,
None,
&owner,
terminology::audit_change_type::CREATION,
))
.unwrap();
assert_eq!(
vo.commit(version(
1,
None,
&owner,
terminology::audit_change_type::CREATION
)),
Err(CommitError::DuplicateVersion)
);
}
#[test]
fn a_deleted_version_may_have_no_data_and_others_may_not() {
let (_, owner) = versioned();
let id: ObjectVersionId = "87284370-2D4B-4E3D-A3F3-F303D2F4F34B::ehr1.example.org::2"
.parse()
.unwrap();
let preceding: ObjectVersionId =
"87284370-2D4B-4E3D-A3F3-F303D2F4F34B::ehr1.example.org::1"
.parse()
.unwrap();
assert!(
OriginalVersion::<String>::new(
id.clone(),
Some(preceding.clone()),
terminology::version_lifecycle_state::DELETED,
None,
audit(terminology::audit_change_type::DELETED),
owner.clone(),
)
.is_ok()
);
assert!(
OriginalVersion::<String>::new(
id,
Some(preceding),
terminology::version_lifecycle_state::COMPLETE,
None,
audit(terminology::audit_change_type::CREATION),
owner,
)
.is_err()
);
}
#[test]
fn a_version_number_and_its_predecessor_must_agree() {
let (_, owner) = versioned();
let first: ObjectVersionId = "87284370-2D4B-4E3D-A3F3-F303D2F4F34B::ehr1.example.org::1"
.parse()
.unwrap();
let second: ObjectVersionId = "87284370-2D4B-4E3D-A3F3-F303D2F4F34B::ehr1.example.org::2"
.parse()
.unwrap();
let build = |uid: ObjectVersionId, preceding: Option<ObjectVersionId>| {
OriginalVersion::new(
uid,
preceding,
terminology::version_lifecycle_state::COMPLETE,
Some("content".to_owned()),
audit(terminology::audit_change_type::CREATION),
owner.clone(),
)
};
assert!(build(first.clone(), None).is_ok());
assert!(build(second.clone(), Some(first.clone())).is_ok());
assert_eq!(
build(first, Some(second.clone())).unwrap_err().to_string(),
ParseError::invariant("VERSION", "Preceding_version_uid_validity").to_string()
);
assert_eq!(
build(second, None).unwrap_err().to_string(),
ParseError::invariant("VERSION", "Preceding_version_uid_validity").to_string()
);
}
#[test]
fn contribution_change_types_are_restricted() {
let uid = HierObjectId::from_uid_str("87284370-2D4B-4E3D-A3F3-F303D2F4F34B").unwrap();
let vs: Vec<ObjectVersionId> = vec![
"87284370-2D4B-4E3D-A3F3-F303D2F4F34B::s.example::1"
.parse()
.unwrap(),
];
assert!(
Contribution::new(
uid.clone(),
vs.clone(),
audit(terminology::audit_change_type::CREATION)
)
.is_ok()
);
assert!(
Contribution::new(
uid.clone(),
vs,
audit(terminology::audit_change_type::SYNTHESIS)
)
.is_err()
);
assert!(
Contribution::new(
uid,
Vec::new(),
audit(terminology::audit_change_type::CREATION)
)
.is_err()
);
}
#[test]
fn party_proxy_infers_its_class_without_a_type_tag() {
let related: PartyProxy = serde_json::from_str(
r#"{"name":"Jane","relationship":{"value":"mother","defining_code":{"terminology_id":{"value":"openehr"},"code_string":"10"}}}"#,
)
.unwrap();
assert_eq!(related.type_name(), "PARTY_RELATED");
assert!(!related.is_subject());
let identified: PartyProxy = serde_json::from_str(r#"{"name":"Dr A Nurse"}"#).unwrap();
assert_eq!(identified.type_name(), "PARTY_IDENTIFIED");
let subject: PartyProxy = serde_json::from_str("{}").unwrap();
assert_eq!(subject.type_name(), "PARTY_SELF");
assert!(subject.is_subject());
}
#[test]
fn a_self_related_party_counts_as_the_subject() {
let self_related = PartyRelated::new(
PartyIdentified::named("The patient").unwrap(),
terminology::subject_relationship::SELF,
)
.unwrap();
assert!(PartyProxy::Related(self_related).is_subject());
}
#[test]
fn version_at_time_skips_incomparable_commit_times() {
let (mut vo, owner) = versioned();
vo.commit(version(
1,
None,
&owner,
terminology::audit_change_type::CREATION,
))
.unwrap();
let local = DvDateTime::new("2026-07-31T10:00:00").unwrap();
assert!(vo.version_at_time(&local).is_none());
let utc = DvDateTime::new("2026-07-31T10:00:00Z").unwrap();
assert!(vo.version_at_time(&utc).is_some());
}
#[test]
fn audit_details_report_the_change_they_record() {
let a = audit(terminology::audit_change_type::CREATION)
.with_description(Text::Plain(DvText::new("initial commit").unwrap()));
assert_eq!(a.system_id(), "ehr1.example.org");
assert_eq!(a.time_committed().as_str(), "2026-07-31T09:15:00Z");
assert_eq!(a.change_type_code(), terminology::audit_change_type::CREATION);
assert_eq!(a.description().map(Text::value), Some("initial commit"));
assert_eq!(a.committer().name(), Some("Dr A Nurse"));
assert!(a.is_creation());
assert!(!a.is_deletion());
let deleted = audit(terminology::audit_change_type::DELETED);
assert!(deleted.is_deletion());
assert!(!deleted.is_creation(), "a deletion was reported as a creation");
let amended = audit(terminology::audit_change_type::AMENDMENT);
assert!(!amended.is_creation());
assert!(!amended.is_deletion());
assert_eq!(audit(terminology::audit_change_type::CREATION).description(), None);
assert!(
AuditDetails::new(
"",
DvDateTime::new("2026-07-31T09:15:00Z").unwrap(),
terminology::audit_change_type::CREATION,
PartyIdentified::named("Dr A Nurse").unwrap().into(),
)
.is_err()
);
}
#[test]
fn an_original_version_reports_its_content_and_provenance() {
let (_, owner) = versioned();
let id = |v: u32| -> ObjectVersionId {
format!("87284370-2D4B-4E3D-A3F3-F303D2F4F34B::ehr1.example.org::{v}")
.parse()
.unwrap()
};
let v = OriginalVersion::new(
id(1),
None,
terminology::version_lifecycle_state::COMPLETE,
Some("content".to_owned()),
audit(terminology::audit_change_type::CREATION),
owner.clone(),
)
.unwrap();
assert_eq!(v.uid(), &id(1));
assert_eq!(v.data(), Some(&"content".to_owned()));
assert_eq!(v.commit_audit().system_id(), "ehr1.example.org");
assert_eq!(v.contribution(), &owner);
assert!(v.attestations().is_empty());
assert!(!v.is_merged());
let merged = OriginalVersion::new(
id(2),
Some(id(1)),
terminology::version_lifecycle_state::COMPLETE,
Some("merged".to_owned()),
audit(terminology::audit_change_type::AMENDMENT),
owner.clone(),
)
.unwrap()
.with_other_input_version_uid(id(1));
assert!(merged.is_merged(), "a version with another input is a merge");
assert_eq!(merged.data(), Some(&"merged".to_owned()));
let deleted = OriginalVersion::new(
id(3),
Some(id(2)),
terminology::version_lifecycle_state::DELETED,
None::<String>,
audit(terminology::audit_change_type::DELETED),
owner,
)
.unwrap();
assert_eq!(deleted.data(), None);
assert!(deleted.commit_audit().is_deletion());
}
#[test]
fn a_versioned_object_reports_its_history_and_what_was_current_when() {
let (mut vo, owner) = versioned();
vo.commit(version(1, None, &owner, terminology::audit_change_type::CREATION))
.unwrap();
vo.commit(version(
2,
Some(1),
&owner,
terminology::audit_change_type::AMENDMENT,
))
.unwrap();
assert_eq!(vo.version_count(), 2);
assert_eq!(vo.all_versions().len(), 2, "the history was reported empty");
assert_eq!(vo.latest_version().and_then(Version::data), Some(&"content 2".to_owned()));
let before: DvDateTime = "2026-07-31T09:00:00Z".parse().unwrap();
let after: DvDateTime = "2026-07-31T10:00:00Z".parse().unwrap();
assert!(
!vo.has_version_at_time(&before),
"a version was reported before the record existed"
);
assert!(vo.has_version_at_time(&after));
assert!(vo.version_at_time(&before).is_none());
assert!(vo.version_at_time(&after).is_some());
}
#[test]
fn a_party_reports_every_way_it_identifies_someone() {
let nhs = DvIdentifier::new("943-476-5919").unwrap();
let person = PartyRef::new(
"demographic",
"PERSON",
ObjectId::HierObjectId(
HierObjectId::from_uid_str("6BA7B810-9DAD-11D1-80B4-00C04FD430C8").unwrap(),
),
)
.unwrap();
let full = PartyIdentified::new(
Some("Dr A Nurse".to_owned()),
vec![nhs.clone()],
Some(person.clone()),
)
.unwrap();
assert_eq!(full.name(), Some("Dr A Nurse"));
assert_eq!(full.identifiers(), &[nhs.clone()][..]);
assert_eq!(full.external_ref(), Some(&person));
let by_name = PartyIdentified::named("Dr A Nurse").unwrap();
assert_eq!(by_name.name(), Some("Dr A Nurse"));
assert!(by_name.identifiers().is_empty());
assert_eq!(by_name.external_ref(), None);
let by_id = PartyIdentified::new(None, vec![nhs.clone()], None).unwrap();
assert_eq!(by_id.name(), None);
assert_eq!(by_id.identifiers(), &[nhs][..]);
let by_ref = PartyIdentified::new(None, Vec::new(), Some(person.clone())).unwrap();
assert_eq!(by_ref.external_ref(), Some(&person));
assert!(PartyIdentified::new(None, Vec::new(), None).is_err());
assert!(PartyIdentified::named("").is_err());
let proxy: PartyProxy = full.into();
assert_eq!(proxy.name(), Some("Dr A Nurse"));
assert_eq!(proxy.identifiers().len(), 1);
assert_eq!(proxy.external_ref(), Some(&person));
assert!(!proxy.is_subject(), "an identified party is not the subject");
let anonymous = PartySelf::anonymous();
assert_eq!(anonymous.external_ref(), None);
let known = PartySelf::with_external_ref(person.clone());
assert_eq!(known.external_ref(), Some(&person));
assert_ne!(anonymous, known);
let self_proxy: PartyProxy = anonymous.into();
assert!(self_proxy.is_subject(), "PARTY_SELF is the subject of the record");
assert_eq!(self_proxy.name(), None);
assert!(self_proxy.identifiers().is_empty());
}
#[test]
fn a_locatable_reports_its_identity_links_and_provenance() {
let bare = LocatableAttrs::named("Encounter", "at0000").unwrap();
assert_eq!(bare.archetype_node_id(), "at0000");
assert_eq!(bare.uid(), None);
let uid = UidBasedId::from("6BA7B810-9DAD-11D1-80B4-00C04FD430C8"
.parse::<HierObjectId>()
.unwrap());
let link = Link::new(
Text::Plain(DvText::new("follow-up").unwrap()),
Text::Plain(DvText::new("issue").unwrap()),
DvEhrUri::new("ehr://example.org/records/1").unwrap(),
);
let feeder = FeederAudit::new(
FeederAuditDetails::new("lab.example.org")
.unwrap()
.with_time("2026-07-30T08:00:00Z".parse().unwrap()),
)
.with_originating_item_id(DvIdentifier::new("LAB-7").unwrap());
let attrs = LocatableAttrs::named("Encounter", "at0000")
.unwrap()
.with_uid(uid.clone())
.with_link(link.clone())
.with_feeder_audit(feeder);
assert_eq!(attrs.uid(), Some(&uid));
let full = crate::rm::data_structures::Element::new(
attrs,
DataValue::Text(DvText::new("value").unwrap()),
);
assert_eq!(Locatable::uid(&full), Some(&uid));
assert_eq!(full.links(), &[link][..]);
let audit = full.feeder_audit().expect("a feeder audit was recorded");
assert_eq!(audit.originating_system_audit().system_id(), "lab.example.org");
assert_eq!(
audit.originating_system_audit().time().map(DvDateTime::as_str),
Some("2026-07-30T08:00:00Z")
);
assert_eq!(audit.originating_system_item_ids().len(), 1);
assert_eq!(audit.original_content(), None);
let details: FeederAuditDetails = serde_json::from_str(
r#"{"system_id":"lab.example.org","version_id":"rev-42"}"#,
)
.expect("deserialize");
assert_eq!(details.version_id(), Some("rev-42"));
assert_eq!(details.system_id(), "lab.example.org");
assert_eq!(details.time(), None);
let plain = FeederAuditDetails::new("lab.example.org").unwrap();
assert_eq!(plain.version_id(), None);
}
#[test]
fn the_optional_parts_of_a_version_and_its_contribution_are_reported() {
let (_, owner) = versioned();
let id = |v: u32| -> ObjectVersionId {
format!("87284370-2D4B-4E3D-A3F3-F303D2F4F34B::ehr1.example.org::{v}")
.parse()
.unwrap()
};
let attestation = Attestation::new(
audit(terminology::audit_change_type::ATTESTATION),
Text::Plain(DvText::new("countersigned").unwrap()),
false,
);
let original = OriginalVersion::new(
id(1),
None,
terminology::version_lifecycle_state::COMPLETE,
Some("content".to_owned()),
audit(terminology::audit_change_type::CREATION),
owner.clone(),
)
.unwrap()
.with_attestation(attestation.clone())
.with_other_input_version_uid(id(1));
assert_eq!(original.attestations().len(), 1, "an attestation was dropped");
assert_eq!(
original.attestations()[0].reason().value(),
"countersigned"
);
let live: Version<String> = original.into();
assert!(!live.is_deleted(), "a complete version was reported deleted");
assert_eq!(live.attestations().len(), 1, "an attestation was dropped");
assert_eq!(live.other_input_version_uids(), &[id(1)][..]);
let deleted: Version<String> = OriginalVersion::new(
id(2),
Some(id(1)),
terminology::version_lifecycle_state::DELETED,
None::<String>,
audit(terminology::audit_change_type::DELETED),
owner.clone(),
)
.unwrap()
.into();
assert!(deleted.is_deleted());
assert_ne!(live.is_deleted(), deleted.is_deleted());
assert!(deleted.attestations().is_empty());
let contribution = Contribution::new(
HierObjectId::from_uid_str("6BA7B810-9DAD-11D1-80B4-00C04FD430C8").unwrap(),
vec![id(1), id(2)],
audit(terminology::audit_change_type::CREATION),
)
.unwrap();
assert_eq!(contribution.versions(), &[id(1), id(2)][..]);
assert_eq!(contribution.audit().system_id(), "ehr1.example.org");
assert!(
Contribution::new(
HierObjectId::from_uid_str("6BA7B810-9DAD-11D1-80B4-00C04FD430C8").unwrap(),
Vec::new(),
audit(terminology::audit_change_type::CREATION),
)
.is_err(),
"a contribution with no versions was accepted"
);
}
#[test]
fn optional_attributes_are_absent_only_when_they_were_not_recorded() {
let bare = Archetyped::new("openEHR-EHR-COMPOSITION.encounter.v1", "1.1.0").unwrap();
assert_eq!(bare.template_id(), None);
assert_eq!(bare.rm_version(), "1.1.0");
let templated = Archetyped::new("openEHR-EHR-COMPOSITION.encounter.v1", "1.1.0")
.unwrap()
.with_template("vital_signs.v1")
.unwrap();
assert_eq!(
templated.template_id().map(ToString::to_string),
Some("vital_signs.v1".to_owned())
);
let performer: PartyProxy = PartyIdentified::named("Dr A Nurse").unwrap().into();
let p = Participation::new(
Text::Plain(DvText::new("performer").unwrap()),
performer.clone(),
);
assert_eq!(p.time(), None);
let window = Interval::closed(
DvDateTime::new("2026-07-31T09:00:00Z").unwrap(),
DvDateTime::new("2026-07-31T09:30:00Z").unwrap(),
)
.unwrap();
let timed = Participation::new(
Text::Plain(DvText::new("performer").unwrap()),
performer,
)
.with_time(window.clone());
assert_eq!(timed.time(), Some(&window));
let plain = FeederAudit::new(FeederAuditDetails::new("lab.example.org").unwrap());
assert_eq!(plain.original_content(), None);
let original = DataValue::Parsable(
crate::rm::data_types::DvParsable::new("OBX|1|NM|...", "HL7v2").unwrap(),
);
let kept = FeederAudit::new(FeederAuditDetails::new("lab.example.org").unwrap())
.with_original_content(original.clone())
.unwrap();
assert_eq!(kept.original_content(), Some(&original));
let anonymous = PartySelf::anonymous();
assert_eq!(anonymous.external_ref(), None);
assert_eq!(anonymous, PartySelf::default());
}
}