maven_version/
maven2.rs

1//! # Maven 2 Version Parser
2//! 
3//! This crate is a direct translation of
4//! `https://github.com/apache/maven/blob/maven-2.2.x/maven-artifact/src/main/java/org/apache/maven/artifact/versioning/DefaultArtifactVersion.java`
5//!
6use std::fmt;
7use std::convert::From;
8use std::cmp::Ordering;
9use std::hash;
10
11use ::ArtifactVersion;
12
13#[derive(Debug)]
14pub struct Maven2ArtifactVersion<'a> {
15    major_version: Option<u32>,
16    minor_version: Option<u32>,
17    incremental_version: Option<u32>,
18    build_number: Option<u32>,
19    qualifier: Option<&'a str>,
20    unparsed: &'a str,
21}
22
23impl<'a> Maven2ArtifactVersion<'a> {
24
25    /// Creates a new instance of Maven2ArtifactVersion.
26    pub fn new(maven_version: &'a str) -> Self {
27        parse_version(maven_version)
28    }
29
30    pub fn major_version(&self) -> u32 {
31        self.major_version.unwrap_or(0)
32    }
33    
34    pub fn minor_version(&self) -> u32 {
35        self.minor_version.unwrap_or(0)
36    }
37
38    pub fn incremental_version(&self) -> u32 {
39        self.incremental_version.unwrap_or(0)
40    }
41
42    pub fn build_number(&self) -> u32 {
43        self.build_number.unwrap_or(0)
44    }
45
46    pub fn qualifier(&self) -> Option<&'a str> {
47        self.qualifier
48    }    
49}
50
51impl<'a> ArtifactVersion for Maven2ArtifactVersion<'a> {
52
53    fn version(&self) -> &str {
54        self.unparsed
55    }
56}
57
58// From
59//
60impl<'a> From<&'a str> for Maven2ArtifactVersion<'a> {
61
62    fn from(maven_version: &'a str) -> Maven2ArtifactVersion<'a> {
63        Maven2ArtifactVersion::new(maven_version)
64    }
65}
66
67// Display
68//
69impl<'a> fmt::Display for Maven2ArtifactVersion<'a> {
70
71    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
72        write!(f, "{}", self.unparsed)
73    }
74}
75
76#[allow(unused_assignments)]
77fn parse_version(maven_version: &str) -> Maven2ArtifactVersion {
78    let mut major_version: Option<u32> = None;
79    let mut minor_version: Option<u32> = None;
80    let mut incremental_version: Option<u32> = None;
81    let mut build_number: Option<u32> = None;
82    let mut qualifier: Option<&str> = None;
83
84    // 0. Let's go
85    let mut part1: Option<&str> = None;
86    let mut part2: Option<&str> = None;
87
88    // 1. Split version-qualifier
89    let splitted: Vec<&str> = maven_version.splitn(2, '-').collect();
90    if splitted.len() == 2 {
91        part1 = Some(splitted[0]);
92        part2 = Some(splitted[1]);
93    } else {
94        part1 = Some(maven_version);
95    }
96
97    // 2. Parse quailifier
98    if let Some(q) = part2 {
99        if q.chars().count() == 1 || !q.starts_with('0') {
100            if let Ok(bn) = q.parse::<u32>() {
101                build_number = Some(bn);
102            } else {
103                qualifier = part2;
104            }
105        } else {
106            qualifier = part2;
107        }
108    }
109
110    // 3. Parse Version
111    if let Some(part1_str) = part1 {
112        if !part1_str.contains('.') && !part1_str.starts_with('0') {
113            if let Ok(mv) = part1_str.parse::<u32>() {
114                major_version = Some(mv);
115            } else {
116                qualifier = Some(maven_version);
117                build_number = None;
118            } 
119
120        } else {
121            let mut fallback = false;
122            let mut token_iter = part1_str.split('.');
123
124            // major
125            if let Some(value) = token_iter.next() {
126                if let Ok(i) = parse_integer_token(value) {
127                    major_version = Some(i);
128                } else {
129                    fallback = true;
130                }   
131            } else {
132                fallback = true;
133            }
134
135            // minor
136            if let Some(value) = token_iter.next() {
137                if let Ok(i) = parse_integer_token(value) {
138                    minor_version = Some(i);
139                } else {
140                    fallback = true;
141                }   
142            }
143
144            // incremental
145            if let Some(value) = token_iter.next() {
146                if let Ok(i) = parse_integer_token(value) {
147                    incremental_version = Some(i);
148                } else {
149                    fallback = true;
150                }   
151            }
152
153            // rest            
154            if token_iter.next().is_some() {
155                fallback = true;
156            }
157
158            if part1_str.contains("..") || part1_str.starts_with('.') || part1_str.ends_with('.') {
159                fallback = true;
160            }
161
162            if fallback {
163                // qualifier is the whole version, including "-"
164                qualifier = Some(maven_version);
165                major_version = None;
166                minor_version = None;
167                incremental_version = None;
168                build_number = None;                
169            }
170        }
171    }
172
173    Maven2ArtifactVersion {
174        major_version: major_version,
175        minor_version: minor_version,
176        incremental_version: incremental_version,
177        build_number: build_number,
178        qualifier: qualifier,
179        unparsed: maven_version,
180    }
181}
182
183fn parse_integer_token(token: &str) -> Result<u32, String> {
184    if token.chars().count() > 1 && token.starts_with('0') {
185        Err(format!("Number part has a leading 0: '{}'", token))
186    } else {
187        token.parse::<u32>().map_err(|_| "Number is invalid".to_string())
188    }
189}
190
191// PartialEq
192//
193impl<'a> PartialEq for Maven2ArtifactVersion<'a> {
194
195    fn eq(&self, other: &Maven2ArtifactVersion<'a>) -> bool {
196        self.partial_cmp(other) == Some(Ordering::Equal)
197    }
198}
199
200// Eq
201//
202impl<'a> Eq for Maven2ArtifactVersion<'a> {}
203
204// PartialOrd
205//
206impl<'a> PartialOrd for Maven2ArtifactVersion<'a> {
207
208    fn partial_cmp(&self, other: &Maven2ArtifactVersion<'a>) -> Option<Ordering> {
209        Some(self.cmp(other))
210    }
211}
212
213// Ord
214//
215impl<'a> Ord for Maven2ArtifactVersion<'a> {
216
217    fn cmp(&self, other: &Maven2ArtifactVersion<'a>) -> Ordering {
218        let mut result: Ordering = self.major_version().cmp(&other.major_version());
219
220        if result == Ordering::Equal {
221            result = self.minor_version().cmp(&other.minor_version());
222        }
223
224        if result == Ordering::Equal {
225            result = self.incremental_version().cmp(&other.incremental_version());
226        }
227
228        if result == Ordering::Equal {
229            if let Some(qualifier) = self.qualifier() {
230                if let Some(other_qualifier) = other.qualifier() {
231                    let qualifier_count = qualifier.chars().count();
232                    let other_qualifier_count = other_qualifier.chars().count();
233
234                    if qualifier_count > other_qualifier_count && qualifier.starts_with(other_qualifier) {
235                        // here, the longer one that otherwise match is considered older
236                        result = Ordering::Less;
237                    } else if qualifier_count < other_qualifier_count && other_qualifier.starts_with(qualifier){
238                        // here, the longer one that otherwise match is considered older
239                        result = Ordering::Greater;
240                    } else {
241                        result = qualifier.cmp(other_qualifier);
242                    }
243
244                } else {
245                    // otherVersion has no qualifier but we do - that's newer
246                    result = Ordering::Less;                    
247                }                
248            } else if other.qualifier().is_some() {
249                // otherVersion has a qualifier but we don't, we're newer
250                result = Ordering::Greater;
251            } else {
252                result = self.build_number().cmp(&other.build_number());
253            }
254        }
255
256        result
257    }
258}
259
260// Hash
261//
262impl<'a> hash::Hash for Maven2ArtifactVersion<'a> {
263
264    fn hash<H: hash::Hasher>(&self, state: &mut H) {
265        self.major_version().hash(state);
266        self.minor_version().hash(state);
267        self.incremental_version().hash(state);
268        self.build_number().hash(state);
269
270        if let Some(qualifier) = self.qualifier() {
271            qualifier.hash(state);
272        }
273    }
274}
275
276#[cfg(test)]
277mod tests {
278
279    // https://github.com/apache/maven/blob/maven-2.2.x/maven-artifact/src/test/java/org/apache/maven/artifact/versioning/DefaultArtifactVersionTest.java
280
281    use super::*;
282
283    #[test]
284    fn test_version_parsing() {
285        check_version_parsing( "1", 1, 0, 0, 0, None );
286        check_version_parsing( "1.2", 1, 2, 0, 0, None );
287        check_version_parsing( "1.2.3", 1, 2, 3, 0, None );
288        check_version_parsing( "1.2.3-1", 1, 2, 3, 1, None );
289        check_version_parsing( "1.2.3-alpha-1", 1, 2, 3, 0, Some("alpha-1") );
290        check_version_parsing( "1.2-alpha-1", 1, 2, 0, 0, Some("alpha-1") );
291        check_version_parsing( "1.2-alpha-1-20050205.060708-1", 1, 2, 0, 0, Some("alpha-1-20050205.060708-1") );
292        check_version_parsing( "RELEASE", 0, 0, 0, 0, Some("RELEASE") );
293        check_version_parsing( "2.0-1", 2, 0, 0, 1, None );
294
295        // 0 at the beginning of a number has a special handling
296        check_version_parsing( "02", 0, 0, 0, 0, Some("02") );
297        check_version_parsing( "0.09", 0, 0, 0, 0, Some("0.09") );
298        check_version_parsing( "0.2.09", 0, 0, 0, 0, Some("0.2.09") );
299        check_version_parsing( "2.0-01", 2, 0, 0, 0, Some("01") );
300
301        // version schemes not really supported: fully transformed as qualifier
302        check_version_parsing( "1.0.1b", 0, 0, 0, 0, Some("1.0.1b") );
303        check_version_parsing( "1.0M2", 0, 0, 0, 0, Some("1.0M2") );
304        check_version_parsing( "1.0RC2", 0, 0, 0, 0, Some("1.0RC2") );
305        check_version_parsing( "1.7.3.0", 0, 0, 0, 0, Some("1.7.3.0") );
306        check_version_parsing( "1.7.3.0-1", 0, 0, 0, 0, Some("1.7.3.0-1") );
307        check_version_parsing( "PATCH-1193602", 0, 0, 0, 0, Some("PATCH-1193602") );
308        check_version_parsing( "5.0.0alpha-2006020117", 0, 0, 0, 0, Some("5.0.0alpha-2006020117") );
309        check_version_parsing( "1.0.0.-SNAPSHOT", 0, 0, 0, 0, Some("1.0.0.-SNAPSHOT") );
310        check_version_parsing( "1..0-SNAPSHOT", 0, 0, 0, 0, Some("1..0-SNAPSHOT") );
311        check_version_parsing( "1.0.-SNAPSHOT", 0, 0, 0, 0, Some("1.0.-SNAPSHOT") );
312        check_version_parsing( ".1.0-SNAPSHOT", 0, 0, 0, 0, Some(".1.0-SNAPSHOT") );
313
314        check_version_parsing( "1.2.3.200705301630", 0, 0, 0, 0, Some("1.2.3.200705301630") );
315        check_version_parsing( "1.2.3-200705301630", 1, 2, 3, 0, Some("200705301630") );
316    }
317
318    fn check_version_parsing(maven_version: &str, major: u32, minor: u32, incremental: u32, build_number: u32, qualifier: Option<&str>) {
319        let actual = Maven2ArtifactVersion::from(maven_version);
320        let parsed = format!("'{}' parsed as ('{:?}', '{:?}', '{:?}', '{:?}', '{:?}'), ",
321                         maven_version, actual.major_version, actual.minor_version,
322                         actual.incremental_version, actual.build_number,
323                         actual.qualifier);
324
325        assert_eq!(major, actual.major_version(), "{} check major version", parsed );
326        assert_eq!(minor, actual.minor_version(), "{} check minor version", parsed );
327        assert_eq!(incremental, actual.incremental_version(), "{} check incremental version", parsed );
328        assert_eq!(build_number, actual.build_number(), "{} check build number version", parsed );
329        assert_eq!(qualifier, actual.qualifier(), "{} check qualifier version", parsed );
330    }
331
332    #[test]
333    fn test_version_comparing() {
334        assert_version_equal( "1", "1" );
335        assert_version_older( "1", "2" );
336        assert_version_older( "1.5", "2" );
337        assert_version_older( "1", "2.5" );
338        assert_version_equal( "1", "1.0" );
339        assert_version_equal( "1", "1.0.0" );
340        assert_version_older( "1.0", "1.1" );
341        assert_version_older( "1.1", "1.2" );
342        assert_version_older( "1.0.0", "1.1" );
343        assert_version_older( "1.1", "1.2.0" );
344        assert_version_older( "1.2", "1.10" );
345
346        assert_version_older( "1.0-alpha-1", "1.0" );
347        assert_version_older( "1.0-alpha-1", "1.0-alpha-2" );
348        assert_version_older( "1.0-alpha-1", "1.0-beta-1" );
349
350        assert_version_older( "1.0-SNAPSHOT", "1.0-beta-1" );
351        assert_version_older( "1.0-SNAPSHOT", "1.0" );
352        assert_version_older( "1.0-alpha-1-SNAPSHOT", "1.0-alpha-1" );
353
354        assert_version_older( "1.0", "1.0-1" );
355        assert_version_older( "1.0-1", "1.0-2" );
356        assert_version_equal( "2.0-0", "2.0" );
357        assert_version_older( "2.0", "2.0-1" );
358        assert_version_older( "2.0.0", "2.0-1" );
359        assert_version_older( "2.0-1", "2.0.1" );
360
361        assert_version_older( "2.0.1-klm", "2.0.1-lmn" );
362        assert_version_older( "2.0.1-xyz", "2.0.1" );
363
364        assert_version_older( "2.0.1", "2.0.1-123" );
365        assert_version_older( "2.0.1-xyz", "2.0.1-123" );
366    }
367
368    #[test]
369    fn test_version_snapshot_comparing() {
370        assert_version_equal( "1-SNAPSHOT", "1-SNAPSHOT" );
371        assert_version_older( "1-SNAPSHOT", "2-SNAPSHOT" );
372        assert_version_older( "1.5-SNAPSHOT", "2-SNAPSHOT" );
373        assert_version_older( "1-SNAPSHOT", "2.5-SNAPSHOT" );
374        assert_version_equal( "1-SNAPSHOT", "1.0-SNAPSHOT" );
375        assert_version_equal( "1-SNAPSHOT", "1.0.0-SNAPSHOT" );
376        assert_version_older( "1.0-SNAPSHOT", "1.1-SNAPSHOT" );
377        assert_version_older( "1.1-SNAPSHOT", "1.2-SNAPSHOT" );
378        assert_version_older( "1.0.0-SNAPSHOT", "1.1-SNAPSHOT" );
379        assert_version_older( "1.1-SNAPSHOT", "1.2.0-SNAPSHOT" );
380        assert_version_older( "1.0-alpha-1-SNAPSHOT", "1.0-alpha-2-SNAPSHOT" );
381        assert_version_older( "1.0-alpha-1-SNAPSHOT", "1.0-beta-1-SNAPSHOT" );
382        assert_version_older( "1.0-SNAPSHOT-SNAPSHOT", "1.0-beta-1-SNAPSHOT" );
383        assert_version_older( "1.0-SNAPSHOT-SNAPSHOT", "1.0-SNAPSHOT" );
384        assert_version_older( "1.0-alpha-1-SNAPSHOT-SNAPSHOT", "1.0-alpha-1-SNAPSHOT" );
385        assert_version_older( "2.0-1-SNAPSHOT", "2.0.1-SNAPSHOT" );
386        assert_version_older( "2.0.1-klm-SNAPSHOT", "2.0.1-lmn-SNAPSHOT" );
387    }    
388
389    #[test]
390    fn test_snapshot_releases() {
391         assert_version_older( "1.0-RC1", "1.0-SNAPSHOT" );
392    }
393
394    fn assert_version_older(left: &str, right: &str) {
395        assert!(new_artifact_version( left ).cmp( &new_artifact_version( right ) ) == Ordering::Less, "{} should be older than {}", left, right);
396        assert!(new_artifact_version( right ).cmp( &new_artifact_version( left ) ) == Ordering::Greater, "{} should be newer than {}", right, left);
397    }
398
399    fn assert_version_equal(left: &str, right: &str) {
400        assert!(new_artifact_version( left ).cmp( &new_artifact_version( right ) ) == Ordering::Equal, "{} should be equal to {}", left, right);
401        assert!(new_artifact_version( right ).cmp( &new_artifact_version( left ) ) == Ordering::Equal, "{} should be equal to {}", right, left);
402    }
403
404    fn new_artifact_version<'a>(version: &'a str) -> Maven2ArtifactVersion<'a> {
405        Maven2ArtifactVersion::from(version)
406    }
407}