Skip to main content

rer_version/
version.rs

1use core::cmp::Ordering;
2use lazy_static::lazy_static;
3use rand::{distributions::Alphanumeric, Rng};
4use regex::Regex;
5use std::fmt;
6use std::rc::Rc;
7
8lazy_static! {
9    static ref ALPHABET_REGEX: Regex =
10        Regex::new(r"[a-zA-Z0-9_]+").expect("Can't compile ALPHABET_REGEX regex");
11    static ref SEMVER_REGEX: Regex = Regex::new(
12        r"^(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)(?:-(?P<prerelease>(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P<buildmetadata>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$"
13    ).expect("Can't compile SEMVER_REGEX regex");
14    static ref NUMERIC_REGEX: Regex = Regex::new(r"[0-9]+").expect("Can't compile NUMERIC_REGEX regex");
15}
16
17#[derive(Debug, PartialEq, Eq, Clone, Hash)]
18struct SubToken {
19    s: String,
20    n: Option<i64>,
21}
22
23impl SubToken {
24    fn new(s: &str) -> Self {
25        let n = s.parse::<i64>().ok();
26        SubToken {
27            s: s.to_string(),
28            n,
29        }
30    }
31
32    fn custom_char_order(&self, c: char) -> u8 {
33        match c {
34            '_' => 0,
35            'a'..='z' => 1 + (c as u8 - b'a'),
36            'A'..='Z' => 27 + (c as u8 - b'A'),
37            '0'..='9' => 53 + (c as u8 - b'0'),
38            _ => 255, // Other characters are considered the largest
39        }
40    }
41    fn compare_subtokens(&self, a: &str, b: &str) -> Ordering {
42        a.chars()
43            .zip(b.chars())
44            .map(|(ac, bc)| self.custom_char_order(ac).cmp(&self.custom_char_order(bc)))
45            .find(|&ordering| ordering != Ordering::Equal)
46            .unwrap_or_else(|| a.len().cmp(&b.len()))
47    }
48}
49
50#[test]
51fn test_subtoken_new() {
52    let a = SubToken::new("1");
53    assert_eq!(a.s, "1");
54    assert_eq!(a.n, Some(1));
55    let a = SubToken::new("a");
56    assert_eq!(a.s, "a");
57    assert_eq!(a.n, None);
58    let a = SubToken::new("a1");
59    assert_eq!(a.s, "a1");
60}
61
62#[test]
63fn test_subtoken_numeric_ordering() {
64    // Numeric subtokens compare numerically, not lexically.
65    assert!(SubToken::new("2") < SubToken::new("10"));
66    assert!(SubToken::new("9") < SubToken::new("100"));
67    assert!(SubToken::new("122") > SubToken::new("34"));
68    // Zero-padding tie-break: equal value, shorter/padded string compares lower.
69    assert!(SubToken::new("01") < SubToken::new("1"));
70    // Ord and PartialOrd must agree.
71    assert_eq!(SubToken::new("2").cmp(&SubToken::new("10")), Ordering::Less);
72    assert_eq!(
73        SubToken::new("2").partial_cmp(&SubToken::new("10")),
74        Some(Ordering::Less)
75    );
76}
77
78#[test]
79fn test_version_numeric_ordering() {
80    // Multi-token versions must order numerically token-by-token.
81    let a: RerVersion = "2.34.1".try_into().unwrap();
82    let b: RerVersion = "2.122.2".try_into().unwrap();
83    assert!(a < b, "2.34.1 should sort below 2.122.2");
84    let a: RerVersion = "1.9.0".try_into().unwrap();
85    let b: RerVersion = "1.10.0".try_into().unwrap();
86    assert!(a < b, "1.9.0 should sort below 1.10.0");
87}
88
89impl Ord for SubToken {
90    fn cmp(&self, other: &Self) -> Ordering {
91        // Mirrors rez's `_SubToken.__lt__` (rez/src/rez/version/_version.py:145):
92        match (self.n, other.n) {
93            // Two alpha subtokens: custom char ordering ('_' < a-z < A-Z).
94            (None, None) => self.compare_subtokens(&self.s, &other.s),
95            // Alphas always sort before numbers.
96            (None, Some(_)) => Ordering::Less,
97            (Some(_), None) => Ordering::Greater,
98            // Two numeric subtokens: compare numerically, then break ties on the
99            // raw string so that e.g. "01" < "1" (padding-sensitive, per rez).
100            (Some(a), Some(b)) => a
101                .cmp(&b)
102                .then_with(|| self.compare_subtokens(&self.s, &other.s)),
103        }
104    }
105}
106
107impl PartialOrd for SubToken {
108    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
109        Some(self.cmp(other))
110    }
111}
112
113impl fmt::Display for SubToken {
114    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
115        write!(f, "{}", self.s)
116    }
117}
118
119#[derive(Debug, PartialEq, Eq, Clone, PartialOrd, Ord, Hash)]
120struct AlphanumericVersionToken {
121    subtokens: Vec<SubToken>,
122}
123
124impl AlphanumericVersionToken {
125    fn new(token: &str) -> Result<Self, &'static str> {
126        if !ALPHABET_REGEX.is_match(token) {
127            Err("Invalid version token")
128        } else {
129            Ok(Self {
130                subtokens: Self::parse(token),
131            })
132        }
133    }
134    // Testing purposes only
135    #[allow(dead_code)] // Should be use for test
136    fn create_random_token_string() -> Result<Self, &'static str> {
137        let s: String = rand::thread_rng()
138            .sample_iter(&Alphanumeric)
139            .take(7)
140            .map(char::from)
141            .collect();
142        Self::new(&s)
143    }
144
145    fn parse(s: &str) -> Vec<SubToken> {
146        let mut subtokens = Vec::new();
147        let mut alphas = NUMERIC_REGEX.split(s).peekable();
148        let mut numerics = NUMERIC_REGEX.find_iter(s).peekable();
149
150        while alphas.peek().is_some() || numerics.peek().is_some() {
151            if let Some(alpha) = alphas.next() {
152                if !alpha.is_empty() {
153                    subtokens.push(SubToken::new(alpha));
154                }
155            }
156            if let Some(numeric) = numerics.next() {
157                subtokens.push(SubToken::new(numeric.as_str()));
158            }
159        }
160
161        subtokens
162    }
163}
164
165#[test]
166fn test_generate_random_version() {
167    AlphanumericVersionToken::create_random_token_string().unwrap();
168}
169
170impl fmt::Display for AlphanumericVersionToken {
171    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
172        write!(
173            f,
174            "{}",
175            self.subtokens
176                .iter()
177                .map(ToString::to_string)
178                .collect::<String>()
179        )
180    }
181}
182
183impl AlphanumericVersionToken {
184    fn lowest() -> Self {
185        AlphanumericVersionToken {
186            subtokens: vec![SubToken::new("_")],
187        }
188    }
189    fn bump(&self) -> Self {
190        let mut next_subtokens = self.subtokens.clone();
191        let last = next_subtokens
192            .pop()
193            .expect("Token should have at least one subtoken");
194        if last.n.is_some() {
195            next_subtokens.push(last);
196            next_subtokens.push(SubToken::new("_"));
197        } else {
198            let new_last = SubToken::new(&(last.s + "_"));
199            next_subtokens.push(new_last);
200        }
201        AlphanumericVersionToken {
202            subtokens: next_subtokens,
203        }
204    }
205}
206#[allow(dead_code)] // Need to be checked
207impl AlphanumericVersionToken {
208    pub fn compare(&self, other: &Self) -> Ordering {
209        self.subtokens.iter().cmp(other.subtokens.iter())
210    }
211}
212
213#[test]
214fn test_bump_alpha_num() {
215    let a = AlphanumericVersionToken::new("1").unwrap();
216    assert_eq!(a.subtokens[0].n, Some(1));
217    let b = AlphanumericVersionToken::new("1_").unwrap();
218    assert_eq!(b.subtokens[0].n, Some(1));
219    assert_eq!(b.subtokens[1].s, "_");
220    assert_eq!(b.subtokens[1].n, None);
221    assert_eq!(a.bump(), b);
222}
223/// # RerVersion
224///
225/// # Description
226///
227/// A version type that uses a custom versioning scheme. To match the actual Rez versioning scheme,
228/// the version string must be alphanumeric and can contain any character except for whitespace.
229///
230/// ## Examples
231/// ```
232/// use rer_version::RerVersion;
233/// let v: RerVersion = "1.2.3-alpha+beta".try_into().unwrap();
234/// assert_eq!(v.to_string(), "1.2.3-alpha+beta");
235/// ```
236///
237/// Internally `Rc`-wrapped so cloning a version — which happens constantly,
238/// since versions are embedded in every `Ranges` the solver copies — is a
239/// refcount bump rather than a deep `Vec`/`String` copy.
240#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
241pub struct RerVersion(Rc<RerVersionInner>);
242
243/// Token/separator representation behind a [`RerVersion`].
244#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
245struct RerVersionInner {
246    tokens: Vec<AlphanumericVersionToken>,
247    seps: Vec<char>,
248}
249impl RerVersion {
250    /// # from_str
251    ///
252    /// # Description
253    ///
254    /// Parses a version string into a RerVersion. The version string must be alphanumeric and can
255    /// contain any character except for whitespace.
256    /// ## Examples
257    /// ```
258    /// use rer_version::RerVersion;
259    /// let v: RerVersion = "1.2.3-alpha+beta".try_into().unwrap();
260    /// assert_eq!(v.to_string(), "1.2.3-alpha+beta");
261    /// ```
262    fn parse_from_string(s: &str) -> Result<Self, &'static str> {
263        if !ALPHABET_REGEX.is_match(s) {
264            Err("Invalid version token")
265        } else {
266            let mut tokens = Vec::new();
267            let mut seps: Vec<char> = Vec::new();
268            let toks = ALPHABET_REGEX.find_iter(s);
269            let mut seps_iter = ALPHABET_REGEX.split(s);
270            for tok in toks {
271                tokens.push(AlphanumericVersionToken::new(tok.as_str())?);
272                if let Some(sep) = seps_iter.next() {
273                    if let Some(c) = sep.chars().next() {
274                        seps.push(c)
275                    }
276                }
277            }
278            Ok(RerVersion(Rc::new(RerVersionInner { tokens, seps })))
279        }
280    }
281}
282impl RerVersion {
283    pub fn lowest() -> Self {
284        RerVersion(Rc::new(RerVersionInner {
285            tokens: vec![AlphanumericVersionToken::lowest()],
286            seps: vec![],
287        }))
288    }
289    pub fn bump(&self) -> Self {
290        let mut next_tokens = self.0.tokens.clone();
291        let last = next_tokens
292            .pop()
293            .expect("Token should have at least one subtoken");
294        next_tokens.push(last.bump());
295        RerVersion(Rc::new(RerVersionInner {
296            tokens: next_tokens,
297            seps: self.0.seps.clone(),
298        }))
299    }
300}
301impl fmt::Display for RerVersion {
302    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
303        let mut s = String::new();
304        for (i, token) in self.0.tokens.iter().enumerate() {
305            s.push_str(&token.to_string());
306            if i < self.0.seps.len() {
307                s.push(self.0.seps[i]);
308            }
309        }
310        write!(f, "{}", s)
311    }
312}
313
314impl TryFrom<&str> for RerVersion {
315    type Error = &'static str;
316    fn try_from(s: &str) -> Result<Self, Self::Error> {
317        RerVersion::parse_from_string(s)
318    }
319}
320impl TryFrom<String> for RerVersion {
321    type Error = &'static str;
322    fn try_from(s: String) -> Result<Self, Self::Error> {
323        RerVersion::parse_from_string(&s)
324    }
325}
326
327#[test]
328fn test_from() {
329    let v: RerVersion = "1.2.3-alpha+beta".try_into().unwrap();
330    assert_eq!(v.to_string(), "1.2.3-alpha+beta");
331}
332#[test]
333fn test_display() {
334    let v = RerVersion::parse_from_string("1.2.3-alpha+beta").unwrap();
335    assert_eq!(v.to_string(), "1.2.3-alpha+beta");
336}
337#[test]
338fn test_from_str() {
339    let v = RerVersion::parse_from_string("1.2.3").unwrap();
340    assert_eq!(v.0.tokens.len(), 3);
341    assert_eq!(v.0.seps.len(), 2);
342    let v = RerVersion::parse_from_string("1.2.3-alpha").unwrap();
343    assert_eq!(v.0.tokens.len(), 4);
344    assert_eq!(v.0.seps.len(), 3);
345    let v = RerVersion::parse_from_string("1.2.3-alpha+beta").unwrap();
346    assert_eq!(v.0.tokens.len(), 5);
347    assert_eq!(v.0.seps.len(), 4);
348    let v: RerVersion = "2.0.0_".try_into().unwrap();
349    assert_eq!(v.0.tokens[2], AlphanumericVersionToken::new("0_").unwrap());
350    assert_eq!(v.0.seps, vec!['.', '.']);
351}
352#[test]
353fn test_order() {
354    let a = RerVersion::parse_from_string("1.2.3").unwrap();
355    let b = RerVersion::parse_from_string("1.2.4").unwrap();
356    assert!(a < b);
357    let a = RerVersion::parse_from_string("1.2.3").unwrap();
358    let b = RerVersion::parse_from_string("1.2.3-alpha").unwrap();
359    assert!(a < b);
360    let a = RerVersion::parse_from_string("2.0.0").unwrap();
361    let b = RerVersion::parse_from_string("2.0.0_").unwrap();
362    assert!(a < b);
363}
364
365#[test]
366fn test_bump_rez_version() {
367    let a: RerVersion = "1.2.3".try_into().unwrap();
368    let b: RerVersion = "1.2.3_".try_into().unwrap();
369    assert_eq!(a.bump(), b);
370}
371
372#[test]
373fn test_compare_subtoken() {
374    let a = SubToken::new("1");
375    let b = SubToken::new("2");
376    assert!(a < b);
377    let a = SubToken::new("1");
378    let b = SubToken::new("1");
379    assert!(a == b);
380    let a = SubToken::new("1");
381    let b = SubToken::new("1a");
382    assert!(a >= b);
383    let a = SubToken::new("a");
384    let b = SubToken::new("1");
385    assert!(a < b);
386    let a = SubToken::new("a");
387    let b = SubToken::new("a");
388    assert!(a == b);
389    let a = SubToken::new("a");
390    let b = SubToken::new("A");
391    assert!(a < b);
392}
393#[test]
394fn test_alphanumeric_version_token_compare() {
395    let a = AlphanumericVersionToken::new("3").unwrap();
396    let b = AlphanumericVersionToken::new("4").unwrap();
397    assert!(a < b);
398    let a = AlphanumericVersionToken::new("01").unwrap();
399    let b = AlphanumericVersionToken::new("1").unwrap();
400    assert!(a < b);
401    let a = AlphanumericVersionToken::new("beta").unwrap();
402    let b = AlphanumericVersionToken::new("1").unwrap();
403    assert!(a < b);
404    let a = AlphanumericVersionToken::new("a").unwrap();
405    let b = AlphanumericVersionToken::new("A").unwrap();
406    assert!(a < b);
407    let a = AlphanumericVersionToken::new("alpha3").unwrap();
408    let b = AlphanumericVersionToken::new("alpha4").unwrap();
409    assert!(a < b);
410    let a = AlphanumericVersionToken::new("alpha").unwrap();
411    let b = AlphanumericVersionToken::new("alpha3").unwrap();
412    assert!(a < b);
413    let a = AlphanumericVersionToken::new("gamma33").unwrap();
414    let b = AlphanumericVersionToken::new("33gamma").unwrap();
415    assert!(a < b);
416}