Add support for build metadata in versions

This commit is contained in:
Alexandre Archambault 2017-04-21 14:52:19 +02:00
parent 876129a605
commit dba6225ac1
2 changed files with 93 additions and 45 deletions

View File

@ -29,6 +29,10 @@ object Version {
case (BigNumber(a), Number(b)) => a.compare(b)
case (Qualifier(_, a), Qualifier(_, b)) => a.compare(b)
case (Literal(a), Literal(b)) => a.compareToIgnoreCase(b)
case (BuildMetadata(_), BuildMetadata(_)) =>
// Semver § 10: two versions that differ only in the build metadata, have the same precedence.
// Might introduce some non-determinism though :-/
0
case _ =>
val rel0 = compareToEmpty
@ -67,6 +71,10 @@ object Version {
val order = -1
override def compareToEmpty = if (value.isEmpty) 0 else 1
}
final case class BuildMetadata(value: String) extends Item {
val order = 1
override def compareToEmpty = if (value.isEmpty) 0 else 1
}
case object Min extends Item {
val order = -8
@ -97,6 +105,7 @@ object Version {
case object Dot extends Separator
case object Hyphen extends Separator
case object Underscore extends Separator
case object Plus extends Separator
case object None extends Separator
def apply(s: String): (Item, Stream[(Separator, Item)]) = {
@ -135,6 +144,7 @@ object Version {
case '.' => (Dot, s.tail)
case '-' => (Hyphen, s.tail)
case '_' => (Underscore, s.tail)
case '+' => (Plus, s.tail)
case _ => (None, s)
}
}
@ -143,9 +153,13 @@ object Version {
if (s.isEmpty) Stream()
else {
val (sep, rem0) = parseSeparator(s)
val (item, rem) = parseItem(rem0)
(sep, item) #:: helper(rem)
sep match {
case Plus =>
Stream((sep, BuildMetadata(rem0.mkString)))
case _ =>
val (item, rem) = parseItem(rem0)
(sep, item) #:: helper(rem)
}
}
}
@ -154,51 +168,51 @@ object Version {
}
}
def postProcess(prevIsNumeric: Option[Boolean], item: Item, tokens0: Stream[(Tokenizer.Separator, Item)]): Stream[Item] = {
val tokens = {
var _tokens = tokens0
if (isNumeric(item)) {
val nextNonDotZero = _tokens.dropWhile{case (Tokenizer.Dot, n: Numeric) => n.isEmpty; case _ => false }
if (nextNonDotZero.forall(t => t._1 == Tokenizer.Hyphen || ((t._1 == Tokenizer.Dot || t._1 == Tokenizer.None) && !isNumeric(t._2)))) { // Dot && isNumeric(t._2)
_tokens = nextNonDotZero
}
}
_tokens
}
def ifFollowedByNumberElse(ifFollowedByNumber: Item, default: Item) = {
val followedByNumber = tokens.headOption
.exists{ case (Tokenizer.None, num: Numeric) if !num.isEmpty => true; case _ => false }
if (followedByNumber) ifFollowedByNumber
else default
}
def next =
if (tokens.isEmpty) Stream()
else postProcess(Some(isNumeric(item)), tokens.head._2, tokens.tail)
item match {
case Literal("min") => Min #:: next
case Literal("max") => Max #:: next
case Literal("a") =>
ifFollowedByNumberElse(qualifiersMap("alpha"), item) #:: next
case Literal("b") =>
ifFollowedByNumberElse(qualifiersMap("beta"), item) #:: next
case Literal("m") =>
ifFollowedByNumberElse(qualifiersMap("milestone"), item) #:: next
case _ =>
item #:: next
}
}
def isNumeric(item: Item) = item match { case _: Numeric => true; case _ => false }
def items(repr: String): List[Item] = {
val (first, tokens) = Tokenizer(repr)
def isNumeric(item: Item) = item match { case _: Numeric => true; case _ => false }
def postProcess(prevIsNumeric: Option[Boolean], item: Item, tokens0: Stream[(Tokenizer.Separator, Item)]): Stream[Item] = {
val tokens = {
var _tokens = tokens0
if (isNumeric(item)) {
val nextNonDotZero = _tokens.dropWhile{case (Tokenizer.Dot, n: Numeric) => n.isEmpty; case _ => false }
if (nextNonDotZero.forall(t => t._1 == Tokenizer.Hyphen || ((t._1 == Tokenizer.Dot || t._1 == Tokenizer.None) && !isNumeric(t._2)))) { // Dot && isNumeric(t._2)
_tokens = nextNonDotZero
}
}
_tokens
}
def ifFollowedByNumberElse(ifFollowedByNumber: Item, default: Item) = {
val followedByNumber = tokens.headOption
.exists{ case (Tokenizer.None, num: Numeric) if !num.isEmpty => true; case _ => false }
if (followedByNumber) ifFollowedByNumber
else default
}
def next =
if (tokens.isEmpty) Stream()
else postProcess(Some(isNumeric(item)), tokens.head._2, tokens.tail)
item match {
case Literal("min") => Min #:: next
case Literal("max") => Max #:: next
case Literal("a") =>
ifFollowedByNumberElse(qualifiersMap("alpha"), item) #:: next
case Literal("b") =>
ifFollowedByNumberElse(qualifiersMap("beta"), item) #:: next
case Literal("m") =>
ifFollowedByNumberElse(qualifiersMap("milestone"), item) #:: next
case _ =>
item #:: next
}
}
postProcess(None, first, tokens).toList
}

View File

@ -43,6 +43,40 @@ object VersionTests extends TestSuite {
assert(max == v241)
}
'buildMetadata - {
* - {
assert(compare("1.2", "1.2+foo") < 0)
// Semver § 10: two versions that differ only in the build metadata, have the same precedence
assert(compare("1.2+bar", "1.2+foo") == 0)
assert(compare("1.2+bar.1", "1.2+bar.2") == 0)
}
'shouldNotParseMetadata - {
* - {
val items = Version("1.2+bar.2").items
val expectedItems = Seq(
Version.Number(1), Version.Number(2), Version.BuildMetadata("bar.2")
)
assert(items == expectedItems)
}
* - {
val items = Version("1.2+bar-2").items
val expectedItems = Seq(
Version.Number(1), Version.Number(2), Version.BuildMetadata("bar-2")
)
assert(items == expectedItems)
}
* - {
val items = Version("1.2+bar+foo").items
val expectedItems = Seq(
Version.Number(1), Version.Number(2), Version.BuildMetadata("bar+foo")
)
assert(items == expectedItems)
}
}
}
// Adapted from aether-core/aether-util/src/test/java/org/eclipse/aether/util/version/GenericVersionTest.java
// Only one test doesn't pass (see FIXME below)