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");
}
}