use std::cmp::Ordering;
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct Canonical<'a> {
url: &'a str,
version: Option<&'a str>,
fragment: Option<&'a str>,
}
impl<'a> Canonical<'a> {
#[must_use]
pub fn parse(text: &'a str) -> Self {
let (rest, fragment) = match text.split_once('#') {
Some((rest, fragment)) => (rest, Some(fragment).filter(|f| !f.is_empty())),
None => (text, None),
};
let (url, version) = match rest.split_once('|') {
Some((url, version)) => (url, Some(version).filter(|v| !v.is_empty())),
None => (rest, None),
};
Self {
url,
version,
fragment,
}
}
#[must_use]
pub const fn url(&self) -> &'a str {
self.url
}
#[must_use]
pub const fn version(&self) -> Option<&'a str> {
self.version
}
#[must_use]
pub const fn fragment(&self) -> Option<&'a str> {
self.fragment
}
}
impl fmt::Display for Canonical<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.url)?;
if let Some(version) = self.version {
write!(f, "|{version}")?;
}
if let Some(fragment) = self.fragment {
write!(f, "#{fragment}")?;
}
Ok(())
}
}
#[must_use]
pub fn compare_versions(left: &str, right: &str) -> Ordering {
let (left_release, left_pre) = split_version(left);
let (right_release, right_pre) = split_version(right);
compare_identifiers(left_release, right_release).then_with(|| match (left_pre, right_pre) {
(None, None) => Ordering::Equal,
(None, Some(_)) => Ordering::Greater,
(Some(_), None) => Ordering::Less,
(Some(left), Some(right)) => compare_identifiers(left, right),
})
}
fn split_version(version: &str) -> (&str, Option<&str>) {
let version = version.split_once('+').map_or(version, |(core, _)| core);
match version.split_once('-') {
Some((release, pre)) => (release, Some(pre)),
None => (version, None),
}
}
fn compare_identifiers(left: &str, right: &str) -> Ordering {
let mut left = left.split('.');
let mut right = right.split('.');
loop {
match (left.next(), right.next()) {
(None, None) => return Ordering::Equal,
(None, Some(_)) => return Ordering::Less,
(Some(_), None) => return Ordering::Greater,
(Some(l), Some(r)) => {
let order = match (l.parse::<u64>(), r.parse::<u64>()) {
(Ok(l), Ok(r)) => l.cmp(&r),
(Ok(_), Err(_)) => Ordering::Less,
(Err(_), Ok(_)) => Ordering::Greater,
(Err(_), Err(_)) => l.cmp(r),
};
if order != Ordering::Equal {
return order;
}
}
}
}
}
#[cfg(test)]
mod tests {
use std::cmp::Ordering;
use super::{Canonical, compare_versions};
#[test]
fn a_canonical_splits_into_url_version_and_fragment() {
let canonical = Canonical::parse("http://example.org/Questionnaire/q|1.0#vs1");
assert_eq!(canonical.url(), "http://example.org/Questionnaire/q");
assert_eq!(canonical.version(), Some("1.0"));
assert_eq!(canonical.fragment(), Some("vs1"));
}
#[test]
fn a_bare_canonical_has_no_version() {
let canonical = Canonical::parse("http://example.org/StructureDefinition/p");
assert_eq!(canonical.version(), None);
assert_eq!(canonical.fragment(), None);
}
#[test]
fn versions_order_by_semver_precedence() {
assert_eq!(compare_versions("2.0.10", "2.0.9"), Ordering::Greater);
assert_eq!(compare_versions("1.0.0-ballot", "1.0.0"), Ordering::Less);
assert_eq!(
compare_versions("1.0.0-alpha.2", "1.0.0-alpha.10"),
Ordering::Less
);
assert_eq!(compare_versions("1.0.0+build", "1.0.0"), Ordering::Equal);
assert_eq!(compare_versions("4.0", "4.0.1"), Ordering::Less);
}
}