maven_version/
maven3.rs

1//! # Maven 3 Version Parser
2//! 
3//! This crate is a direct translation of
4//! `https://github.com/apache/maven/blob/master/maven-artifact/src/main/java/org/apache/maven/artifact/versioning/ComparableVersion.java`
5//!
6
7// see additional https://cwiki.apache.org/confluence/display/MAVENOLD/Versioning
8
9use std::convert::{AsRef, From};
10use std::fmt;
11use std::hash;
12use std::cmp::Ordering;
13use std::cmp;
14
15use ::ArtifactVersion;
16
17const RELEASE_VERSION_INDEX: &'static str = "5"; // QUALIFIERS index of ""
18
19// ---------------------------------------
20// Maven3ArtifactVersion
21// ---------------------------------------
22
23#[derive(Debug)]
24pub struct Maven3ArtifactVersion<'a> {
25    version: &'a str,
26    canonical: String,
27    items: Vec<Item>,
28}
29
30/// Maven3 Version Artifact
31impl<'a> Maven3ArtifactVersion<'a> {
32
33    /// Creates a new instance of Maven3ArtifactVersion.
34    ///
35    /// # Example
36    ///
37    /// ```
38    /// use maven_version::Maven3ArtifactVersion; 
39    ///
40    /// let v1 = Maven3ArtifactVersion::new("2.0.1");
41    /// let v2 = Maven3ArtifactVersion::new("2.0.1-xyz");
42    /// assert!(v1 < v2);
43    /// ```
44    pub fn new(version: &'a str) -> Self {
45        let items = parse_items(version);
46        let canonical = make_canonical(&items);
47        Maven3ArtifactVersion{version, canonical, items}
48    }
49
50    pub fn canonical(&self) -> &str {
51        &self.canonical
52    }
53}
54
55impl<'a> ArtifactVersion for Maven3ArtifactVersion<'a> {
56
57    fn version(&self) -> &str {
58        self.version
59    }
60}
61
62// From
63//
64impl<'a> From<&'a str> for Maven3ArtifactVersion<'a> {
65
66    fn from(version: &'a str) -> Maven3ArtifactVersion<'a> {
67        Maven3ArtifactVersion::new(version)
68    }
69}
70
71// Eq
72//
73impl<'a> Eq for Maven3ArtifactVersion<'a> {}
74
75impl<'a> PartialEq for Maven3ArtifactVersion<'a> {
76
77    fn eq(&self, other: &Maven3ArtifactVersion) -> bool {
78        self.canonical == other.canonical
79    }
80}
81
82// Ord
83//
84impl<'a> Ord for Maven3ArtifactVersion<'a> {
85
86    fn cmp(&self, other: &Maven3ArtifactVersion) -> Ordering {
87        compare_all_items(&self.items, &other.items)
88    }
89}
90
91fn compare_all_items(first: &[Item], second: &[Item]) -> Ordering {
92    use self::Item::*;
93
94    let first_length = first.len();
95    let second_length = second.len();
96
97    for index in 0..cmp::max(first_length, second_length) { 
98        let left: Option<&Item> = first.get(index);
99        let right: Option<&Item> = second.get(index);
100
101        let result = match left {
102            Some(&Integer(i)) => compare_integer_with(i, right),
103            Some(&Str(ref s)) => compare_str_with(s, right),
104            Some(&Minus) => {
105                match right {
106                    Some(&Integer(_)) => Ordering::Less,  // 1-1 < 1.0.x
107                    Some(&Str(_)) => Ordering::Greater, // 1-1 > 1-sp
108                    Some(&Minus) => compare_all_items(&first[(index + 1)..], &second[(index + 1)..]),
109                    None => compare_all_items(&first[(index + 1)..], &second[index..]),
110                }
111            },
112            None => {
113                let result_ordering = match right {
114                    Some(_) => compare_all_items(&second[index..], &first[index..]),
115                    None => Ordering::Equal,
116                };
117
118                match result_ordering {
119                    Ordering::Greater => Ordering::Less,
120                    Ordering::Less => Ordering::Greater,
121                    _ => Ordering::Equal,
122                } 
123            },
124        };
125
126        if result != Ordering::Equal {
127            return result;
128        }
129    }
130
131    Ordering::Equal
132}
133
134fn compare_integer_with(value: u32, item: Option<&Item>) -> Ordering {
135    use self::Item::*;
136
137    match item {
138        Some(&Integer(ref i)) => value.cmp(i),
139        Some(&Str(_)) | Some(&Minus) => Ordering::Greater, // 1.1 > 1-sp | 1.1 > 1-1
140        None => {
141            // 1.0 == 1, 1.1 > 1
142            if value == 0 {
143                Ordering::Equal
144            } else {
145                Ordering::Greater
146            }
147        },
148    }
149}
150
151fn compare_str_with(value: &str, item: Option<&Item>) -> Ordering {
152    use self::Item::*;
153
154    match item {
155        Some(&Integer(_)) | Some(&Minus) => Ordering::Less, // 1.any < 1.1 ? | 1.any < 1-1
156        Some(&Str(ref s)) => comparable_str_qualifier(value).cmp(&comparable_str_qualifier(s)),
157        None => {
158            // 1-rc < 1, 1-ga > 1
159            let cmp_qualifier: &str = &comparable_str_qualifier(value);
160            cmp_qualifier.cmp(RELEASE_VERSION_INDEX)
161        },
162    }
163}
164
165impl<'a> PartialOrd for Maven3ArtifactVersion<'a> {
166
167    fn partial_cmp(&self, other: &Maven3ArtifactVersion) -> Option<Ordering> {
168        Some(self.cmp(other))
169    }
170}
171
172// Hash
173//
174impl<'a> hash::Hash for Maven3ArtifactVersion<'a> {
175    
176    fn hash<H: hash::Hasher>(&self, state: &mut H) {
177        self.canonical.hash(state);
178    }
179}
180
181// Display
182//
183impl<'a> fmt::Display for Maven3ArtifactVersion<'a> {
184    
185    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
186        write!(f, "{}", self.version)
187    }
188}
189
190fn make_canonical(items: &[Item]) -> String {
191    let mut buffer = String::new();
192
193    let mut prev_val = false;
194    for item in items {
195        match *item {
196            Item::Integer(i) => {
197                if prev_val {
198                    buffer.push('.');
199                }                    
200                prev_val = true;
201                buffer.push_str(&format!("{}", i));
202            },
203            Item::Str(ref s) => {
204                if prev_val {
205                    buffer.push('.');
206                }                    
207                prev_val = true;
208                buffer.push_str(s);
209            },
210            Item::Minus => {
211                prev_val = false;
212                buffer.push('-');
213            },
214        };
215    }
216
217    buffer
218}
219
220// ---------------------------------------
221// Item
222// ---------------------------------------
223
224#[derive(Debug)]
225enum Item {
226    Integer(u32),
227    Str(String),
228    Minus, // list equivalent
229}
230
231impl Item {
232
233    fn is_minus(&self) -> bool {
234        match *self {
235            Item::Minus => true,
236            _ => false,
237        }
238    }    
239}
240
241impl fmt::Display for Item {
242    
243    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
244        match *self {
245            Item::Integer(i) => write!(f, "{}", i),
246            Item::Str(ref s) => write!(f, "{}", s),
247            Item::Minus => write!(f, "-"),
248        }
249    }
250}
251
252fn comparable_str_qualifier(qualifier: &str) -> String {
253    match qualifier {
254        "alpha" => "0".to_owned(),
255        "beta" => "1".to_owned(),
256        "milestone" => "2".to_owned(),
257        "rc" => "3".to_owned(),
258        "snapshot" => "4".to_owned(),
259        "" => "5".to_owned(),
260        "sp" => "6".to_owned(),
261        _ => format!("7-{}", qualifier), // length of all special qualifiers + qualifier
262    }
263}
264
265fn parse_items(version: &str) -> Vec<Item> {
266    let version = version.to_lowercase();
267    let mut items: Vec<Item> = Vec::new();
268    
269    let mut is_digit = false;
270    let mut start_index = 0;
271
272    let chars: Vec<char> = version.chars().collect();
273    for (index, c) in chars.iter().enumerate() {
274        if *c == '.' {
275            if index == start_index {
276                items.push(Item::Integer(0));
277            } else {
278                let substring: String = chars[start_index..index].into_iter().collect();
279                items.push(parse_item(is_digit, &substring));
280            }
281            start_index = index + 1;
282
283        } else if *c == '-' {
284            if index == start_index {
285                items.push(Item::Integer(0));
286            } else {
287                let substring: String = chars[start_index..index].into_iter().collect();
288                items.push(parse_item(is_digit, &substring));
289            }
290
291            start_index = index + 1;
292            items.push(Item::Minus);
293
294        } else if c.is_digit(10) {
295            if !is_digit && index > start_index {
296                let substring: String = chars[start_index..index].into_iter().collect();
297                items.push(to_string_item(substring, true));
298
299                start_index = index;
300                items.push(Item::Minus);
301            }
302            is_digit = true;
303
304        } else {
305            if is_digit && index > start_index {
306                let substring: String = chars[start_index..index].into_iter().collect();
307                items.push(parse_item(true, substring));
308
309                start_index = index;
310                items.push(Item::Minus);
311            }
312            is_digit = false;
313        }
314    }
315
316    if chars.len() > start_index {
317        let substring: String = chars[start_index..].into_iter().collect();
318        items.push(parse_item(is_digit, substring));
319    }
320
321    normalize(&mut items);
322
323    items
324}
325
326fn parse_item<T: AsRef<str>>(is_digit: bool, buf: T) -> Item {
327    if is_digit {
328        to_integer_item(buf.as_ref())
329    } else {
330        to_string_item(buf, false)
331    }
332}
333
334fn to_integer_item(value: &str) -> Item {
335    Item::Integer(value.parse::<u32>().unwrap())
336}
337
338fn to_string_item<T: AsRef<str>>(value: T, followed_by_digit: bool) -> Item {
339    let mut value: &str = value.as_ref();
340    if followed_by_digit && value.chars().count() == 1 {
341        value = match value.chars().nth(0) {
342            Some('a') => "alpha",
343            Some('b') => "beta",
344            Some('m') => "milestone",
345            _ => value,
346        }
347    }
348    value = match value {
349        "ga" | "final" => "",
350        "cr" => "rc",
351        _ => value,
352    };
353    Item::Str(value.to_string())
354}
355
356// Splits all items at Item::Minus and normalize
357fn normalize(items: &mut Vec<Item>) {
358    let mut start_index = items.len() - 1;
359
360    for index in (0..items.len()).rev() {
361        if items[index].is_minus() {
362            normalize_sublist(items, (index + 1), start_index + 1);
363            start_index = index;
364        }      
365    }
366
367    normalize_sublist(items, 0, start_index + 1);
368}
369
370fn normalize_sublist(items: &mut Vec<Item>, start_index: usize, end_index: usize) {
371    for index in (start_index..end_index).rev() {
372        // check for null
373        let is_null = match items[index] {
374            Item::Minus => (items.len() - index) <= 1, // minus only in list
375            Item::Integer(i) => i == 0,
376            Item::Str(ref s) => comparable_str_qualifier(s) == RELEASE_VERSION_INDEX,
377        };
378
379        if is_null {
380            // remove null trailing items: 0, "", empty list
381            items.remove(index);
382        } else if !items[index].is_minus() {
383            break;
384        }
385    }
386}
387
388#[cfg(test)]
389mod tests {
390
391    // see https://github.com/apache/maven/blob/master/maven-artifact/src/test/java/org/apache/maven/artifact/versioning/ComparableVersionTest.java
392
393    use super::*;
394    use std::hash::{Hash, Hasher};
395    use std::collections::hash_map::DefaultHasher;
396
397    const VERSIONS_QUALIFIER: [&'static str; 22] = ["1-alpha2snapshot", "1-alpha2", "1-alpha-123", "1-beta-2", "1-beta123", "1-m2", "1-m11", "1-rc", "1-cr2",
398            "1-rc123", "1-SNAPSHOT", "1", "1-sp", "1-sp2", "1-sp123", "1-abc", "1-def", "1-pom-1", "1-1-snapshot", "1-1", "1-2", "1-123" ];
399
400    const VERSIONS_NUMBER: [&'static str; 25] = ["2.0", "2-1", "2.0.a", "2.0.0.a", "2.0.2", "2.0.123", "2.1.0", "2.1-a", "2.1b", "2.1-c", "2.1-1", "2.1.0.1",
401            "2.2", "2.123", "11.a2", "11.a11", "11.b2", "11.b11", "11.m2", "11.m11", "11", "11.a", "11b", "11c", "11m"];
402
403    fn new_artifact_version(version: &str) -> Maven3ArtifactVersion {
404        let artifact_version = Maven3ArtifactVersion::new(version);
405
406        {
407            let canonical = artifact_version.canonical();
408
409            let parsed_artifact_version = Maven3ArtifactVersion::new(canonical);
410            let parsed_canonical = parsed_artifact_version.canonical();
411
412            //println!("canonical( {} ) = {}", version, canonical);
413            assert_eq!(canonical, parsed_canonical, "canonical( {} ) = {} -> canonical: {}", version, canonical, parsed_canonical);
414        }
415
416        artifact_version
417    }
418
419    fn calc_hash<T: Hash>(t: &T) -> u64 {
420        let mut s = DefaultHasher::new();
421        t.hash(&mut s);
422        s.finish()
423    }
424
425    fn check_versions_equal(v1: &str, v2: &str) {
426        let c1 = new_artifact_version(v1);
427        let c2 = new_artifact_version(v2);
428
429        assert!(c1.cmp(&c2) == Ordering::Equal, "expected {} == {}", v1, v2);
430        assert!(c2.cmp(&c1) == Ordering::Equal, "expected {} == {}", v2, v1);
431        assert!(calc_hash(&c1) == calc_hash(&c2), "expected same hashcode for {} and {}", v1, v2);
432        assert!(c1 == c2, "expected {} == {}", v1, v2);
433        assert!(c2 == c1, "expected {} == {}", v2, v1);
434    }
435
436    fn check_versions_order(v1: &str, v2: &str) {
437        let c1 = new_artifact_version(v1);
438        let c2 = new_artifact_version(v2);
439        assert!(c1 < c2, "expected {} < {}", v1, v2);
440        assert!(c2 > c1, "expected {} > {}", v2, v1);
441    }
442
443    fn check_all_versions_order(versions: &[&'static str]) {
444        let c: Vec<Maven3ArtifactVersion> = versions.iter().map(|version| Maven3ArtifactVersion::new(version)).collect();
445        for i in 1..versions.len() {
446            let low = &c[i - 1];
447
448            for j in i..versions.len() {
449                let high = &c[j];
450                assert!(low < high, "expected {} < {}", low, high);
451                assert!(high > low, "expected {} > {}", high, low);
452            }
453        }
454    }
455
456    #[test]
457    fn test_versions_qualifier() {
458        check_all_versions_order(&VERSIONS_QUALIFIER);
459    }
460
461    #[test]
462    fn test_versions_number() {
463        check_all_versions_order(&VERSIONS_NUMBER);
464    }
465
466    #[test]
467    fn test_versions_equal() {
468        new_artifact_version( "1.0-alpha" );
469        check_versions_equal( "1", "1" );
470        check_versions_equal( "1", "1.0" );
471        check_versions_equal( "1", "1.0.0" );
472        check_versions_equal( "1.0", "1.0.0" );
473        check_versions_equal( "1", "1-0" );
474        check_versions_equal( "1", "1.0-0" );
475        check_versions_equal( "1.0", "1.0-0" );
476        // no separator between number and character
477        check_versions_equal( "1a", "1-a" );
478        check_versions_equal( "1a", "1.0-a" );
479        check_versions_equal( "1a", "1.0.0-a" );
480        check_versions_equal( "1.0a", "1-a" );
481        check_versions_equal( "1.0.0a", "1-a" );
482        check_versions_equal( "1x", "1-x" );
483        check_versions_equal( "1x", "1.0-x" );
484        check_versions_equal( "1x", "1.0.0-x" );
485        check_versions_equal( "1.0x", "1-x" );
486        check_versions_equal( "1.0.0x", "1-x" );
487
488        // aliases
489        check_versions_equal( "1ga", "1" );
490        check_versions_equal( "1final", "1" );
491        check_versions_equal( "1cr", "1rc" );
492
493        // special "aliases" a, b and m for alpha, beta and milestone
494        check_versions_equal( "1a1", "1-alpha-1" );
495        check_versions_equal( "1b2", "1-beta-2" );
496        check_versions_equal( "1m3", "1-milestone-3" );
497
498        // case insensitive
499        check_versions_equal( "1X", "1x" );
500        check_versions_equal( "1A", "1a" );
501        check_versions_equal( "1B", "1b" );
502        check_versions_equal( "1M", "1m" );
503        check_versions_equal( "1Ga", "1" );
504        check_versions_equal( "1GA", "1" );
505        check_versions_equal( "1Final", "1" );
506        check_versions_equal( "1FinaL", "1" );
507        check_versions_equal( "1FINAL", "1" );
508        check_versions_equal( "1Cr", "1Rc" );
509        check_versions_equal( "1cR", "1rC" );
510        check_versions_equal( "1m3", "1Milestone3" );
511        check_versions_equal( "1m3", "1MileStone3" );
512        check_versions_equal( "1m3", "1MILESTONE3" );        
513    }
514    
515    #[test]
516    fn test_version_comparing() {
517        check_versions_order( "1", "2" );
518        check_versions_order( "1.5", "2" );
519        check_versions_order( "1", "2.5" );
520        check_versions_order( "1.0", "1.1" );
521        check_versions_order( "1.1", "1.2" );
522        check_versions_order( "1.0.0", "1.1" );
523        check_versions_order( "1.0.1", "1.1" );
524        check_versions_order( "1.1", "1.2.0" );
525
526        check_versions_order( "1.0-alpha-1", "1.0" );
527        check_versions_order( "1.0-alpha-1", "1.0-alpha-2" );
528        check_versions_order( "1.0-alpha-1", "1.0-beta-1" );
529
530        check_versions_order( "1.0-beta-1", "1.0-SNAPSHOT" );
531        check_versions_order( "1.0-SNAPSHOT", "1.0" );
532        check_versions_order( "1.0-alpha-1-SNAPSHOT", "1.0-alpha-1" );
533
534        check_versions_order( "1.0", "1.0-1" );
535        check_versions_order( "1.0-1", "1.0-2" );
536        check_versions_order( "1.0.0", "1.0-1" );
537
538        check_versions_order( "2.0-1", "2.0.1" );
539        check_versions_order( "2.0.1-klm", "2.0.1-lmn" );
540        check_versions_order( "2.0.1", "2.0.1-xyz" );
541
542        check_versions_order( "2.0.1", "2.0.1-123" );
543        check_versions_order( "2.0.1-xyz", "2.0.1-123" );
544    }
545
546    #[test]
547    fn test_mng5568() {
548        let a = "6.1.0";
549        let b = "6.1.0rc3";
550        let c = "6.1H.5-beta"; // this is the unusual version string, with 'H' in the middle
551
552        check_versions_order( b, a ); // classical
553        check_versions_order( b, c ); // now b < c, but before MNG-5568, we had b > c
554        check_versions_order( a, c );
555    }
556}