diff options
Diffstat (limited to 'semver-parser/src')
-rw-r--r-- | semver-parser/src/common.rs | 66 | ||||
-rw-r--r-- | semver-parser/src/lib.rs | 8 | ||||
-rw-r--r-- | semver-parser/src/range.rs | 696 | ||||
-rw-r--r-- | semver-parser/src/recognize.rs | 154 | ||||
-rw-r--r-- | semver-parser/src/version.rs | 365 |
5 files changed, 1289 insertions, 0 deletions
diff --git a/semver-parser/src/common.rs b/semver-parser/src/common.rs new file mode 100644 index 0000000..267b4d9 --- /dev/null +++ b/semver-parser/src/common.rs @@ -0,0 +1,66 @@ +use version::Identifier; +use recognize::{Recognize, Alt, OneOrMore, Inclusive, OneByte}; +use std::str::from_utf8; + +// by the time we get here, we know that it's all valid characters, so this doesn't need to return +// a result or anything +fn parse_meta(s: &str) -> Vec<Identifier> { + // Originally, I wanted to implement this method via calling parse, but parse is tolerant of + // leading zeroes, and we want anything with leading zeroes to be considered alphanumeric, not + // numeric. So the strategy is to check with a recognizer first, and then call parse once + // we've determined that it's a number without a leading zero. + s.split(".") + .map(|part| { + // another wrinkle: we made sure that any number starts with a + // non-zero. But there's a problem: an actual zero is a number, yet + // gets left out by this heuristic. So let's also check for the + // single, lone zero. + if is_alpha_numeric(part) { + Identifier::AlphaNumeric(part.to_string()) + } else { + // we can unwrap here because we know it is only digits due to the regex + Identifier::Numeric(part.parse().unwrap()) + } + }).collect() +} + +// parse optional metadata (preceded by the prefix character) +pub fn parse_optional_meta(s: &[u8], prefix_char: u8)-> Result<(Vec<Identifier>, usize), String> { + if let Some(len) = prefix_char.p(s) { + let start = len; + if let Some(len) = letters_numbers_dash_dot(&s[start..]) { + let end = start + len; + Ok((parse_meta(from_utf8(&s[start..end]).unwrap()), end)) + } else { + Err("Error parsing prerelease".to_string()) + } + } else { + Ok((Vec::new(), 0)) + } +} + +pub fn is_alpha_numeric(s: &str) -> bool { + if let Some((_val, len)) = numeric_identifier(s.as_bytes()) { + // Return true for number with leading zero + // Note: doing it this way also handily makes overflow fail over. + len != s.len() + } else { + true + } +} + +// Note: could plumb overflow error up to return value as Result +pub fn numeric_identifier(s: &[u8]) -> Option<(u64, usize)> { + if let Some(len) = Alt(b'0', OneOrMore(Inclusive(b'0'..b'9'))).p(s) { + from_utf8(&s[0..len]).unwrap().parse().ok().map(|val| (val, len)) + } else { + None + } +} + +pub fn letters_numbers_dash_dot(s: &[u8]) -> Option<usize> { + OneOrMore(OneByte(|c| c == b'-' || c == b'.' || + (b'0' <= c && c <= b'9') || + (b'a' <= c && c <= b'z') || + (b'A' <= c && c <= b'Z'))).p(s) +} diff --git a/semver-parser/src/lib.rs b/semver-parser/src/lib.rs new file mode 100644 index 0000000..3b0d8f0 --- /dev/null +++ b/semver-parser/src/lib.rs @@ -0,0 +1,8 @@ +pub mod version; +pub mod range; + +// for private stuff the two share +mod common; + +// for recognizer combinators +mod recognize; diff --git a/semver-parser/src/range.rs b/semver-parser/src/range.rs new file mode 100644 index 0000000..858be9f --- /dev/null +++ b/semver-parser/src/range.rs @@ -0,0 +1,696 @@ +use common::{self, numeric_identifier, letters_numbers_dash_dot}; +use version::Identifier; +use std::str::{FromStr, from_utf8}; +use recognize::*; + +#[derive(Debug)] +pub struct VersionReq { + pub predicates: Vec<Predicate>, +} + +#[derive(PartialEq,Debug)] +pub enum WildcardVersion { + Major, + Minor, + Patch, +} + +#[derive(PartialEq,Debug)] +pub enum Op { + Ex, // Exact + Gt, // Greater than + GtEq, // Greater than or equal to + Lt, // Less than + LtEq, // Less than or equal to + Tilde, // e.g. ~1.0.0 + Compatible, // compatible by definition of semver, indicated by ^ + Wildcard(WildcardVersion), // x.y.*, x.*, * +} + +impl FromStr for Op { + type Err = String; + + fn from_str(s: &str) -> Result<Op, String> { + match s { + "=" => Ok(Op::Ex), + ">" => Ok(Op::Gt), + ">=" => Ok(Op::GtEq), + "<" => Ok(Op::Lt), + "<=" => Ok(Op::LtEq), + "~" => Ok(Op::Tilde), + "^" => Ok(Op::Compatible), + _ => Err(String::from("Could not parse Op")), + } + } +} + +#[derive(PartialEq,Debug)] +pub struct Predicate { + pub op: Op, + pub major: u64, + pub minor: Option<u64>, + pub patch: Option<u64>, + pub pre: Vec<Identifier>, +} + +fn numeric_or_wild(s: &[u8]) -> Option<(Option<u64>, usize)> { + if let Some((val, len)) = numeric_identifier(s) { + Some((Some(val), len)) + } else if let Some(len) = OneOf(b"*xX").p(s) { + Some((None, len)) + } else { + None + } +} + +fn dot_numeric_or_wild(s: &[u8]) -> Option<(Option<u64>, usize)> { + b'.'.p(s).and_then(|len| + numeric_or_wild(&s[len..]).map(|(val, len2)| (val, len + len2)) + ) +} + +fn operation(s: &[u8]) -> Option<(Op, usize)> { + if let Some(len) = "=".p(s) { + Some((Op::Ex, len)) + } else if let Some(len) = ">=".p(s) { + Some((Op::GtEq, len)) + } else if let Some(len) = ">".p(s) { + Some((Op::Gt, len)) + } else if let Some(len) = "<=".p(s) { + Some((Op::LtEq, len)) + } else if let Some(len) = "<".p(s) { + Some((Op::Lt, len)) + } else if let Some(len) = "~".p(s) { + Some((Op::Tilde, len)) + } else if let Some(len) = "^".p(s) { + Some((Op::Compatible, len)) + } else { + None + } +} + +fn whitespace(s: &[u8]) -> Option<usize> { + ZeroOrMore(OneOf(b"\t\r\n ")).p(s) +} + +pub fn parse_predicate(range: &str) -> Result<Predicate, String> { + let s = range.trim().as_bytes(); + let mut i = 0; + let mut operation = if let Some((op, len)) = operation(&s[i..]) { + i += len; + op + } else { + // operations default to Compatible + Op::Compatible + }; + if let Some(len) = whitespace.p(&s[i..]) { + i += len; + } + let major = if let Some((major, len)) = numeric_identifier(&s[i..]) { + i += len; + major + } else { + return Err("Error parsing major version number: ".to_string()); + }; + let minor = if let Some((minor, len)) = dot_numeric_or_wild(&s[i..]) { + i += len; + if minor.is_none() { + operation = Op::Wildcard(WildcardVersion::Minor); + } + minor + } else { + None + }; + let patch = if let Some((patch, len)) = dot_numeric_or_wild(&s[i..]) { + i += len; + if patch.is_none() { + operation = Op::Wildcard(WildcardVersion::Patch); + } + patch + } else { + None + }; + let (pre, pre_len) = common::parse_optional_meta(&s[i..], b'-')?; + i += pre_len; + if let Some(len) = (b'+', letters_numbers_dash_dot).p(&s[i..]) { + i += len; + } + if i != s.len() { + return Err("Extra junk after valid predicate: ".to_string() + + from_utf8(&s[i..]).unwrap()); + } + Ok(Predicate { + op: operation, + major: major, + minor: minor, + patch: patch, + pre: pre, + }) +} + +pub fn parse(ranges: &str) -> Result<VersionReq, String> { + // null is an error + if ranges == "\0" { + return Err(String::from("Null is not a valid VersionReq")); + } + + // an empty range is a major version wildcard + // so is a lone * or x of either capitalization + if (ranges == "") + || (ranges == "*") + || (ranges == "x") + || (ranges == "X") { + return Ok(VersionReq { + predicates: vec![Predicate { + op: Op::Wildcard(WildcardVersion::Major), + major: 0, + minor: None, + patch: None, + pre: Vec::new(), + }], + }); + } + + + let ranges = ranges.trim(); + + let predicates: Result<Vec<_>, String> = ranges + .split(",") + .map(|range| { + parse_predicate(range) + }) + .collect(); + + let predicates = try!(predicates); + + if predicates.len() == 0 { + return Err(String::from("VersionReq did not parse properly")); + } + + Ok(VersionReq { + predicates: predicates, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use range; + use version::Identifier; + + #[test] + fn test_parsing_default() { + let r = range::parse("1.0.0").unwrap(); + + assert_eq!(Predicate { + op: Op::Compatible, + major: 1, + minor: Some(0), + patch: Some(0), + pre: Vec::new(), + }, + r.predicates[0] + ); + } + + #[test] + fn test_parsing_exact_01() { + let r = range::parse("=1.0.0").unwrap(); + + assert_eq!(Predicate { + op: Op::Ex, + major: 1, + minor: Some(0), + patch: Some(0), + pre: Vec::new(), + }, + r.predicates[0] + ); + } + + #[test] + fn test_parsing_exact_02() { + let r = range::parse("=0.9.0").unwrap(); + + assert_eq!(Predicate { + op: Op::Ex, + major: 0, + minor: Some(9), + patch: Some(0), + pre: Vec::new(), + }, + r.predicates[0] + ); + } + + #[test] + fn test_parsing_exact_03() { + let r = range::parse("=0.1.0-beta2.a").unwrap(); + + assert_eq!(Predicate { + op: Op::Ex, + major: 0, + minor: Some(1), + patch: Some(0), + pre: vec![Identifier::AlphaNumeric(String::from("beta2")), + Identifier::AlphaNumeric(String::from("a"))], + }, + r.predicates[0] + ); + } + + #[test] + pub fn test_parsing_greater_than() { + let r = range::parse("> 1.0.0").unwrap(); + + assert_eq!(Predicate { + op: Op::Gt, + major: 1, + minor: Some(0), + patch: Some(0), + pre: Vec::new(), + }, + r.predicates[0] + ); + } + + #[test] + pub fn test_parsing_greater_than_01() { + let r = range::parse(">= 1.0.0").unwrap(); + + assert_eq!(Predicate { + op: Op::GtEq, + major: 1, + minor: Some(0), + patch: Some(0), + pre: Vec::new(), + }, + r.predicates[0] + ); + } + + #[test] + pub fn test_parsing_greater_than_02() { + let r = range::parse(">= 2.1.0-alpha2").unwrap(); + + assert_eq!(Predicate { + op: Op::GtEq, + major: 2, + minor: Some(1), + patch: Some(0), + pre: vec![Identifier::AlphaNumeric(String::from("alpha2"))], + }, + r.predicates[0] + ); + } + + #[test] + pub fn test_parsing_less_than() { + let r = range::parse("< 1.0.0").unwrap(); + + assert_eq!(Predicate { + op: Op::Lt, + major: 1, + minor: Some(0), + patch: Some(0), + pre: Vec::new(), + }, + r.predicates[0] + ); + } + + #[test] + pub fn test_parsing_less_than_eq() { + let r = range::parse("<= 2.1.0-alpha2").unwrap(); + + assert_eq!(Predicate { + op: Op::LtEq, + major: 2, + minor: Some(1), + patch: Some(0), + pre: vec![Identifier::AlphaNumeric(String::from("alpha2"))], + }, + r.predicates[0] + ); + } + + #[test] + pub fn test_parsing_tilde() { + let r = range::parse("~1").unwrap(); + + assert_eq!(Predicate { + op: Op::Tilde, + major: 1, + minor: None, + patch: None, + pre: Vec::new(), + }, + r.predicates[0] + ); + } + + #[test] + pub fn test_parsing_compatible() { + let r = range::parse("^0").unwrap(); + + assert_eq!(Predicate { + op: Op::Compatible, + major: 0, + minor: None, + patch: None, + pre: Vec::new(), + }, + r.predicates[0] + ); + } + + #[test] + fn test_parsing_blank() { + let r = range::parse("").unwrap(); + + assert_eq!(Predicate { + op: Op::Wildcard(WildcardVersion::Major), + major: 0, + minor: None, + patch: None, + pre: Vec::new(), + }, + r.predicates[0] + ); + } + + #[test] + fn test_parsing_wildcard() { + let r = range::parse("*").unwrap(); + + assert_eq!(Predicate { + op: Op::Wildcard(WildcardVersion::Major), + major: 0, + minor: None, + patch: None, + pre: Vec::new(), + }, + r.predicates[0] + ); + } + + #[test] + fn test_parsing_x() { + let r = range::parse("x").unwrap(); + + assert_eq!(Predicate { + op: Op::Wildcard(WildcardVersion::Major), + major: 0, + minor: None, + patch: None, + pre: Vec::new(), + }, + r.predicates[0] + ); + } + + #[test] + fn test_parsing_capital_x() { + let r = range::parse("X").unwrap(); + + assert_eq!(Predicate { + op: Op::Wildcard(WildcardVersion::Major), + major: 0, + minor: None, + patch: None, + pre: Vec::new(), + }, + r.predicates[0] + ); + } + + #[test] + fn test_parsing_minor_wildcard_star() { + let r = range::parse("1.*").unwrap(); + + assert_eq!(Predicate { + op: Op::Wildcard(WildcardVersion::Minor), + major: 1, + minor: None, + patch: None, + pre: Vec::new(), + }, + r.predicates[0] + ); + } + + #[test] + fn test_parsing_minor_wildcard_x() { + let r = range::parse("1.x").unwrap(); + + assert_eq!(Predicate { + op: Op::Wildcard(WildcardVersion::Minor), + major: 1, + minor: None, + patch: None, + pre: Vec::new(), + }, + r.predicates[0] + ); + } + + #[test] + fn test_parsing_minor_wildcard_capital_x() { + let r = range::parse("1.X").unwrap(); + + assert_eq!(Predicate { + op: Op::Wildcard(WildcardVersion::Minor), + major: 1, + minor: None, + patch: None, + pre: Vec::new(), + }, + r.predicates[0] + ); + } + + #[test] + fn test_parsing_patch_wildcard_star() { + let r = range::parse("1.2.*").unwrap(); + + assert_eq!(Predicate { + op: Op::Wildcard(WildcardVersion::Patch), + major: 1, + minor: Some(2), + patch: None, + pre: Vec::new(), + }, + r.predicates[0] + ); + } + + #[test] + fn test_parsing_patch_wildcard_x() { + let r = range::parse("1.2.x").unwrap(); + + assert_eq!(Predicate { + op: Op::Wildcard(WildcardVersion::Patch), + major: 1, + minor: Some(2), + patch: None, + pre: Vec::new(), + }, + r.predicates[0] + ); + } + + #[test] + fn test_parsing_patch_wildcard_capital_x() { + let r = range::parse("1.2.X").unwrap(); + + assert_eq!(Predicate { + op: Op::Wildcard(WildcardVersion::Patch), + major: 1, + minor: Some(2), + patch: None, + pre: Vec::new(), + }, + r.predicates[0] + ); + } + + #[test] + pub fn test_multiple_01() { + let r = range::parse("> 0.0.9, <= 2.5.3").unwrap(); + + assert_eq!(Predicate { + op: Op::Gt, + major: 0, + minor: Some(0), + patch: Some(9), + pre: Vec::new(), + }, + r.predicates[0] + ); + + assert_eq!(Predicate { + op: Op::LtEq, + major: 2, + minor: Some(5), + patch: Some(3), + pre: Vec::new(), + }, + r.predicates[1] + ); + } + + #[test] + pub fn test_multiple_02() { + let r = range::parse("0.3.0, 0.4.0").unwrap(); + + assert_eq!(Predicate { + op: Op::Compatible, + major: 0, + minor: Some(3), + patch: Some(0), + pre: Vec::new(), + }, + r.predicates[0] + ); + + assert_eq!(Predicate { + op: Op::Compatible, + major: 0, + minor: Some(4), + patch: Some(0), + pre: Vec::new(), + }, + r.predicates[1] + ); + } + + #[test] + pub fn test_multiple_03() { + let r = range::parse("<= 0.2.0, >= 0.5.0").unwrap(); + + assert_eq!(Predicate { + op: Op::LtEq, + major: 0, + minor: Some(2), + patch: Some(0), + pre: Vec::new(), + }, + r.predicates[0] + ); + + assert_eq!(Predicate { + op: Op::GtEq, + major: 0, + minor: Some(5), + patch: Some(0), + pre: Vec::new(), + }, + r.predicates[1] + ); + } + + #[test] + pub fn test_multiple_04() { + let r = range::parse("0.1.0, 0.1.4, 0.1.6").unwrap(); + + assert_eq!(Predicate { + op: Op::Compatible, + major: 0, + minor: Some(1), + patch: Some(0), + pre: Vec::new(), + }, + r.predicates[0] + ); + + assert_eq!(Predicate { + op: Op::Compatible, + major: 0, + minor: Some(1), + patch: Some(4), + pre: Vec::new(), + }, + r.predicates[1] + ); + + assert_eq!(Predicate { + op: Op::Compatible, + major: 0, + minor: Some(1), + patch: Some(6), + pre: Vec::new(), + }, + r.predicates[2] + ); + } + + #[test] + pub fn test_multiple_05() { + let r = range::parse(">=0.5.1-alpha3, <0.6").unwrap(); + + assert_eq!(Predicate { + op: Op::GtEq, + major: 0, + minor: Some(5), + patch: Some(1), + pre: vec![Identifier::AlphaNumeric(String::from("alpha3"))], + }, + r.predicates[0] + ); + + assert_eq!(Predicate { + op: Op::Lt, + major: 0, + minor: Some(6), + patch: None, + pre: Vec::new(), + }, + r.predicates[1] + ); + } + + #[test] + fn test_parse_build_metadata_with_predicate() { + assert_eq!(range::parse("^1.2.3+meta").unwrap().predicates[0].op, + Op::Compatible); + assert_eq!(range::parse("~1.2.3+meta").unwrap().predicates[0].op, + Op::Tilde); + assert_eq!(range::parse("=1.2.3+meta").unwrap().predicates[0].op, + Op::Ex); + assert_eq!(range::parse("<=1.2.3+meta").unwrap().predicates[0].op, + Op::LtEq); + assert_eq!(range::parse(">=1.2.3+meta").unwrap().predicates[0].op, + Op::GtEq); + assert_eq!(range::parse("<1.2.3+meta").unwrap().predicates[0].op, + Op::Lt); + assert_eq!(range::parse(">1.2.3+meta").unwrap().predicates[0].op, + Op::Gt); + } + + #[test] + pub fn test_parse_errors() { + assert!(range::parse("\0").is_err()); + assert!(range::parse(">= >= 0.0.2").is_err()); + assert!(range::parse(">== 0.0.2").is_err()); + assert!(range::parse("a.0.0").is_err()); + assert!(range::parse("1.0.0-").is_err()); + assert!(range::parse(">=").is_err()); + assert!(range::parse("> 0.1.0,").is_err()); + assert!(range::parse("> 0.3.0, ,").is_err()); + } + + #[test] + pub fn test_large_major_version() { + assert!(range::parse("18446744073709551617.0.0").is_err()); + } + + #[test] + pub fn test_large_minor_version() { + assert!(range::parse("0.18446744073709551617.0").is_err()); + } + + #[test] + pub fn test_large_patch_version() { + assert!(range::parse("0.0.18446744073709551617").is_err()); + } +} diff --git a/semver-parser/src/recognize.rs b/semver-parser/src/recognize.rs new file mode 100644 index 0000000..c0dd771 --- /dev/null +++ b/semver-parser/src/recognize.rs @@ -0,0 +1,154 @@ +// Copyright 2017 Google Inc. All rights reserved. +// +// Licensed under either of MIT or Apache License, Version 2.0, +// at your option. +// +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Simple recognizer combinators. + +// This version is similar to a similar one in the "lang" module of +// xi-editor, but is stripped down to only the needed combinators. + +use std::ops; + +pub trait Recognize { + fn p(&self, s: &[u8]) -> Option<usize>; +} + +impl<F: Fn(&[u8]) -> Option<usize>> Recognize for F { + #[inline(always)] + fn p(&self, s: &[u8]) -> Option<usize> { + self(s) + } +} + +pub struct OneByte<F>(pub F); + +impl<F: Fn(u8) -> bool> Recognize for OneByte<F> { + #[inline(always)] + fn p(&self, s: &[u8]) -> Option<usize> { + if s.is_empty() || !self.0(s[0]) { + None + } else { + Some(1) + } + } +} + +impl Recognize for u8 { + #[inline(always)] + fn p(&self, s: &[u8]) -> Option<usize> { + OneByte(|b| b == *self).p(s) + } +} + +/// Use Inclusive(a..b) to indicate an inclusive range. When a...b syntax becomes +/// stable, we can get rid of this and switch to that. +pub struct Inclusive<T>(pub T); + +impl Recognize for Inclusive<ops::Range<u8>> { + #[inline(always)] + fn p(&self, s: &[u8]) -> Option<usize> { + OneByte(|x| x >= self.0.start && x <= self.0.end).p(s) + } +} + +impl<'a> Recognize for &'a [u8] { + #[inline(always)] + fn p(&self, s: &[u8]) -> Option<usize> { + let len = self.len(); + if s.len() >= len && &s[..len] == *self { + Some(len) + } else { + None + } + } +} + +impl<'a> Recognize for &'a str { + #[inline(always)] + fn p(&self, s: &[u8]) -> Option<usize> { + self.as_bytes().p(s) + } +} + +impl<P1: Recognize, P2: Recognize> Recognize for (P1, P2) { + #[inline(always)] + fn p(&self, s: &[u8]) -> Option<usize> { + self.0.p(s).and_then(|len1| + self.1.p(&s[len1..]).map(|len2| + len1 + len2)) + } +} + +/// Choice from two heterogeneous alternatives. +pub struct Alt<P1, P2>(pub P1, pub P2); + +impl<P1: Recognize, P2: Recognize> Recognize for Alt<P1, P2> { + #[inline(always)] + fn p(&self, s: &[u8]) -> Option<usize> { + self.0.p(s).or_else(|| self.1.p(s)) + } +} + +/// Choice from a homogenous slice of parsers. +pub struct OneOf<'a, P: 'a>(pub &'a [P]); + +impl<'a, P: Recognize> Recognize for OneOf<'a, P> { + #[inline] + fn p(&self, s: &[u8]) -> Option<usize> { + for ref p in self.0 { + if let Some(len) = p.p(s) { + return Some(len); + } + } + None + } +} + +pub struct OneOrMore<P>(pub P); + +impl<P: Recognize> Recognize for OneOrMore<P> { + #[inline] + fn p(&self, s: &[u8]) -> Option<usize> { + let mut i = 0; + let mut count = 0; + while let Some(len) = self.0.p(&s[i..]) { + i += len; + count += 1; + } + if count >= 1 { + Some(i) + } else { + None + } + } +} + +pub struct ZeroOrMore<P>(pub P); + +impl<P: Recognize> Recognize for ZeroOrMore<P> { + #[inline] + fn p(&self, s: &[u8]) -> Option<usize> { + let mut i = 0; + while let Some(len) = self.0.p(&s[i..]) { + i += len; + } + Some(i) + } +} diff --git a/semver-parser/src/version.rs b/semver-parser/src/version.rs new file mode 100644 index 0000000..570f947 --- /dev/null +++ b/semver-parser/src/version.rs @@ -0,0 +1,365 @@ +use std::fmt; +use std::str::from_utf8; + +use recognize::*; + +use common::{self, numeric_identifier}; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Version { + pub major: u64, + pub minor: u64, + pub patch: u64, + pub pre: Vec<Identifier>, + pub build: Vec<Identifier>, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Identifier { + /// An identifier that's solely numbers. + Numeric(u64), + /// An identifier with letters and numbers. + AlphaNumeric(String), +} + +pub fn parse(version: &str) -> Result<Version, String> { + let s = version.trim().as_bytes(); + let mut i = 0; + let major = if let Some((major, len)) = numeric_identifier(&s[i..]) { + i += len; + major + } else { + return Err("Error parsing major identifier".to_string()); + }; + if let Some(len) = b'.'.p(&s[i..]) { + i += len; + } else { + return Err("Expected dot".to_string()); + } + let minor = if let Some((minor, len)) = numeric_identifier(&s[i..]) { + i += len; + minor + } else { + return Err("Error parsing minor identifier".to_string()); + }; + if let Some(len) = b'.'.p(&s[i..]) { + i += len; + } else { + return Err("Expected dot".to_string()); + } + let patch = if let Some((patch, len)) = numeric_identifier(&s[i..]) { + i += len; + patch + } else { + return Err("Error parsing patch identifier".to_string()); + }; + let (pre, pre_len) = common::parse_optional_meta(&s[i..], b'-')?; + i += pre_len; + let (build, build_len) = common::parse_optional_meta(&s[i..], b'+')?; + i += build_len; + if i != s.len() { + return Err("Extra junk after valid version: ".to_string() + + from_utf8(&s[i..]).unwrap()); + } + Ok(Version { + major: major, + minor: minor, + patch: patch, + pre: pre, + build: build, + }) +} + +impl fmt::Display for Version { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + try!(write!(f, "{}.{}.{}", self.major, self.minor, self.patch)); + if !self.pre.is_empty() { + let strs: Vec<_> = + self.pre.iter().map(ToString::to_string).collect(); + try!(write!(f, "-{}", strs.join("."))); + } + if !self.build.is_empty() { + let strs: Vec<_> = + self.build.iter().map(ToString::to_string).collect(); + try!(write!(f, "+{}", strs.join("."))); + } + Ok(()) + } +} + +impl fmt::Display for Identifier { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + Identifier::Numeric(ref id) => id.fmt(f), + Identifier::AlphaNumeric(ref id) => id.fmt(f), + } + } +} + +#[cfg(test)] +mod tests { + use version; + use super::*; + + #[test] + fn parse_empty() { + let version = ""; + + let parsed = version::parse(version); + + assert!(parsed.is_err(), "empty string incorrectly considered a valid parse"); + } + + #[test] + fn parse_blank() { + let version = " "; + + let parsed = version::parse(version); + + assert!(parsed.is_err(), "blank string incorrectly considered a valid parse"); + } + + #[test] + fn parse_no_minor_patch() { + let version = "1"; + + let parsed = version::parse(version); + + assert!(parsed.is_err(), format!("'{}' incorrectly considered a valid parse", version)); + } + + #[test] + fn parse_no_patch() { + let version = "1.2"; + + let parsed = version::parse(version); + + assert!(parsed.is_err(), format!("'{}' incorrectly considered a valid parse", version)); + } + + #[test] + fn parse_empty_pre() { + let version = "1.2.3-"; + + let parsed = version::parse(version); + + assert!(parsed.is_err(), format!("'{}' incorrectly considered a valid parse", version)); + } + + #[test] + fn parse_letters() { + let version = "a.b.c"; + + let parsed = version::parse(version); + + assert!(parsed.is_err(), format!("'{}' incorrectly considered a valid parse", version)); + } + + #[test] + fn parse_with_letters() { + let version = "1.2.3 a.b.c"; + + let parsed = version::parse(version); + + assert!(parsed.is_err(), format!("'{}' incorrectly considered a valid parse", version)); + } + + #[test] + fn parse_basic_version() { + let version = "1.2.3"; + + let parsed = version::parse(version).unwrap(); + + assert_eq!(1, parsed.major); + assert_eq!(2, parsed.minor); + assert_eq!(3, parsed.patch); + } + + #[test] + fn parse_trims_input() { + let version = " 1.2.3 "; + + let parsed = version::parse(version).unwrap(); + + assert_eq!(1, parsed.major); + assert_eq!(2, parsed.minor); + assert_eq!(3, parsed.patch); + } + + #[test] + fn parse_no_major_leading_zeroes() { + let version = "01.0.0"; + + let parsed = version::parse(version); + + assert!(parsed.is_err(), "01 incorrectly considered a valid major version"); + } + + #[test] + fn parse_no_minor_leading_zeroes() { + let version = "0.01.0"; + + let parsed = version::parse(version); + + assert!(parsed.is_err(), "01 incorrectly considered a valid minor version"); + } + + #[test] + fn parse_no_patch_leading_zeroes() { + let version = "0.0.01"; + + let parsed = version::parse(version); + + assert!(parsed.is_err(), "01 incorrectly considered a valid patch version"); + } + + #[test] + fn parse_no_major_overflow() { + let version = "98765432109876543210.0.0"; + + let parsed = version::parse(version); + + assert!(parsed.is_err(), "98765432109876543210 incorrectly considered a valid major version"); + } + + #[test] + fn parse_no_minor_overflow() { + let version = "0.98765432109876543210.0"; + + let parsed = version::parse(version); + + assert!(parsed.is_err(), "98765432109876543210 incorrectly considered a valid minor version"); + } + + #[test] + fn parse_no_patch_overflow() { + let version = "0.0.98765432109876543210"; + + let parsed = version::parse(version); + + assert!(parsed.is_err(), "98765432109876543210 incorrectly considered a valid patch version"); + } + + #[test] + fn parse_basic_prerelease() { + let version = "1.2.3-pre"; + + let parsed = version::parse(version).unwrap(); + + let expected_pre = vec![Identifier::AlphaNumeric(String::from("pre"))]; + assert_eq!(expected_pre, parsed.pre); + } + + #[test] + fn parse_prerelease_alphanumeric() { + let version = "1.2.3-alpha1"; + + let parsed = version::parse(version).unwrap(); + + let expected_pre = vec![Identifier::AlphaNumeric(String::from("alpha1"))]; + assert_eq!(expected_pre, parsed.pre); + } + + #[test] + fn parse_prerelease_zero() { + let version = "1.2.3-pre.0"; + + let parsed = version::parse(version).unwrap(); + + let expected_pre = vec![Identifier::AlphaNumeric(String::from("pre")), + Identifier::Numeric(0)]; + assert_eq!(expected_pre, parsed.pre); + } + + #[test] + fn parse_basic_build() { + let version = "1.2.3+build"; + + let parsed = version::parse(version).unwrap(); + + let expected_build = vec![Identifier::AlphaNumeric(String::from("build"))]; + assert_eq!(expected_build, parsed.build); + } + + #[test] + fn parse_build_alphanumeric() { + let version = "1.2.3+build5"; + + let parsed = version::parse(version).unwrap(); + + let expected_build = vec![Identifier::AlphaNumeric(String::from("build5"))]; + assert_eq!(expected_build, parsed.build); + } + + #[test] + fn parse_pre_and_build() { + let version = "1.2.3-alpha1+build5"; + + let parsed = version::parse(version).unwrap(); + + let expected_pre = vec![Identifier::AlphaNumeric(String::from("alpha1"))]; + assert_eq!(expected_pre, parsed.pre); + + let expected_build = vec![Identifier::AlphaNumeric(String::from("build5"))]; + assert_eq!(expected_build, parsed.build); + } + + #[test] + fn parse_complex_metadata_01() { + let version = "1.2.3-1.alpha1.9+build5.7.3aedf "; + + let parsed = version::parse(version).unwrap(); + + let expected_pre = vec![Identifier::Numeric(1), + Identifier::AlphaNumeric(String::from("alpha1")), + Identifier::Numeric(9)]; + assert_eq!(expected_pre, parsed.pre); + + let expected_build = vec![Identifier::AlphaNumeric(String::from("build5")), + Identifier::Numeric(7), + Identifier::AlphaNumeric(String::from("3aedf"))]; + assert_eq!(expected_build, parsed.build); + } + + #[test] + fn parse_complex_metadata_02() { + let version = "0.4.0-beta.1+0851523"; + + let parsed = version::parse(version).unwrap(); + + let expected_pre = vec![Identifier::AlphaNumeric(String::from("beta")), + Identifier::Numeric(1)]; + assert_eq!(expected_pre, parsed.pre); + + let expected_build = vec![Identifier::AlphaNumeric(String::from("0851523"))]; + assert_eq!(expected_build, parsed.build); + } + + #[test] + fn parse_metadata_overflow() { + let version = "0.4.0-beta.1+98765432109876543210"; + + let parsed = version::parse(version).unwrap(); + + let expected_pre = vec![Identifier::AlphaNumeric(String::from("beta")), + Identifier::Numeric(1)]; + assert_eq!(expected_pre, parsed.pre); + + let expected_build = vec![Identifier::AlphaNumeric(String::from("98765432109876543210"))]; + assert_eq!(expected_build, parsed.build); + } + + #[test] + fn parse_regression_01() { + let version = "0.0.0-WIP"; + + let parsed = version::parse(version).unwrap(); + + assert_eq!(0, parsed.major); + assert_eq!(0, parsed.minor); + assert_eq!(0, parsed.patch); + + let expected_pre = vec![Identifier::AlphaNumeric(String::from("WIP"))]; + assert_eq!(expected_pre, parsed.pre); + } +} |