#![expect(
clippy::disallowed_types,
reason = "the wire-boundary validation reads the canonical JSON node before the typed decode \
(#1694 boundary class)"
)]
use serde_json::Value;
use crate::v1_2::common::change_control::version::Version;
use crate::v1_2::data_types::text::dv_coded_text::DvCodedText;
use openehr_base::v1_3::prelude::{HierObjectId, ObjectVersionId};
impl<T> Version<T> {
#[must_use]
pub fn uid(&self) -> &ObjectVersionId {
match self {
Version::OriginalVersion(original) => &original.uid,
Version::ImportedVersion(imported) => imported.uid(),
}
}
#[must_use]
pub fn owner_id(&self) -> HierObjectId {
HierObjectId::from(self.uid().object_id())
}
#[must_use]
pub fn is_branch(&self) -> bool {
self.uid().is_branch()
}
#[must_use]
pub fn preceding_version_uid(&self) -> Option<&ObjectVersionId> {
match self {
Version::OriginalVersion(original) => original.preceding_version_uid.as_ref(),
Version::ImportedVersion(imported) => imported.preceding_version_uid(),
}
}
#[must_use]
pub fn lifecycle_state(&self) -> &DvCodedText {
match self {
Version::OriginalVersion(original) => &original.lifecycle_state,
Version::ImportedVersion(imported) => imported.lifecycle_state(),
}
}
#[must_use]
pub fn data(&self) -> Option<&T> {
match self {
Version::OriginalVersion(original) => original.data.as_ref(),
Version::ImportedVersion(imported) => imported.data(),
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum CanonicalFormError {
#[error("RFC 8785 (JCS) canonicalisation failed")]
Canonicalize(#[source] serde_json::Error),
}
pub fn canonical_form_of_json(value: &Value) -> Result<String, CanonicalFormError> {
match value {
Value::Object(map) if map.contains_key("signature") => {
let trimmed: std::collections::BTreeMap<&str, &Value> = map
.iter()
.filter(|(k, _)| k.as_str() != "signature")
.map(|(k, v)| (k.as_str(), v))
.collect();
serde_jcs::to_string(&trimmed).map_err(CanonicalFormError::Canonicalize)
}
_ => serde_jcs::to_string(value).map_err(CanonicalFormError::Canonicalize),
}
}
pub fn canonical_form_of_json_with_data(
envelope: &Value,
data: &Value,
) -> Result<String, CanonicalFormError> {
let Value::Object(map) = envelope else {
return canonical_form_of_json(envelope);
};
let mut view: std::collections::BTreeMap<&str, &Value> = map
.iter()
.filter(|(k, _)| k.as_str() != "signature")
.map(|(k, v)| (k.as_str(), v))
.collect();
if !data.is_null() {
view.insert("data", data);
}
serde_jcs::to_string(&view).map_err(CanonicalFormError::Canonicalize)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::v1_2::common::change_control::imported_version::ImportedVersion;
use crate::v1_2::common::change_control::original_version::OriginalVersion;
use crate::v1_2::common::generic::audit_details::{AuditDetails, AuditDetailsData};
use crate::v1_2::common::generic::party_proxy::PartyProxy;
use crate::v1_2::common::generic::party_self::PartySelf;
use crate::v1_2::data_types::quantity::date_time::dv_date_time::DvDateTime;
use crate::v1_2::data_types::text::code_phrase::CodePhrase;
use crate::v1_2::data_types::text::dv_coded_text::DvCodedText;
use openehr_base::v1_3::prelude::{ObjectId, ObjectRef, ObjectRefData, TerminologyId};
use openehr_term::codes::{AuditChangeType, VersionLifecycleState};
fn typed_original(value: &str) -> OriginalVersion<String> {
OriginalVersion {
contribution: ObjectRef::ObjectRef(ObjectRefData {
namespace: "local".to_owned(),
r#type: "CONTRIBUTION".to_owned(),
id: ObjectId::HierObjectId(
HierObjectId::new("11111111-1111-4111-8111-111111111111".to_owned())
.expect("a well-formed identifier"),
),
}),
signature: None,
commit_audit: AuditDetails::AuditDetails(AuditDetailsData {
system_id: "ferroehr.local".to_owned(),
time_committed: DvDateTime {
normal_status: None,
normal_range: None,
other_reference_ranges: openehr_base::containers::present_nonempty(Vec::new()),
magnitude_status: None,
accuracy: None,
value: "2026-07-07T10:11:12Z".to_owned(),
},
change_type: coded(
AuditChangeType::Creation.code(),
AuditChangeType::Creation.rubric(),
),
description: None,
committer: PartyProxy::PartySelf(PartySelf { external_ref: None }),
}),
uid: ObjectVersionId::new(value.to_owned()).expect("a well-formed identifier"),
preceding_version_uid: None,
other_input_version_uids: openehr_base::containers::present_nonempty(Vec::new()),
lifecycle_state: coded(
VersionLifecycleState::Complete.code(),
VersionLifecycleState::Complete.rubric(),
),
attestations: openehr_base::containers::present_nonempty(Vec::new()),
data: Some("content".to_owned()),
}
}
fn typed_version(value: &str) -> Version<String> {
Version::OriginalVersion(typed_original(value))
}
fn coded(code: &str, rubric: &str) -> DvCodedText {
DvCodedText {
value: rubric.to_owned(),
hyperlink: None,
formatting: None,
mappings: openehr_base::containers::present_nonempty(Vec::new()),
language: None,
encoding: None,
defining_code: CodePhrase {
terminology_id: TerminologyId {
value: "openehr".to_owned(),
},
code_string: code.to_owned(),
preferred_term: None,
},
}
}
#[test]
fn owner_id_is_the_uids_object_id() {
let version = typed_version("8849182c-82ad-4088-a07f-48ead4180515::ferroehr.local::2.1.1");
assert_eq!(
version.owner_id().value(),
"8849182c-82ad-4088-a07f-48ead4180515"
);
assert_eq!(
version.owner_id(),
HierObjectId::from(version.uid().object_id()),
"Owner_id_valid"
);
}
#[test]
fn is_branch_follows_the_version_tree_id() {
assert!(
!typed_version("8849182c-82ad-4088-a07f-48ead4180515::ferroehr.local::2").is_branch()
);
assert!(
typed_version("8849182c-82ad-4088-a07f-48ead4180515::ferroehr.local::2.1.1")
.is_branch()
);
}
#[test]
fn an_imported_version_derives_its_identity_from_the_wrapped_original() {
let item = typed_original("8849182c-82ad-4088-a07f-48ead4180515::remote.example::3.2.1");
let wrapper = Version::ImportedVersion(ImportedVersion {
contribution: ObjectRef::ObjectRef(ObjectRefData {
namespace: "local".to_owned(),
r#type: "CONTRIBUTION".to_owned(),
id: ObjectId::HierObjectId(
HierObjectId::new("22222222-2222-4222-8222-222222222222".to_owned())
.expect("a well-formed identifier"),
),
}),
signature: Some("local-wrapper-signature".to_owned()),
commit_audit: AuditDetails::AuditDetails(AuditDetailsData {
system_id: "local.example".to_owned(),
time_committed: DvDateTime {
normal_status: None,
normal_range: None,
other_reference_ranges: openehr_base::containers::present_nonempty(Vec::new()),
magnitude_status: None,
accuracy: None,
value: "2026-07-09T08:00:00Z".to_owned(),
},
change_type: coded(
AuditChangeType::Creation.code(),
AuditChangeType::Creation.rubric(),
),
description: None,
committer: PartyProxy::PartySelf(PartySelf { external_ref: None }),
}),
item,
});
assert_eq!(
wrapper.uid().value(),
"8849182c-82ad-4088-a07f-48ead4180515::remote.example::3.2.1"
);
assert_eq!(
wrapper.owner_id().value(),
"8849182c-82ad-4088-a07f-48ead4180515"
);
assert!(wrapper.is_branch());
}
#[test]
fn an_original_version_answers_from_its_own_attributes() {
let mut original =
typed_original("8849182c-82ad-4088-a07f-48ead4180515::ferroehr.local::2");
original.preceding_version_uid = Some(
ObjectVersionId::new(
"8849182c-82ad-4088-a07f-48ead4180515::ferroehr.local::1".to_owned(),
)
.expect("a well-formed identifier"),
);
let version = Version::OriginalVersion(original);
assert_eq!(
version.preceding_version_uid().map(ObjectVersionId::value),
Some("8849182c-82ad-4088-a07f-48ead4180515::ferroehr.local::1")
);
assert_eq!(
version.lifecycle_state().value,
VersionLifecycleState::Complete.rubric()
);
assert_eq!(version.data(), Some(&"content".to_owned()));
}
#[test]
fn the_absent_cases_are_version_states_not_missing_values() {
let mut original =
typed_original("8849182c-82ad-4088-a07f-48ead4180515::ferroehr.local::1");
original.data = None;
let version = Version::OriginalVersion(original);
assert!(version.preceding_version_uid().is_none());
assert!(version.data().is_none());
}
#[test]
fn an_imported_version_answers_from_the_wrapped_original() {
let mut item = typed_original("8849182c-82ad-4088-a07f-48ead4180515::remote.example::3");
item.preceding_version_uid = Some(
ObjectVersionId::new(
"8849182c-82ad-4088-a07f-48ead4180515::remote.example::2".to_owned(),
)
.expect("a well-formed identifier"),
);
item.data = Some("imported content".to_owned());
let wrapper = Version::ImportedVersion(ImportedVersion {
contribution: ObjectRef::ObjectRef(ObjectRefData {
namespace: "local".to_owned(),
r#type: "CONTRIBUTION".to_owned(),
id: ObjectId::HierObjectId(
HierObjectId::new("33333333-3333-4333-8333-333333333333".to_owned())
.expect("a well-formed identifier"),
),
}),
signature: None,
commit_audit: AuditDetails::AuditDetails(AuditDetailsData {
system_id: "local.example".to_owned(),
time_committed: DvDateTime {
normal_status: None,
normal_range: None,
other_reference_ranges: openehr_base::containers::present_nonempty(Vec::new()),
magnitude_status: None,
accuracy: None,
value: "2026-07-09T08:00:00Z".to_owned(),
},
change_type: coded(
AuditChangeType::Creation.code(),
AuditChangeType::Creation.rubric(),
),
description: None,
committer: PartyProxy::PartySelf(PartySelf { external_ref: None }),
}),
item,
});
assert_eq!(
wrapper.preceding_version_uid().map(ObjectVersionId::value),
Some("8849182c-82ad-4088-a07f-48ead4180515::remote.example::2")
);
assert_eq!(
wrapper.lifecycle_state().value,
VersionLifecycleState::Complete.rubric()
);
assert_eq!(wrapper.data(), Some(&"imported content".to_owned()));
}
}