use crate::{Error, Result};
#[deprecated(since = "2.1.0", note = "use version::format_versions(); removed in 3.0")]
pub const FORMAT_VERSIONS: [&str; 2] = ["1.0", "1.1"];
pub fn format_versions() -> &'static [&'static str] {
&["1.0", "1.1", "1.2"]
}
pub const BASE_VERSION: &str = "1.0";
pub const LATEST_VERSION: &str = "1.2";
#[cfg(test)]
thread_local! {
static READ_AS: std::cell::Cell<Option<&'static str>> = const { std::cell::Cell::new(None) };
}
pub(crate) fn implemented() -> &'static str {
#[cfg(test)]
if let Some(version) = READ_AS.with(std::cell::Cell::get) {
return version;
}
LATEST_VERSION
}
#[cfg(test)]
pub(crate) fn read_as<T>(version: &'static str, f: impl FnOnce() -> T) -> T {
let before = READ_AS.with(|c| c.replace(Some(version)));
let out = f();
READ_AS.with(|c| c.set(before));
out
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Support {
Full,
Projection,
Unsupported,
}
pub fn parse(version: &str) -> Option<(u32, u32)> {
let number = |text: &str| if text.bytes().all(|b| b.is_ascii_digit()) { text.parse().ok() } else { None };
let (major, minor) = version.split_once('.').unwrap_or((version, "0"));
Some((number(major)?, number(minor)?))
}
pub fn support(version: &str) -> Support {
let latest = implemented();
if version.split('.').next() != latest.split('.').next() {
return Support::Unsupported;
}
match (parse(version), parse(latest)) {
(Some((_, minor)), Some((_, latest))) if minor <= latest => Support::Full,
_ => Support::Projection,
}
}
pub fn next_minor(version: &str) -> Option<String> {
let (major, minor) = parse(version)?;
Some(format!("{major}.{}", minor.checked_add(1)?))
}
pub fn is_projection(version: &str) -> bool {
support(version) == Support::Projection
}
pub fn at_least(version: &str, floor: &str) -> bool {
match (parse(version), parse(floor)) {
(Some(a), Some(b)) => a >= b,
_ => false,
}
}
pub fn later<'a>(a: &'a str, b: &'a str) -> &'a str {
match (parse(a), parse(b)) {
(Some(x), Some(y)) if y > x => b,
_ => a,
}
}
pub fn required_for<S: AsRef<str>>(profiles: &[S]) -> &'static str {
if profiles.iter().any(|p| p.as_ref() == crate::clinical::PROFILE) {
crate::clinical::MIN_VERSION
} else {
BASE_VERSION
}
}
pub fn required_for_content<S: AsRef<str>, K: AsRef<str>>(profiles: &[S], kinds: &[K]) -> &'static str {
kinds
.iter()
.filter_map(|k| crate::annotations::header::kind_since(k.as_ref()))
.fold(required_for(profiles), |needed, since| later(needed, since))
}
pub fn require_amendable(version: &str, profiles: &[String], what: &str) -> Result<()> {
match support(version) {
Support::Full => {}
Support::Projection => {
return Err(Error::Version(format!(
"{what} is MEDH5 {version}; this engine implements up to {LATEST_VERSION} and can read it only as a \
projection --- amending it would drop or misattest what a later minor defines, so it is refused \
(1.1 §2.3)"
)))
}
Support::Unsupported => {
return Err(Error::Version(format!("{what} is MEDH5 {version}; this engine implements {LATEST_VERSION}")))
}
}
let mut unknown: Vec<&String> =
profiles.iter().filter(|p| !crate::sample::PROFILES.contains(&p.as_str())).collect();
unknown.sort();
unknown.dedup();
if !unknown.is_empty() {
return Err(Error::coded(
"E007",
format!(
"{what} declares profile(s) {} this engine does not implement; amending it could break their \
requirements unseen, so it is refused (1.1 §2.3)",
crate::json::repr_list(&unknown)
),
));
}
Ok(())
}
#[cfg(test)]
pub(crate) fn later_minor() -> String {
next_minor(LATEST_VERSION).expect("LATEST_VERSION parses")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn s2_2_the_later_minor_is_one_past_the_newest() {
assert_eq!(next_minor("1.1").as_deref(), Some("1.2"));
assert_eq!(later_minor(), "1.3");
assert_eq!(next_minor("1.9").as_deref(), Some("1.10"));
assert_eq!(next_minor("1").as_deref(), Some("1.1"));
assert_eq!(next_minor("1.x"), None);
assert_eq!(format_versions().last(), Some(&LATEST_VERSION));
assert_eq!(support(&later_minor()), Support::Projection);
assert!(format_versions().iter().all(|v| support(v) == Support::Full));
#[allow(deprecated)]
let array = FORMAT_VERSIONS;
assert_eq!(&array[..], &format_versions()[..2]);
}
#[test]
fn s2_2_versions_are_full_projection_or_unsupported() {
assert_eq!(support("1.0"), Support::Full);
assert_eq!(support("1.1"), Support::Full);
assert_eq!(support("1.2"), Support::Full);
assert_eq!(support("1"), Support::Full);
assert_eq!(support(&later_minor()), Support::Projection);
assert_eq!(support("1.10"), Support::Projection);
assert_eq!(support("1.x"), Support::Projection);
assert_eq!(support("2.0"), Support::Unsupported);
assert_eq!(support(""), Support::Unsupported);
assert!(at_least("1.1", "1.1") && at_least(&later_minor(), "1.1") && !at_least("1.0", "1.1"));
assert_eq!(later("1.0", "1.1"), "1.1");
assert_eq!(later("1.1", "1.0"), "1.1");
assert_eq!(required_for(&["core", "seg"]), "1.0");
assert_eq!(required_for(&["core", "clinical"]), "1.1");
}
#[test]
fn v1_2_s2_1_the_version_follows_profiles_and_kinds() {
let none: [&str; 0] = [];
assert_eq!(required_for_content(&["core"], &none), "1.0");
assert_eq!(required_for_content(&["core"], &["labelmap", "contours", "points"]), "1.0");
assert_eq!(required_for_content(&["core", "clinical"], &["mesh"]), "1.1");
assert_eq!(required_for_content(&["core"], &["boxes", "polylines"]), "1.2");
assert_eq!(required_for_content(&["core", "clinical"], &["polylines"]), "1.2");
assert_eq!(required_for_content(&["core"], &["spline"]), "1.0");
}
#[test]
fn s2_1_whitespace_is_no_part_of_a_version() {
for refused in [" 1.0", "01.0", "+1.0", "1 .0", "\t1.1"] {
assert_eq!(support(refused), Support::Unsupported, "{refused:?}");
}
for unknown in ["1.0 ", "1. 0", "1.+1", "1.0\n"] {
assert_eq!(support(unknown), Support::Projection, "{unknown:?}");
}
for unparsed in [" 1.0", "+1.0", "1 .0", "1.0 ", "1. 0", "1.+1", "", "1."] {
assert_eq!(parse(unparsed), None, "{unparsed:?}");
}
assert_eq!(parse("1.10"), Some((1, 10)));
assert_eq!(parse("1"), Some((1, 0)));
}
#[test]
fn s2_3_a_higher_minor_or_unknown_profile_is_not_amendable() {
assert!(require_amendable("1.1", &["core".into(), "clinical".into()], "x").is_ok());
assert!(matches!(require_amendable(&later_minor(), &["core".into()], "x"), Err(Error::Version(_))));
let err = require_amendable("1.0", &["core".into(), "quantum".into()], "x").unwrap_err();
assert_eq!(err.code(), Some("E007"));
}
}