Skip to main content

rustev_contract/
version.rs

1//! Numeric versions and comparator sets (spec 034, 3.6.1 and 3.6.2).
2use std::{cmp::Ordering, fmt, str::FromStr};
3
4/// Decimal components are kept as digits: the grammar has no machine-word
5/// ceiling. Length followed by lexical comparison gives numeric ordering.
6#[derive(Debug, Clone, PartialEq, Eq, Hash)]
7pub struct Version([String; 3]);
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub enum VersionError {
10    InvalidVersion,
11    InvalidRange,
12    ComparatorCount,
13}
14impl fmt::Display for VersionError {
15    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
16        f.write_str(match self {
17            Self::InvalidVersion => "expected MAJOR.MINOR.PATCH without leading zeros or suffixes",
18            Self::InvalidRange => "expected comparators separated by single spaces",
19            Self::ComparatorCount => "expected one to four comparators",
20        })
21    }
22}
23impl std::error::Error for VersionError {}
24impl FromStr for Version {
25    type Err = VersionError;
26    fn from_str(s: &str) -> Result<Self, Self::Err> {
27        let parts: Vec<_> = s.split('.').collect();
28        if parts.len() != 3
29            || parts.iter().any(|p| {
30                p.is_empty()
31                    || !p.bytes().all(|b| b.is_ascii_digit())
32                    || (p.len() > 1 && p.starts_with('0'))
33            })
34        {
35            return Err(VersionError::InvalidVersion);
36        }
37        Ok(Self([parts[0].into(), parts[1].into(), parts[2].into()]))
38    }
39}
40impl Ord for Version {
41    fn cmp(&self, other: &Self) -> Ordering {
42        self.0
43            .iter()
44            .zip(&other.0)
45            .map(|(a, b)| a.len().cmp(&b.len()).then_with(|| a.cmp(b)))
46            .find(|c| *c != Ordering::Equal)
47            .unwrap_or(Ordering::Equal)
48    }
49}
50impl PartialOrd for Version {
51    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
52        Some(self.cmp(other))
53    }
54}
55impl fmt::Display for Version {
56    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57        write!(f, "{}.{}.{}", self.0[0], self.0[1], self.0[2])
58    }
59}
60
61/// A conjunction, retaining its original spelling for display (3.6.2).
62#[derive(Debug, Clone, PartialEq, Eq)]
63pub struct VersionRange {
64    text: String,
65    comparators: Vec<(Comparison, Version)>,
66}
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68enum Comparison {
69    Ge,
70    Gt,
71    Le,
72    Lt,
73    Eq,
74}
75impl FromStr for VersionRange {
76    type Err = VersionError;
77    fn from_str(s: &str) -> Result<Self, Self::Err> {
78        let parts: Vec<_> = s.split(' ').collect();
79        if parts.iter().any(|p| p.is_empty()) {
80            return Err(VersionError::InvalidRange);
81        }
82        if parts.len() > 4 {
83            return Err(VersionError::ComparatorCount);
84        }
85        let mut comparators = Vec::new();
86        for p in parts {
87            let (op, v) = [
88                (">=", Comparison::Ge),
89                ("<=", Comparison::Le),
90                (">", Comparison::Gt),
91                ("<", Comparison::Lt),
92                ("=", Comparison::Eq),
93            ]
94            .into_iter()
95            .find_map(|(prefix, op)| p.strip_prefix(prefix).map(|v| (op, v)))
96            .unwrap_or((Comparison::Eq, p));
97            comparators.push((op, v.parse()?));
98        }
99        Ok(Self {
100            text: s.into(),
101            comparators,
102        })
103    }
104}
105impl VersionRange {
106    pub fn satisfies(&self, version: &Version) -> bool {
107        self.comparators.iter().all(|(op, v)| match op {
108            Comparison::Ge => version >= v,
109            Comparison::Gt => version > v,
110            Comparison::Le => version <= v,
111            Comparison::Lt => version < v,
112            Comparison::Eq => version == v,
113        })
114    }
115}
116impl fmt::Display for VersionRange {
117    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118        f.write_str(&self.text)
119    }
120}