use crate::base::uid::Uid;
use crate::error::ParseError;
use core::fmt;
use core::str::FromStr;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct HierObjectId {
root: Uid,
extension: Option<String>,
}
impl HierObjectId {
pub fn new(root: Uid, extension: Option<String>) -> Result<Self, ParseError> {
if let Some(ext) = &extension {
if ext.is_empty() {
return Err(ParseError::new("HIER_OBJECT_ID", "empty extension", ""));
}
if ext.contains("::") {
return Err(ParseError::new(
"HIER_OBJECT_ID",
"extension contains the `::` separator",
ext,
));
}
}
Ok(Self { root, extension })
}
pub fn from_uid_str(uid: &str) -> Result<Self, ParseError> {
Ok(Self {
root: uid.parse()?,
extension: None,
})
}
#[must_use]
pub fn root(&self) -> &Uid {
&self.root
}
#[must_use]
pub fn extension(&self) -> Option<&str> {
self.extension.as_deref()
}
}
impl fmt::Display for HierObjectId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.extension {
Some(ext) => write!(f, "{}::{ext}", self.root),
None => write!(f, "{}", self.root),
}
}
}
impl FromStr for HierObjectId {
type Err = ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.split_once("::") {
Some((root, ext)) => Self::new(root.parse()?, Some(ext.to_owned())),
None => Self::new(s.parse()?, None),
}
}
}
crate::impl_valued_serde!(HierObjectId, "HIER_OBJECT_ID");
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[allow(clippy::struct_field_names)]
pub struct ObjectVersionId {
object_id: Uid,
creating_system_id: Uid,
version_tree_id: VersionTreeId,
}
impl ObjectVersionId {
#[must_use]
pub fn new(object_id: Uid, creating_system_id: Uid, version_tree_id: VersionTreeId) -> Self {
Self {
object_id,
creating_system_id,
version_tree_id,
}
}
#[must_use]
pub fn object_id(&self) -> &Uid {
&self.object_id
}
#[must_use]
pub fn creating_system_id(&self) -> &Uid {
&self.creating_system_id
}
#[must_use]
pub fn version_tree_id(&self) -> &VersionTreeId {
&self.version_tree_id
}
#[must_use]
pub fn is_branch(&self) -> bool {
self.version_tree_id.is_branch()
}
#[must_use]
pub fn same_object_as(&self, other: &Self) -> bool {
self.object_id == other.object_id
}
}
impl fmt::Display for ObjectVersionId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}::{}::{}",
self.object_id, self.creating_system_id, self.version_tree_id
)
}
}
impl FromStr for ObjectVersionId {
type Err = ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let parts: Vec<&str> = s.split("::").collect();
let [object_id, creating_system_id, version_tree_id] = parts.as_slice() else {
return Err(ParseError::new(
"OBJECT_VERSION_ID",
"expected object_id::creating_system_id::version_tree_id",
s,
));
};
Ok(Self {
object_id: object_id.parse()?,
creating_system_id: creating_system_id.parse()?,
version_tree_id: version_tree_id.parse()?,
})
}
}
crate::impl_valued_serde!(ObjectVersionId, "OBJECT_VERSION_ID");
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct VersionTreeId {
trunk_version: u32,
branch: Option<(u32, u32)>,
}
impl VersionTreeId {
pub const FIRST: Self = Self {
trunk_version: 1,
branch: None,
};
pub fn trunk(trunk_version: u32) -> Result<Self, ParseError> {
if trunk_version == 0 {
return Err(ParseError::new(
"VERSION_TREE_ID",
"trunk_version is 0",
"0",
));
}
Ok(Self {
trunk_version,
branch: None,
})
}
pub fn branch(
trunk_version: u32,
branch_number: u32,
branch_version: u32,
) -> Result<Self, ParseError> {
if trunk_version == 0 || branch_number == 0 || branch_version == 0 {
return Err(ParseError::new(
"VERSION_TREE_ID",
"version numbers start at 1",
"",
));
}
Ok(Self {
trunk_version,
branch: Some((branch_number, branch_version)),
})
}
#[must_use]
pub fn trunk_version(&self) -> u32 {
self.trunk_version
}
#[must_use]
pub fn branch_number(&self) -> Option<u32> {
self.branch.map(|(n, _)| n)
}
#[must_use]
pub fn branch_version(&self) -> Option<u32> {
self.branch.map(|(_, v)| v)
}
#[must_use]
pub fn is_branch(&self) -> bool {
self.branch.is_some()
}
#[must_use]
pub fn is_first(&self) -> bool {
*self == Self::FIRST
}
#[must_use]
pub fn next(&self) -> Self {
match self.branch {
None => Self {
trunk_version: self
.trunk_version
.checked_add(1)
.expect("version tree trunk overflowed u32"),
branch: None,
},
Some((n, v)) => Self {
trunk_version: self.trunk_version,
branch: Some((
n,
v.checked_add(1)
.expect("version tree branch overflowed u32"),
)),
},
}
}
}
impl fmt::Display for VersionTreeId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.branch {
None => write!(f, "{}", self.trunk_version),
Some((n, v)) => write!(f, "{}.{n}.{v}", self.trunk_version),
}
}
}
impl FromStr for VersionTreeId {
type Err = ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
fn num(part: &str, whole: &str) -> Result<u32, ParseError> {
if part.is_empty() || !part.bytes().all(|b| b.is_ascii_digit()) {
return Err(ParseError::new("VERSION_TREE_ID", "not a number", whole));
}
if part.len() > 1 && part.starts_with('0') {
return Err(ParseError::new(
"VERSION_TREE_ID",
"leading zero would not round-trip",
whole,
));
}
part.parse().map_err(|_| {
ParseError::new("VERSION_TREE_ID", "number does not fit in u32", whole)
})
}
let parts: Vec<&str> = s.split('.').collect();
match parts.as_slice() {
[trunk] => Self::trunk(num(trunk, s)?),
[trunk, branch_number, branch_version] => Self::branch(
num(trunk, s)?,
num(branch_number, s)?,
num(branch_version, s)?,
),
_ => Err(ParseError::new(
"VERSION_TREE_ID",
"expected `trunk` or `trunk.branch_number.branch_version`",
s,
)),
}
}
}
crate::impl_string_serde!(VersionTreeId, "VERSION_TREE_ID");
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ArchetypeId {
rm_originator: String,
rm_name: String,
rm_entity: String,
domain_concept: String,
version_id: String,
}
impl ArchetypeId {
#[must_use]
pub fn rm_originator(&self) -> &str {
&self.rm_originator
}
#[must_use]
pub fn rm_name(&self) -> &str {
&self.rm_name
}
#[must_use]
pub fn rm_entity(&self) -> &str {
&self.rm_entity
}
#[must_use]
pub fn domain_concept(&self) -> &str {
&self.domain_concept
}
#[must_use]
pub fn concept_name(&self) -> &str {
self.domain_concept
.split_once('-')
.map_or(self.domain_concept.as_str(), |(head, _)| head)
}
pub fn specialisations(&self) -> impl Iterator<Item = &str> {
let mut parts = self.domain_concept.split('-');
parts.next();
parts
}
#[must_use]
pub fn version_id(&self) -> &str {
&self.version_id
}
#[must_use]
pub fn major_version(&self) -> u32 {
self.version_id[1..]
.split('.')
.next()
.and_then(|n| n.parse().ok())
.expect("parser guarantees a numeric major version")
}
#[must_use]
pub fn constrains(&self, rm_class: &str) -> bool {
self.rm_entity == rm_class
}
}
impl fmt::Display for ArchetypeId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}-{}-{}.{}.{}",
self.rm_originator, self.rm_name, self.rm_entity, self.domain_concept, self.version_id
)
}
}
impl FromStr for ArchetypeId {
type Err = ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut sections = s.splitn(3, '.');
let (Some(qualified), Some(domain_concept), Some(version_id)) =
(sections.next(), sections.next(), sections.next())
else {
return Err(ParseError::new(
"ARCHETYPE_ID",
"expected rm_entity.domain_concept.version",
s,
));
};
let rm: Vec<&str> = qualified.split('-').collect();
let [rm_originator, rm_name, rm_entity] = rm.as_slice() else {
return Err(ParseError::new(
"ARCHETYPE_ID",
"expected rm_originator-rm_name-rm_entity",
s,
));
};
for part in [rm_originator, rm_name, rm_entity] {
if part.is_empty() || !part.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'_') {
return Err(ParseError::new(
"ARCHETYPE_ID",
"reference model name is empty or has an illegal character",
s,
));
}
}
if domain_concept.is_empty()
|| !domain_concept
.bytes()
.all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-')
{
return Err(ParseError::new(
"ARCHETYPE_ID",
"domain concept is empty or has an illegal character",
s,
));
}
if domain_concept.split('-').any(str::is_empty) {
return Err(ParseError::new(
"ARCHETYPE_ID",
"empty specialisation segment",
s,
));
}
let Some(numbers) = version_id.strip_prefix('v') else {
return Err(ParseError::new("ARCHETYPE_ID", "version lacks `v`", s));
};
let components: Vec<&str> = numbers.split('.').collect();
if components.is_empty() || components.len() > 3 {
return Err(ParseError::new(
"ARCHETYPE_ID",
"version has more than three components",
s,
));
}
for c in &components {
if c.is_empty() || !c.bytes().all(|b| b.is_ascii_digit()) {
return Err(ParseError::new(
"ARCHETYPE_ID",
"version component is not a number",
s,
));
}
if c.parse::<u32>().is_err() {
return Err(ParseError::new(
"ARCHETYPE_ID",
"version component does not fit in u32",
s,
));
}
}
Ok(Self {
rm_originator: (*rm_originator).to_owned(),
rm_name: (*rm_name).to_owned(),
rm_entity: (*rm_entity).to_owned(),
domain_concept: domain_concept.to_owned(),
version_id: version_id.to_owned(),
})
}
}
crate::impl_valued_serde!(ArchetypeId, "ARCHETYPE_ID");
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct TemplateId(String);
impl TemplateId {
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for TemplateId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl FromStr for TemplateId {
type Err = ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.is_empty() {
return Err(ParseError::new("TEMPLATE_ID", "empty", s));
}
if s.chars().any(char::is_whitespace) {
return Err(ParseError::new("TEMPLATE_ID", "contains whitespace", s));
}
Ok(Self(s.to_owned()))
}
}
crate::impl_valued_serde!(TemplateId, "TEMPLATE_ID");
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct TerminologyId {
name: String,
version_id: Option<String>,
}
impl TerminologyId {
#[must_use]
#[allow(non_snake_case)]
pub fn openehr() -> Self {
Self {
name: "openehr".to_owned(),
version_id: None,
}
}
#[must_use]
pub fn name(&self) -> &str {
&self.name
}
#[must_use]
pub fn version_id(&self) -> Option<&str> {
self.version_id.as_deref()
}
#[must_use]
pub fn is_openehr(&self) -> bool {
self.name.eq_ignore_ascii_case("openehr")
}
}
impl fmt::Display for TerminologyId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.version_id {
Some(v) => write!(f, "{}({v})", self.name),
None => f.write_str(&self.name),
}
}
}
impl FromStr for TerminologyId {
type Err = ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let (name, version_id) = match s.split_once('(') {
None => (s, None),
Some((name, rest)) => {
let Some(version) = rest.strip_suffix(')') else {
return Err(ParseError::new("TERMINOLOGY_ID", "unclosed `(`", s));
};
if version.is_empty() {
return Err(ParseError::new("TERMINOLOGY_ID", "empty version", s));
}
(name, Some(version.to_owned()))
}
};
if name.is_empty() {
return Err(ParseError::new("TERMINOLOGY_ID", "empty name", s));
}
Ok(Self {
name: name.to_owned(),
version_id,
})
}
}
crate::impl_valued_serde!(TerminologyId, "TERMINOLOGY_ID");
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub struct GenericId {
#[serde(rename = "_type", default = "generic_id_type")]
_type: GenericIdType,
value: String,
scheme: String,
}
fn generic_id_type() -> GenericIdType {
GenericIdType::GenericId
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
#[non_exhaustive]
pub enum GenericIdType {
#[serde(rename = "GENERIC_ID")]
GenericId,
}
impl GenericId {
pub fn new(value: impl Into<String>, scheme: impl Into<String>) -> Result<Self, ParseError> {
let value = value.into();
let scheme = scheme.into();
if value.is_empty() {
return Err(ParseError::new("GENERIC_ID", "empty value", &value));
}
if scheme.is_empty() {
return Err(ParseError::new("GENERIC_ID", "empty scheme", &scheme));
}
Ok(Self {
_type: GenericIdType::GenericId,
value,
scheme,
})
}
#[must_use]
pub fn value(&self) -> &str {
&self.value
}
#[must_use]
pub fn scheme(&self) -> &str {
&self.scheme
}
}
impl fmt::Display for GenericId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.value)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ObjectId {
HierObjectId(HierObjectId),
ObjectVersionId(ObjectVersionId),
ArchetypeId(ArchetypeId),
TemplateId(TemplateId),
TerminologyId(TerminologyId),
GenericId(GenericId),
}
impl ObjectId {
#[must_use]
pub fn value(&self) -> String {
self.to_string()
}
#[must_use]
pub fn type_name(&self) -> &'static str {
match self {
Self::HierObjectId(_) => "HIER_OBJECT_ID",
Self::ObjectVersionId(_) => "OBJECT_VERSION_ID",
Self::ArchetypeId(_) => "ARCHETYPE_ID",
Self::TemplateId(_) => "TEMPLATE_ID",
Self::TerminologyId(_) => "TERMINOLOGY_ID",
Self::GenericId(_) => "GENERIC_ID",
}
}
}
impl fmt::Display for ObjectId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::HierObjectId(v) => v.fmt(f),
Self::ObjectVersionId(v) => v.fmt(f),
Self::ArchetypeId(v) => v.fmt(f),
Self::TemplateId(v) => v.fmt(f),
Self::TerminologyId(v) => v.fmt(f),
Self::GenericId(v) => v.fmt(f),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum UidBasedId {
HierObjectId(HierObjectId),
ObjectVersionId(ObjectVersionId),
}
impl UidBasedId {
#[must_use]
pub fn root(&self) -> &Uid {
match self {
Self::HierObjectId(id) => id.root(),
Self::ObjectVersionId(id) => id.object_id(),
}
}
#[must_use]
pub fn extension(&self) -> Option<&str> {
match self {
Self::HierObjectId(id) => id.extension(),
Self::ObjectVersionId(_) => None,
}
}
#[must_use]
pub fn type_name(&self) -> &'static str {
match self {
Self::HierObjectId(_) => "HIER_OBJECT_ID",
Self::ObjectVersionId(_) => "OBJECT_VERSION_ID",
}
}
}
impl fmt::Display for UidBasedId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::HierObjectId(v) => v.fmt(f),
Self::ObjectVersionId(v) => v.fmt(f),
}
}
}
impl FromStr for UidBasedId {
type Err = ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.matches("::").count() == 2 {
return Ok(Self::ObjectVersionId(s.parse()?));
}
Ok(Self::HierObjectId(s.parse()?))
}
}
impl From<HierObjectId> for UidBasedId {
fn from(v: HierObjectId) -> Self {
Self::HierObjectId(v)
}
}
impl From<ObjectVersionId> for UidBasedId {
fn from(v: ObjectVersionId) -> Self {
Self::ObjectVersionId(v)
}
}
mod uid_based_id_serde {
use super::{HierObjectId, ObjectVersionId, UidBasedId};
use serde::de::{Error as _, MapAccess, Visitor};
use serde::ser::SerializeStruct as _;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
impl Serialize for UidBasedId {
fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
let mut st = s.serialize_struct("UID_BASED_ID", 2)?;
st.serialize_field("_type", self.type_name())?;
st.serialize_field("value", &self.to_string())?;
st.end()
}
}
impl<'de> Deserialize<'de> for UidBasedId {
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
struct V;
impl<'de> Visitor<'de> for V {
type Value = UidBasedId;
fn expecting(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str("a UID_BASED_ID as a string or a typed object")
}
fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<UidBasedId, E> {
v.parse().map_err(E::custom)
}
fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<UidBasedId, A::Error> {
let mut ty: Option<String> = None;
let mut value: Option<String> = None;
while let Some(key) = map.next_key::<String>()? {
match key.as_str() {
"_type" => ty = Some(map.next_value()?),
"value" => value = Some(map.next_value()?),
_ => {
map.next_value::<serde::de::IgnoredAny>()?;
}
}
}
let value = value.ok_or_else(|| A::Error::missing_field("value"))?;
match ty.as_deref() {
Some("HIER_OBJECT_ID") => Ok(UidBasedId::HierObjectId(
value.parse::<HierObjectId>().map_err(A::Error::custom)?,
)),
Some("OBJECT_VERSION_ID") => Ok(UidBasedId::ObjectVersionId(
value.parse::<ObjectVersionId>().map_err(A::Error::custom)?,
)),
Some(other) => Err(A::Error::custom(format!(
"_type is {other}, expected HIER_OBJECT_ID or OBJECT_VERSION_ID"
))),
None => value.parse().map_err(A::Error::custom),
}
}
}
d.deserialize_any(V)
}
}
}
mod object_id_serde {
use super::{
ArchetypeId, GenericId, HierObjectId, ObjectId, ObjectVersionId, TemplateId, TerminologyId,
UidBasedId,
};
use serde::de::{Error as _, MapAccess, Visitor};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
impl Serialize for ObjectId {
fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
match self {
ObjectId::HierObjectId(v) => v.serialize(s),
ObjectId::ObjectVersionId(v) => v.serialize(s),
ObjectId::ArchetypeId(v) => v.serialize(s),
ObjectId::TemplateId(v) => v.serialize(s),
ObjectId::TerminologyId(v) => v.serialize(s),
ObjectId::GenericId(v) => v.serialize(s),
}
}
}
impl<'de> Deserialize<'de> for ObjectId {
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
struct V;
impl<'de> Visitor<'de> for V {
type Value = ObjectId;
fn expecting(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str("an OBJECT_ID as a string or a typed object")
}
fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<ObjectId, E> {
infer(v).map_err(E::custom)
}
fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<ObjectId, A::Error> {
let mut ty: Option<String> = None;
let mut value: Option<String> = None;
let mut scheme: Option<String> = None;
while let Some(key) = map.next_key::<String>()? {
match key.as_str() {
"_type" => ty = Some(map.next_value()?),
"value" => value = Some(map.next_value()?),
"scheme" => scheme = Some(map.next_value()?),
_ => {
map.next_value::<serde::de::IgnoredAny>()?;
}
}
}
let value = value.ok_or_else(|| A::Error::missing_field("value"))?;
match ty.as_deref() {
Some("HIER_OBJECT_ID") => Ok(ObjectId::HierObjectId(
value.parse::<HierObjectId>().map_err(A::Error::custom)?,
)),
Some("OBJECT_VERSION_ID") => Ok(ObjectId::ObjectVersionId(
value.parse::<ObjectVersionId>().map_err(A::Error::custom)?,
)),
Some("ARCHETYPE_ID") => Ok(ObjectId::ArchetypeId(
value.parse::<ArchetypeId>().map_err(A::Error::custom)?,
)),
Some("TEMPLATE_ID") => Ok(ObjectId::TemplateId(
value.parse::<TemplateId>().map_err(A::Error::custom)?,
)),
Some("TERMINOLOGY_ID") => Ok(ObjectId::TerminologyId(
value.parse::<TerminologyId>().map_err(A::Error::custom)?,
)),
Some("GENERIC_ID") => {
let scheme = scheme.ok_or_else(|| A::Error::missing_field("scheme"))?;
Ok(ObjectId::GenericId(
GenericId::new(value, scheme).map_err(A::Error::custom)?,
))
}
Some(other) => Err(A::Error::custom(format!(
"{other} is not an OBJECT_ID class"
))),
None => infer(&value).map_err(A::Error::custom),
}
}
}
fn infer(value: &str) -> Result<ObjectId, crate::ParseError> {
Ok(match value.parse::<UidBasedId>()? {
UidBasedId::HierObjectId(v) => ObjectId::HierObjectId(v),
UidBasedId::ObjectVersionId(v) => ObjectId::ObjectVersionId(v),
})
}
d.deserialize_any(V)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn version_id_round_trips_exactly() {
for text in [
"87284370-2D4B-4E3D-A3F3-F303D2F4F34B::ehr1.nhs.uk::1",
"87284370-2d4b-4e3d-a3f3-f303d2f4f34b::2.16.840.1::12.3.7",
] {
assert_eq!(text.parse::<ObjectVersionId>().unwrap().to_string(), text);
}
}
#[test]
fn hier_object_id_rejects_a_double_colon_extension() {
assert!("2.16.840::b::c".parse::<HierObjectId>().is_err());
}
#[test]
fn uid_based_id_infers_by_separator_count() {
assert!(matches!(
"1.2.3::x".parse::<UidBasedId>().unwrap(),
UidBasedId::HierObjectId(_)
));
assert!(matches!(
"1.2.3::sys.example::4".parse::<UidBasedId>().unwrap(),
UidBasedId::ObjectVersionId(_)
));
}
#[test]
fn archetype_id_round_trips_and_splits() {
let text = "openEHR-EHR-OBSERVATION.blood_pressure-mine-yours.v1.2.3";
let id: ArchetypeId = text.parse().unwrap();
assert_eq!(id.to_string(), text);
assert_eq!(id.concept_name(), "blood_pressure");
assert_eq!(id.specialisations().collect::<Vec<_>>(), ["mine", "yours"]);
assert_eq!(id.major_version(), 1);
}
#[test]
fn archetype_id_rejects_malformed_forms() {
for text in [
"openEHR-EHR.blood_pressure.v1", "openEHR-EHR-OBSERVATION.blood_pressure", "openEHR-EHR-OBSERVATION.blood_pressure.1", "openEHR-EHR-OBSERVATION.blood_pressure.v", "openEHR-EHR-OBSERVATION..v1", "openEHR-EHR-OBSERVATION.a-.v1", "openEHR-EHR-OBSERVATION.a.v1.2.3.4", ] {
assert!(text.parse::<ArchetypeId>().is_err(), "accepted {text}");
}
}
#[test]
fn version_tree_next_advances_the_right_counter() {
let trunk: VersionTreeId = "3".parse().unwrap();
assert_eq!(trunk.next().to_string(), "4");
let branch: VersionTreeId = "3.2.9".parse().unwrap();
assert_eq!(branch.next().to_string(), "3.2.10");
}
#[test]
fn every_number_in_a_version_tree_id_is_read_and_none_may_be_zero() {
let v = VersionTreeId::branch(1, 2, 3).expect("1.2.3 is a branch");
assert_eq!(v.trunk_version(), 1);
assert_eq!(v.branch_number(), Some(2));
assert_eq!(v.branch_version(), Some(3));
assert!(v.is_branch());
assert_eq!(v.to_string(), "1.2.3");
let v = VersionTreeId::branch(7, 8, 9).unwrap();
assert_eq!(
(v.trunk_version(), v.branch_number(), v.branch_version()),
(7, Some(8), Some(9))
);
for (t, n, ver) in [(0, 2, 3), (1, 0, 3), (1, 2, 0), (0, 0, 0)] {
assert!(
VersionTreeId::branch(t, n, ver).is_err(),
"{t}.{n}.{ver} was accepted"
);
}
let trunk: VersionTreeId = "5".parse().unwrap();
assert_eq!(trunk.trunk_version(), 5);
assert_eq!(trunk.branch_number(), None);
assert_eq!(trunk.branch_version(), None);
assert!(!trunk.is_branch());
}
#[test]
fn a_version_tree_id_refuses_what_would_not_round_trip() {
for text in ["1", "1.2.3", "10", "1.20.300"] {
let v: VersionTreeId = text.parse().unwrap_or_else(|e| panic!("{text}: {e}"));
assert_eq!(v.to_string(), text, "{text} did not round-trip");
}
for bad in [
"1.x.3", "1..3", "", "01", "1.02.3", "1.2.03",
"0", "1.2", "1.2.3.4",
"-1",
] {
assert!(
bad.parse::<VersionTreeId>().is_err(),
"{bad} was accepted as a version tree id"
);
}
}
#[test]
fn an_archetype_id_is_read_in_full_and_refuses_each_malformation() {
let id: ArchetypeId = "openEHR-EHR-OBSERVATION.blood_pressure.v2".parse().unwrap();
assert_eq!(id.rm_originator(), "openEHR");
assert_eq!(id.rm_name(), "EHR");
assert_eq!(id.rm_entity(), "OBSERVATION");
assert_eq!(id.domain_concept(), "blood_pressure");
assert_eq!(id.concept_name(), "blood_pressure");
assert_eq!(id.to_string(), "openEHR-EHR-OBSERVATION.blood_pressure.v2");
let id: ArchetypeId = "openEHR-EHR-COMPOSITION.progress_note-naturopathy.v1"
.parse()
.unwrap();
assert_eq!(id.domain_concept(), "progress_note-naturopathy");
assert_eq!(id.concept_name(), "progress_note");
for bad in [
"-EHR-OBSERVATION.x.v1", "openEHR--OBSERVATION.x.v1", "openEHR-EHR-.x.v1", "openEHR-EHR-OBS!.x.v1", "openEHR-EHR-OBSERVATION..v1", "openEHR-EHR-OBSERVATION.x!.v1", "openEHR-EHR-OBSERVATION.x-.v1", "openEHR-EHR-OBSERVATION.-x.v1",
"openEHR-EHR-OBSERVATION.x.1", "openEHR-EHR-OBSERVATION.x.vx", "openEHR-EHR-OBSERVATION.x.v", "openEHR-EHR-OBSERVATION.x.v1.2.3.4",
] {
assert!(
bad.parse::<ArchetypeId>().is_err(),
"{bad} was accepted as an archetype id"
);
}
}
#[test]
fn every_object_id_class_names_itself_and_round_trips_through_json() {
let cases: Vec<(ObjectId, &str, &str)> = vec![
(
ObjectId::HierObjectId("6BA7B810-9DAD-11D1-80B4-00C04FD430C8".parse().unwrap()),
"HIER_OBJECT_ID",
"6BA7B810-9DAD-11D1-80B4-00C04FD430C8",
),
(
ObjectId::ObjectVersionId(
"6BA7B810-9DAD-11D1-80B4-00C04FD430C8::example.org::1"
.parse()
.unwrap(),
),
"OBJECT_VERSION_ID",
"6BA7B810-9DAD-11D1-80B4-00C04FD430C8::example.org::1",
),
(
ObjectId::ArchetypeId("openEHR-EHR-OBSERVATION.blood_pressure.v2".parse().unwrap()),
"ARCHETYPE_ID",
"openEHR-EHR-OBSERVATION.blood_pressure.v2",
),
(
ObjectId::TemplateId("vital_signs.v1".parse().unwrap()),
"TEMPLATE_ID",
"vital_signs.v1",
),
(
ObjectId::TerminologyId("SNOMED-CT".parse().unwrap()),
"TERMINOLOGY_ID",
"SNOMED-CT",
),
(
ObjectId::GenericId(GenericId::new("12345", "NHS").unwrap()),
"GENERIC_ID",
"12345",
),
];
let mut seen: Vec<&str> = Vec::new();
for (id, type_name, text) in &cases {
assert_eq!(id.type_name(), *type_name);
assert_eq!(id.to_string(), *text, "{type_name} printed the wrong value");
assert_eq!(id.value(), *text);
seen.push(type_name);
let json = serde_json::to_string(id).expect("serialize");
assert!(json.contains(type_name), "{type_name} missing from {json}");
let back: ObjectId = serde_json::from_str(&json).expect(&json);
assert_eq!(&back, id, "{type_name} did not round-trip: {json}");
}
seen.sort_unstable();
seen.dedup();
assert_eq!(seen.len(), cases.len(), "two classes share a `_type`");
assert!(
serde_json::from_str::<ObjectId>(r#"{"_type":"GENERIC_ID","value":"12345"}"#).is_err(),
"a GENERIC_ID with no scheme was accepted"
);
assert!(
serde_json::from_str::<ObjectId>(r#"{"_type":"XYZZY","value":"12345"}"#).is_err()
);
}
#[test]
fn only_a_hier_object_id_carries_an_extension() {
let plain: UidBasedId = "6BA7B810-9DAD-11D1-80B4-00C04FD430C8".parse().unwrap();
assert_eq!(plain.extension(), None);
let with_ext: UidBasedId = "6BA7B810-9DAD-11D1-80B4-00C04FD430C8::extension"
.parse()
.unwrap();
assert_eq!(with_ext.extension(), Some("extension"));
let version: UidBasedId = "6BA7B810-9DAD-11D1-80B4-00C04FD430C8::example.org::1"
.parse()
.unwrap();
assert_eq!(version.extension(), None, "an OBJECT_VERSION_ID has none");
}
#[test]
fn two_version_ids_name_the_same_object_only_when_their_object_ids_agree() {
let id = |s: &str| s.parse::<ObjectVersionId>().unwrap();
let a = id("87284370-2D4B-4E3D-A3F3-F303D2F4F34B::a.example::1");
assert!(a.same_object_as(&id("87284370-2D4B-4E3D-A3F3-F303D2F4F34B::b.example::2")));
assert!(a.same_object_as(&a));
assert!(
!a.same_object_as(&id("6BA7B810-9DAD-11D1-80B4-00C04FD430C8::a.example::1")),
"two unrelated records were called versions of one"
);
}
#[test]
fn each_malformed_version_tree_id_is_refused_for_its_own_reason() {
let reason = |text: &str| {
text.parse::<VersionTreeId>()
.expect_err(text)
.reason
};
assert_eq!(reason("1.x.3"), "not a number");
assert_eq!(reason("1..3"), "not a number", "an empty part is not a number");
assert_eq!(reason("01"), "leading zero would not round-trip");
assert_eq!(reason("1.02.3"), "leading zero would not round-trip");
assert_eq!(reason("0"), "trunk_version is 0");
assert_eq!(reason("1.2"), "expected `trunk` or `trunk.branch_number.branch_version`");
assert_eq!(reason("4294967296"), "number does not fit in u32");
}
}