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 (BigNumber(a), Number(b)) => a.compare(b)
case (Qualifier(_, a), Qualifier(_, b)) => a.compare(b) case (Qualifier(_, a), Qualifier(_, b)) => a.compare(b)
case (Literal(a), Literal(b)) => a.compareToIgnoreCase(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 _ => case _ =>
val rel0 = compareToEmpty val rel0 = compareToEmpty
@ -67,6 +71,10 @@ object Version {
val order = -1 val order = -1
override def compareToEmpty = if (value.isEmpty) 0 else 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 { case object Min extends Item {
val order = -8 val order = -8
@ -97,6 +105,7 @@ object Version {
case object Dot extends Separator case object Dot extends Separator
case object Hyphen extends Separator case object Hyphen extends Separator
case object Underscore extends Separator case object Underscore extends Separator
case object Plus extends Separator
case object None extends Separator case object None extends Separator
def apply(s: String): (Item, Stream[(Separator, Item)]) = { def apply(s: String): (Item, Stream[(Separator, Item)]) = {
@ -135,6 +144,7 @@ object Version {
case '.' => (Dot, s.tail) case '.' => (Dot, s.tail)
case '-' => (Hyphen, s.tail) case '-' => (Hyphen, s.tail)
case '_' => (Underscore, s.tail) case '_' => (Underscore, s.tail)
case '+' => (Plus, s.tail)
case _ => (None, s) case _ => (None, s)
} }
} }
@ -143,9 +153,13 @@ object Version {
if (s.isEmpty) Stream() if (s.isEmpty) Stream()
else { else {
val (sep, rem0) = parseSeparator(s) val (sep, rem0) = parseSeparator(s)
val (item, rem) = parseItem(rem0) sep match {
case Plus =>
(sep, item) #:: helper(rem) 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] = { def items(repr: String): List[Item] = {
val (first, tokens) = Tokenizer(repr) 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 postProcess(None, first, tokens).toList
} }

View File

@ -43,6 +43,40 @@ object VersionTests extends TestSuite {
assert(max == v241) 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 // 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) // Only one test doesn't pass (see FIXME below)