mirror of https://github.com/sbt/sbt.git
pseudo-case class VersionNumber
VersionNumber is a pseudo-case class that represents any form of version number. The unapply extractor can parse String into three sequences that makes up VersionNumber. VersionNumberCompatibility trait uses two VersionNumber instances to evaluate binary compatibility between them. Two implementations SemVer and SecondSegment are provided.
This commit is contained in:
parent
3fe8f41a69
commit
439f8498b6
|
|
@ -0,0 +1,147 @@
|
|||
package sbt
|
||||
|
||||
final class VersionNumber private[sbt] (
|
||||
val numbers: Seq[Long],
|
||||
val tags: Seq[String],
|
||||
val extras: Seq[String]) {
|
||||
def _1: Option[Long] = get(0)
|
||||
def _2: Option[Long] = get(1)
|
||||
def _3: Option[Long] = get(2)
|
||||
def _4: Option[Long] = get(3)
|
||||
def get(idx: Int): Option[Long] =
|
||||
if (size <= idx) None
|
||||
else Some(numbers(idx))
|
||||
def size: Int = numbers.size
|
||||
|
||||
private[this] val versionStr: String =
|
||||
numbers.mkString(".") +
|
||||
(tags match {
|
||||
case Seq() => ""
|
||||
case ts => "-" + ts.mkString("-")
|
||||
}) +
|
||||
extras.mkString("")
|
||||
override def toString: String = versionStr
|
||||
override def hashCode: Int =
|
||||
numbers.hashCode * 41 * 41 +
|
||||
tags.hashCode * 41 +
|
||||
extras.hashCode
|
||||
override def equals(o: Any): Boolean =
|
||||
o match {
|
||||
case v: VersionNumber => (this.numbers == v.numbers) && (this.tags == v.tags) && (this.extras == v.extras)
|
||||
case _ => false
|
||||
}
|
||||
}
|
||||
|
||||
object VersionNumber {
|
||||
/**
|
||||
* @param numbers numbers delimited by a dot.
|
||||
* @param tags string prefixed by a dash.
|
||||
* @param any other strings at the end.
|
||||
*/
|
||||
def apply(numbers: Seq[Long], tags: Seq[String], extras: Seq[String]): VersionNumber =
|
||||
new VersionNumber(numbers, tags, extras)
|
||||
def apply(v: String): VersionNumber =
|
||||
unapply(v) match {
|
||||
case Some((ns, ts, es)) => VersionNumber(ns, ts, es)
|
||||
case _ => sys.error(s"Invalid version number: $v")
|
||||
}
|
||||
|
||||
def unapply(v: VersionNumber): Option[(Seq[Long], Seq[String], Seq[String])] =
|
||||
Some((v.numbers, v.tags, v.extras))
|
||||
|
||||
def unapply(v: String): Option[(Seq[Long], Seq[String], Seq[String])] = {
|
||||
def splitDot(s: String): Vector[Long] =
|
||||
Option(s) match {
|
||||
case Some(x) => x.split('.').toVector.filterNot(_ == "").map(_.toLong)
|
||||
case _ => Vector()
|
||||
}
|
||||
def splitDash(s: String): Vector[String] =
|
||||
Option(s) match {
|
||||
case Some(x) => x.split('-').toVector.filterNot(_ == "")
|
||||
case _ => Vector()
|
||||
}
|
||||
def splitPlus(s: String): Vector[String] =
|
||||
Option(s) match {
|
||||
case Some(x) => x.split('+').toVector.filterNot(_ == "").map("+" + _)
|
||||
case _ => Vector()
|
||||
}
|
||||
val TaggedVersion = """(\d{1,14})([\.\d{1,14}]*)((?:-\w+)*)((?:\+.+)*)""".r
|
||||
val NonSpaceString = """(\S+)""".r
|
||||
v match {
|
||||
case TaggedVersion(m, ns, ts, es) => Some((Vector(m.toLong) ++ splitDot(ns), splitDash(ts), splitPlus(es)))
|
||||
case "" => None
|
||||
case NonSpaceString(s) => Some((Vector(), Vector(), Vector(s)))
|
||||
case _ => None
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Strict. Checks everythig.
|
||||
*/
|
||||
object Strict extends VersionNumberCompatibility {
|
||||
def name: String = "Strict"
|
||||
def isCompatible(v1: VersionNumber, v2: VersionNumber): Boolean = v1 == v2
|
||||
}
|
||||
|
||||
/**
|
||||
* Semantic versioning. See http://semver.org/spec/v2.0.0.html
|
||||
*/
|
||||
object SemVer extends VersionNumberCompatibility {
|
||||
def name: String = "Semantic Versioning"
|
||||
def isCompatible(v1: VersionNumber, v2: VersionNumber): Boolean =
|
||||
doIsCompat(v1, v2) || doIsCompat(v2, v1)
|
||||
private[this] def doIsCompat(v1: VersionNumber, v2: VersionNumber): Boolean =
|
||||
(v1, v2) match {
|
||||
case (v1, v2) if (v1.size >= 2) && (v2.size >= 2) => // A normal version number MUST take the form X.Y.Z
|
||||
(v1._1.get, v1._2.get, v1._3.getOrElse(0), v1.tags, v2._1.get, v2._2.get, v2._3.getOrElse(0), v2.tags) match {
|
||||
case (0L, _, _, _, 0L, _, _, _) =>
|
||||
// Major version zero (0.y.z) is for initial development. Anything may change at any time. The public API should not be considered stable.
|
||||
equalsIgnoreExtra(v1, v2)
|
||||
case (_, 0, 0, ts1, _, 0, 0, ts2) if (!ts1.isEmpty) || (!ts2.isEmpty) =>
|
||||
// A pre-release version MAY be denoted by appending a hyphen and a series of dot separated identifiers
|
||||
equalsIgnoreExtra(v1, v2)
|
||||
case (x1, _, _, _, x2, _, _, _) =>
|
||||
// Patch version Z (x.y.Z | x > 0) MUST be incremented if only backwards compatible bug fixes are introduced.
|
||||
// Minor version Y (x.Y.z | x > 0) MUST be incremented if new, backwards compatible functionality is introduced
|
||||
x1 == x2
|
||||
case _ => equalsIgnoreExtra(v1, v2)
|
||||
}
|
||||
case _ => false
|
||||
}
|
||||
// Build metadata SHOULD be ignored when determining version precedence.
|
||||
private[this] def equalsIgnoreExtra(v1: VersionNumber, v2: VersionNumber): Boolean =
|
||||
(v1.numbers == v2.numbers) && (v1.tags == v2.tags)
|
||||
}
|
||||
|
||||
/* A variant of SemVar that seems to be common among the Scala libraries.
|
||||
* The second segment (y in x.y.z) increments breaks the binary compatibility even when x > 0.
|
||||
* Also API comatibility is expected even when the first segment is zero.
|
||||
*/
|
||||
object SecondSegment extends VersionNumberCompatibility {
|
||||
def name: String = "Second Segment Variant"
|
||||
def isCompatible(v1: VersionNumber, v2: VersionNumber): Boolean =
|
||||
doIsCompat(v1, v2) || doIsCompat(v2, v1)
|
||||
private[this] def doIsCompat(v1: VersionNumber, v2: VersionNumber): Boolean =
|
||||
(v1, v2) match {
|
||||
case (v1, v2) if (v1.size >= 3) && (v2.size >= 3) => // A normal version number MUST take the form X.Y.Z
|
||||
(v1._1.get, v1._2.get, v1._3.get, v1.tags, v2._1.get, v2._2.get, v2._3.get, v2.tags) match {
|
||||
case (x1, y1, 0, ts1, x2, y2, 0, ts2) if (!ts1.isEmpty) || (!ts2.isEmpty) =>
|
||||
// A pre-release version MAY be denoted by appending a hyphen and a series of dot separated identifiers
|
||||
equalsIgnoreExtra(v1, v2)
|
||||
case (x1, y1, _, _, x2, y2, _, _) =>
|
||||
// Patch version Z (x.y.Z | x > 0) MUST be incremented if only backwards compatible changes are introduced.
|
||||
(x1 == x2) && (y1 == y2)
|
||||
case _ => equalsIgnoreExtra(v1, v2)
|
||||
}
|
||||
case _ => false
|
||||
}
|
||||
// Build metadata SHOULD be ignored when determining version precedence.
|
||||
private[this] def equalsIgnoreExtra(v1: VersionNumber, v2: VersionNumber): Boolean =
|
||||
(v1.numbers == v2.numbers) && (v1.tags == v2.tags)
|
||||
}
|
||||
}
|
||||
|
||||
trait VersionNumberCompatibility {
|
||||
def name: String
|
||||
def isCompatible(v1: VersionNumber, v2: VersionNumber): Boolean
|
||||
}
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
package sbt
|
||||
|
||||
import org.specs2._
|
||||
|
||||
class VersionNumberSpec extends Specification {
|
||||
def is = s2"""
|
||||
|
||||
This is a specification to check the version number parsing.
|
||||
|
||||
1 should
|
||||
${beParsedAs("1", Seq(1), Seq(), Seq())}
|
||||
${breakDownTo("1", Some(1))}
|
||||
|
||||
1.0 should
|
||||
${beParsedAs("1.0", Seq(1, 0), Seq(), Seq())}
|
||||
${breakDownTo("1.0", Some(1), Some(0))}
|
||||
|
||||
1.0.0 should
|
||||
${beParsedAs("1.0.0", Seq(1, 0, 0), Seq(), Seq())}
|
||||
${breakDownTo("1.0.0", Some(1), Some(0), Some(0))}
|
||||
|
||||
${beSemVerCompatWith("1.0.0", "1.0.1")}
|
||||
${beSemVerCompatWith("1.0.0", "1.1.1")}
|
||||
${notBeSemVerCompatWith("1.0.0", "2.0.0")}
|
||||
${notBeSemVerCompatWith("1.0.0", "1.0.0-M1")}
|
||||
|
||||
${beSecSegCompatWith("1.0.0", "1.0.1")}
|
||||
${notBeSecSegCompatWith("1.0.0", "1.1.1")}
|
||||
${notBeSecSegCompatWith("1.0.0", "2.0.0")}
|
||||
${notBeSecSegCompatWith("1.0.0", "1.0.0-M1")}
|
||||
|
||||
1.0.0.0 should
|
||||
${beParsedAs("1.0.0.0", Seq(1, 0, 0, 0), Seq(), Seq())}
|
||||
${breakDownTo("1.0.0.0", Some(1), Some(0), Some(0), Some(0))}
|
||||
|
||||
0.12.0 should
|
||||
${beParsedAs("0.12.0", Seq(0, 12, 0), Seq(), Seq())}
|
||||
${breakDownTo("0.12.0", Some(0), Some(12), Some(0))}
|
||||
|
||||
${notBeSemVerCompatWith("0.12.0", "0.12.0-RC1")}
|
||||
${notBeSemVerCompatWith("0.12.0", "0.12.1")}
|
||||
${notBeSemVerCompatWith("0.12.0", "0.12.1-M1")}
|
||||
|
||||
${notBeSecSegCompatWith("0.12.0", "0.12.0-RC1")}
|
||||
${beSecSegCompatWith("0.12.0", "0.12.1")}
|
||||
${beSecSegCompatWith("0.12.0", "0.12.1-M1")}
|
||||
|
||||
0.1.0-SNAPSHOT should
|
||||
${beParsedAs("0.1.0-SNAPSHOT", Seq(0, 1, 0), Seq("SNAPSHOT"), Seq())}
|
||||
|
||||
${beSemVerCompatWith("0.1.0-SNAPSHOT", "0.1.0-SNAPSHOT")}
|
||||
${notBeSemVerCompatWith("0.1.0-SNAPSHOT", "0.1.0")}
|
||||
${beSemVerCompatWith("0.1.0-SNAPSHOT", "0.1.0-SNAPSHOT+001")}
|
||||
|
||||
${beSecSegCompatWith("0.1.0-SNAPSHOT", "0.1.0-SNAPSHOT")}
|
||||
${notBeSecSegCompatWith("0.1.0-SNAPSHOT", "0.1.0")}
|
||||
${beSecSegCompatWith("0.1.0-SNAPSHOT", "0.1.0-SNAPSHOT+001")}
|
||||
|
||||
0.1.0-M1 should
|
||||
${beParsedAs("0.1.0-M1", Seq(0, 1, 0), Seq("M1"), Seq())}
|
||||
|
||||
0.1.0-RC1 should
|
||||
${beParsedAs("0.1.0-RC1", Seq(0, 1, 0), Seq("RC1"), Seq())}
|
||||
|
||||
0.1.0-MSERVER-1 should
|
||||
${beParsedAs("0.1.0-MSERVER-1", Seq(0, 1, 0), Seq("MSERVER", "1"), Seq())}
|
||||
|
||||
2.10.4-20140115-000117-b3a-sources should
|
||||
${beParsedAs("2.10.4-20140115-000117-b3a-sources", Seq(2, 10, 4), Seq("20140115", "000117", "b3a", "sources"), Seq())}
|
||||
|
||||
${beSemVerCompatWith("2.10.4-20140115-000117-b3a-sources", "2.0.0")}
|
||||
|
||||
${notBeSecSegCompatWith("2.10.4-20140115-000117-b3a-sources", "2.0.0")}
|
||||
|
||||
20140115000117-b3a-sources should
|
||||
${beParsedAs("20140115000117-b3a-sources", Seq(20140115000117L), Seq("b3a", "sources"), Seq())}
|
||||
|
||||
1.0.0-alpha+001+002 should
|
||||
${beParsedAs("1.0.0-alpha+001+002", Seq(1, 0, 0), Seq("alpha"), Seq("+001", "+002"))}
|
||||
|
||||
non.space.!?string should
|
||||
${beParsedAs("non.space.!?string", Seq(), Seq(), Seq("non.space.!?string"))}
|
||||
|
||||
space !?string should
|
||||
${beParsedAsError("space !?string")}
|
||||
|
||||
blank string should
|
||||
${beParsedAsError("")}
|
||||
"""
|
||||
|
||||
def beParsedAs(s: String, ns: Seq[Long], ts: Seq[String], es: Seq[String]) =
|
||||
s match {
|
||||
case VersionNumber(ns1, ts1, es1) if (ns1 == ns && ts1 == ts && es1 == es) =>
|
||||
(VersionNumber(ns, ts, es).toString must_== s) and
|
||||
(VersionNumber(ns, ts, es) == VersionNumber(ns, ts, es))
|
||||
case VersionNumber(ns1, ts1, es1) =>
|
||||
sys.error(s"$ns1, $ts1, $es1")
|
||||
}
|
||||
def breakDownTo(s: String, major: Option[Long], minor: Option[Long] = None,
|
||||
patch: Option[Long] = None, buildNumber: Option[Long] = None) =
|
||||
s match {
|
||||
case VersionNumber(ns, ts, es) =>
|
||||
val v = VersionNumber(ns, ts, es)
|
||||
(v._1 must_== major) and
|
||||
(v._2 must_== minor) and
|
||||
(v._3 must_== patch) and
|
||||
(v._4 must_== buildNumber)
|
||||
}
|
||||
def beParsedAsError(s: String) =
|
||||
s match {
|
||||
case VersionNumber(ns1, ts1, es1) => failure
|
||||
case _ => success
|
||||
}
|
||||
def beSemVerCompatWith(v1: String, v2: String) =
|
||||
VersionNumber.SemVer.isCompatible(VersionNumber(v1), VersionNumber(v2)) must_== true
|
||||
def notBeSemVerCompatWith(v1: String, v2: String) =
|
||||
VersionNumber.SemVer.isCompatible(VersionNumber(v1), VersionNumber(v2)) must_== false
|
||||
def beSecSegCompatWith(v1: String, v2: String) =
|
||||
VersionNumber.SecondSegment.isCompatible(VersionNumber(v1), VersionNumber(v2)) must_== true
|
||||
def notBeSecSegCompatWith(v1: String, v2: String) =
|
||||
VersionNumber.SecondSegment.isCompatible(VersionNumber(v1), VersionNumber(v2)) must_== false
|
||||
}
|
||||
Loading…
Reference in New Issue