use crate::dictionary::Dictionary;
use std::sync::{Arc, OnceLock};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[allow(non_camel_case_types)]
pub enum Version {
V2_1,
V2_2,
V2_3,
V2_3_1,
V2_4,
V2_5,
V2_5_1,
V2_6,
V2_7,
V2_7_1,
V2_8,
V2_8_1,
V2_8_2,
V2_9,
}
use Version::{
V2_1, V2_2, V2_3, V2_3_1, V2_4, V2_5, V2_5_1, V2_6, V2_7, V2_7_1, V2_8, V2_8_1, V2_8_2, V2_9,
};
pub const ALL: &[Version] = &[
V2_1, V2_2, V2_3, V2_3_1, V2_4, V2_5, V2_5_1, V2_6, V2_7, V2_7_1, V2_8, V2_8_1, V2_8_2, V2_9,
];
pub const DEFAULT: Version = V2_5;
const FILES: &[(&str, &str)] = &[
("2.1", include_str!("../schemas/v2.1.json")),
("2.2", include_str!("../schemas/v2.2.json")),
("2.3", include_str!("../schemas/v2.3.json")),
("2.3.1", include_str!("../schemas/v2.3.1.json")),
("2.4", include_str!("../schemas/v2.4.json")),
("2.5", include_str!("../schemas/v2.5.json")),
("2.5.1", include_str!("../schemas/v2.5.1.json")),
("2.6", include_str!("../schemas/v2.6.json")),
("2.7", include_str!("../schemas/v2.7.json")),
("2.8", include_str!("../schemas/v2.8.json")),
("2.9", include_str!("../schemas/v2.9.json")),
];
static LOADED: [OnceLock<Arc<Dictionary>>; FILES.len()] = [const { OnceLock::new() }; FILES.len()];
impl Version {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
V2_1 => "2.1",
V2_2 => "2.2",
V2_3 => "2.3",
V2_3_1 => "2.3.1",
V2_4 => "2.4",
V2_5 => "2.5",
V2_5_1 => "2.5.1",
V2_6 => "2.6",
V2_7 => "2.7",
V2_7_1 => "2.7.1",
V2_8 => "2.8",
V2_8_1 => "2.8.1",
V2_8_2 => "2.8.2",
V2_9 => "2.9",
}
}
#[must_use]
pub fn parse(text: &str) -> Option<Version> {
let text = text.trim();
ALL.iter().copied().find(|v| v.as_str() == text)
}
#[must_use]
pub fn nearest(text: &str) -> Option<Version> {
if let Some(version) = Version::parse(text) {
return Some(version);
}
let wanted = numeric(text)?;
ALL.iter()
.copied()
.rfind(|v| numeric(v.as_str()).is_some_and(|known| known <= wanted))
}
#[must_use]
pub fn from_message(message: &er7::Message) -> Option<Version> {
Version::nearest(&message.version()?)
}
pub fn dictionary(self) -> Arc<Dictionary> {
let index = self.file_index();
LOADED[index]
.get_or_init(|| {
let (name, text) = FILES[index];
let dictionary =
Dictionary::from_json_resolving(text, format!("v{name}"), |base| {
Version::parse(base).map(Version::dictionary)
})
.unwrap_or_else(|error| {
panic!("bundled dictionary v{name} is invalid: {error}")
});
Arc::new(dictionary)
})
.clone()
}
fn file_index(self) -> usize {
let name = match self {
V2_7_1 => "2.7",
V2_8_1 | V2_8_2 => "2.8",
other => other.as_str(),
};
FILES
.iter()
.position(|(file, _)| *file == name)
.expect("every release maps to a bundled file")
}
}
impl Default for Version {
fn default() -> Version {
DEFAULT
}
}
impl std::fmt::Display for Version {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl std::str::FromStr for Version {
type Err = UnknownVersion;
fn from_str(text: &str) -> Result<Version, UnknownVersion> {
Version::parse(text).ok_or_else(|| UnknownVersion(text.to_string()))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UnknownVersion(pub String);
impl std::fmt::Display for UnknownVersion {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"unknown HL7 version {:?}; known versions are {}",
self.0,
ALL.iter()
.map(|v| v.as_str())
.collect::<Vec<&str>>()
.join(", ")
)
}
}
impl std::error::Error for UnknownVersion {}
fn numeric(text: &str) -> Option<[u32; 3]> {
let mut parts = [0u32; 3];
let mut any = false;
for (slot, part) in parts.iter_mut().zip(text.trim().split('.')) {
let digits: String = part.chars().take_while(char::is_ascii_digit).collect();
if digits.is_empty() {
break;
}
*slot = digits.parse().ok()?;
any = true;
}
any.then_some(parts)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn round_trips_every_release_string() {
for &version in ALL {
assert_eq!(Version::parse(version.as_str()), Some(version));
assert_eq!(version.as_str().parse::<Version>(), Ok(version));
}
assert_eq!(Version::parse(" 2.5.1 "), Some(V2_5_1));
assert!("2.5.2".parse::<Version>().is_err());
}
#[test]
fn falls_back_to_the_nearest_older_release() {
assert_eq!(Version::nearest("2.5.2"), Some(V2_5_1));
assert_eq!(Version::nearest("2.4.1"), Some(V2_4));
assert_eq!(Version::nearest("3.0"), Some(V2_9));
assert_eq!(Version::nearest("2.0"), None);
assert_eq!(Version::nearest("HL7"), None);
assert_eq!(Version::nearest(""), None);
}
#[test]
fn reads_the_release_out_of_msh_12() {
let message = er7::parse("MSH|^~\\&|A||||1||ACK|1|P|2.3.1\rMSA|AA|1").unwrap();
assert_eq!(Version::from_message(&message), Some(V2_3_1));
let message = er7::parse("MSH|^~\\&|A||||1||ACK|1|P|\rMSA|AA|1").unwrap();
assert_eq!(Version::from_message(&message), None);
}
#[test]
fn bundled_dictionaries_all_load() {
for &version in ALL {
let dictionary = version.dictionary();
assert!(
dictionary.segment_fields("MSH").is_some(),
"v{version} has no MSH"
);
}
}
}