Cleanup & tweak VersionNumber

This commit is contained in:
Dale Wijnand 2018-03-15 01:45:45 +00:00
parent c6b2b626c8
commit 0f93849214
No known key found for this signature in database
GPG Key ID: 4F256E3D151DF5EF
2 changed files with 131 additions and 102 deletions

View File

@ -5,38 +5,31 @@ final class VersionNumber private[sbt] (
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 get(idx: Int): Option[Long] = numbers lift idx
def size: Int = numbers.size
/** The vector of version numbers from more to less specific from this version number. */
lazy val cascadingVersions: Vector[VersionNumber] =
(Vector(this) ++
(numbers.inits filter (_.length >= 2) map (VersionNumber(_, Nil, Nil)))).distinct
(Vector(this) ++ (numbers.inits filter (_.size >= 2) map (VersionNumber(_, Nil, Nil)))).distinct
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
}
override val toString: String =
numbers.mkString(".") + mkString1(tags, "-", "-", "") + extras.mkString("")
override def hashCode: Int = numbers.## * 41 * 41 + tags.## * 41 + extras.##
override def equals(that: Any): Boolean = that match {
case v: VersionNumber => (numbers == v.numbers) && (tags == v.tags) && (extras == v.extras)
case _ => false
}
/** A variant of mkString that returns the empty string if the sequence is empty. */
private[this] def mkString1[A](xs: Seq[A], start: String, sep: String, end: String): String =
if (xs.isEmpty) "" else xs.mkString(start, sep, end)
}
object VersionNumber {
@ -44,120 +37,156 @@ object VersionNumber {
/**
* @param numbers numbers delimited by a dot.
* @param tags string prefixed by a dash.
* @param any other strings at the end.
* @param extras 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 {
def apply(s: String): VersionNumber =
unapply(s) match {
case Some((ns, ts, es)) => VersionNumber(ns, ts, es)
case _ => sys.error(s"Invalid version number: $v")
case _ => throw new IllegalArgumentException(s"Invalid version number: $s")
}
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()
}
def unapply(s: String): Option[(Seq[Long], Seq[String], Seq[String])] = {
// null safe, empty string safe
def splitOn[A](s: String, sep: Char): Vector[String] =
if (s eq null) Vector()
else s.split(sep).filterNot(_ == "").toVector
def splitDot(s: String) = splitOn(s, '.') map (_.toLong)
def splitDash(s: String) = splitOn(s, '-')
def splitPlus(s: String) = splitOn(s, '+') map ("+" + _)
val TaggedVersion = """(\d{1,14})([\.\d{1,14}]*)((?:-\w+)*)((?:\+.+)*)""".r
val NonSpaceString = """(\S+)""".r
v match {
s match {
case TaggedVersion(m, ns, ts, es) =>
Some((Vector(m.toLong) ++ splitDot(ns), splitDash(ts), splitPlus(es)))
val numbers = Vector(m.toLong) ++ splitDot(ns)
val tags = splitDash(ts)
val extras = splitPlus(es)
Some((numbers, tags, extras))
case "" => None
case NonSpaceString(s) => Some((Vector(), Vector(), Vector(s)))
case NonSpaceString(s) => Some((Vector.empty, Vector.empty, Vector(s)))
case _ => None
}
}
/**
* Strict. Checks everythig.
*/
/** Strict. Checks everything. */
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
*/
/** Semantic Versioning. See http://semver.org/spec/v2.0.0.html */
object SemVer extends VersionNumberCompatibility {
def name: String = "Semantic Versioning"
/* Quotes of parts of the rules in the SemVer Spec relevant to compatibility checking:
*
* Rule 2:
* > A normal version number MUST take the form X.Y.Z
*
* Rule 4:
* > Major version zero (0.y.z) is for initial development. Anything may change at any time.
*
* Rule 6:
* > Patch version Z (x.y.Z | x > 0) MUST be incremented if only backwards compatible bug fixes are introduced.
*
* Rule 7:
* > Minor version Y (x.Y.z | x > 0) MUST be incremented if new, backwards compatible functionality is introduced.
*
* Rule 8:
* > Major version X (X.y.z | X > 0) MUST be incremented if any backwards incompatible changes are introduced.
*
* Rule 9:
* > A pre-release version MAY be denoted by appending a hyphen and a series of
* > dot separated identifiers immediately following the patch version.
* > Identifiers MUST comprise only ASCII alphanumerics and hyphen [0-9A-Za-z-].
* > Identifiers MUST NOT be empty.
* > Numeric identifiers MUST NOT include leading zeroes.
* > Pre-release versions have a lower precedence than the associated normal version.
* > A pre-release version indicates that the version is unstable and might not satisfy the
* > intended compatibility requirements as denoted by its associated normal version.
* > Examples: 1.0.0-alpha, 1.0.0-alpha.1, 1.0.0-0.3.7, 1.0.0-x.7.z.92.
*
* Rule 10:
* > Build metadata MAY be denoted by appending a plus sign and a series of
* > dot separated identifiers immediately following the patch or pre-release version.
* > Identifiers MUST comprise only ASCII alphanumerics and hyphen [0-9A-Za-z-].
* > Identifiers MUST NOT be empty.
* > Build metadata SHOULD be ignored when determining version precedence.
* > Thus two versions that differ only in the build metadata, have the same precedence.
* > Examples: 1.0.0-alpha+001, 1.0.0+20130313144700, 1.0.0-beta+exp.sha.5114f85.
*
* Rule 10 means that build metadata is never considered for compatibility
* we'll enforce this immediately by dropping them from both versions
* Rule 2 we enforce with custom extractors.
* Rule 4 we enforce by matching x = 0 & fully equals checking the two versions
* Rule 6, 7 & 8 means version compatibility is determined by comparing the two X values
* Rule 9 means pre-release versions are fully equals checked
*/
def isCompatible(v1: VersionNumber, v2: VersionNumber): Boolean =
doIsCompat(v1, v2) || doIsCompat(v2, v1)
doIsCompat(dropBuildMetadata(v1), dropBuildMetadata(v2))
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(0L),
v1.tags,
v2._1.get,
v2._2.get,
v2._3.getOrElse(0L),
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.nonEmpty || ts2.nonEmpty =>
// 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
case (NormalVersion(0, _, _), NormalVersion(0, _, _)) => v1 == v2 // R4
case (NormalVersion(x1, _, _), NormalVersion(x2, _, _)) => x1 == x2 // R6, R7 & R8
case (PrereleaseVersion(_, _, _), PrereleaseVersion(_, _, _)) => v1 == v2 // R9
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)
// SemVer Spec Rule 10 (above)
private[VersionNumber] def dropBuildMetadata(v: VersionNumber) =
if (v.extras.isEmpty) v else VersionNumber(v.numbers, v.tags, Nil)
// SemVer Spec Rule 9 (above)
private[VersionNumber] def isPrerelease(v: VersionNumber): Boolean = v.tags.nonEmpty
// An extractor for SemVer's "normal version number" - SemVer Spec Rule 2 & Rule 9 (above)
private[VersionNumber] object NormalVersion {
def unapply(v: VersionNumber): Option[(Long, Long, Long)] =
PartialFunction.condOpt(v.numbers) {
// NOTE! We allow the z to be missing, because of legacy like commons-io 1.3
case Seq(x, y, _*) if !isPrerelease(v) => (x, y, v._3 getOrElse 0)
}
}
// An extractor for SemVer's "pre-release versions" - SemVer Spec Rule 2 & Rule 9 (above)
private[VersionNumber] object PrereleaseVersion {
def unapply(v: VersionNumber): Option[(Long, Long, Long)] =
PartialFunction.condOpt(v.numbers) {
// NOTE! We allow the z to be missing, because of legacy like commons-io 1.3
case Seq(x, y, _*) if isPrerelease(v) => (x, y, v._3 getOrElse 0)
}
}
}
/* A variant of SemVar that seems to be common among the Scala libraries.
/** 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.
* Also API compatibility is expected even when the first segment is zero.
*/
object SecondSegment extends VersionNumberCompatibility {
import SemVer._
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 =
doIsCompat(dropBuildMetadata(v1), dropBuildMetadata(v2))
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.nonEmpty || ts2.nonEmpty =>
// 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
case (NormalVersion(x1, y1, _), NormalVersion(x2, y2, _)) => (x1 == x2) && (y1 == y2)
case (PrereleaseVersion(_, _, _), PrereleaseVersion(_, _, _)) => v1 == v2 // R2 & R9
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)
}
}
}

View File

@ -51,7 +51,7 @@ class VersionNumberSpec extends FreeSpec with Matchers with Inside {
assertIsNotCompatibleWith(v, "0.12.0-RC1", SecondSegment)
assertIsCompatibleWith(v, "0.12.1", SecondSegment)
assertIsCompatibleWith(v, "0.12.1-M1", SecondSegment)
assertIsNotCompatibleWith(v, "0.12.1-M1", SecondSegment)
}
version("0.1.0-SNAPSHOT") { v =>
@ -85,7 +85,7 @@ class VersionNumberSpec extends FreeSpec with Matchers with Inside {
version("2.10.4-20140115-000117-b3a-sources") { v =>
assertParsesTo(v, Seq(2, 10, 4), Seq("20140115", "000117", "b3a", "sources"), Seq())
assertCascadesTo(v, Seq("2.10.4-20140115-000117-b3a-sources", "2.10.4", "2.10"))
assertIsCompatibleWith(v, "2.0.0", SemVer)
assertIsNotCompatibleWith(v, "2.0.0", SemVer)
assertIsNotCompatibleWith(v, "2.0.0", SecondSegment)
}