From dba6225ac143a57cc1af2ef1120238daf88d944f Mon Sep 17 00:00:00 2001 From: Alexandre Archambault Date: Fri, 21 Apr 2017 14:52:19 +0200 Subject: [PATCH] Add support for build metadata in versions --- .../main/scala/coursier/core/Version.scala | 104 ++++++++++-------- .../scala/coursier/test/VersionTests.scala | 34 ++++++ 2 files changed, 93 insertions(+), 45 deletions(-) diff --git a/core/shared/src/main/scala/coursier/core/Version.scala b/core/shared/src/main/scala/coursier/core/Version.scala index 08b2c3652..de1df7198 100644 --- a/core/shared/src/main/scala/coursier/core/Version.scala +++ b/core/shared/src/main/scala/coursier/core/Version.scala @@ -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 } diff --git a/tests/shared/src/test/scala/coursier/test/VersionTests.scala b/tests/shared/src/test/scala/coursier/test/VersionTests.scala index 14016fdae..c94225959 100644 --- a/tests/shared/src/test/scala/coursier/test/VersionTests.scala +++ b/tests/shared/src/test/scala/coursier/test/VersionTests.scala @@ -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)