Merge pull request #239 from tanishiking/semver-selector

Add semantic version selector API
This commit is contained in:
eugene yokota 2018-06-14 01:22:29 -04:00 committed by GitHub
commit 69b9d61cbd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 879 additions and 0 deletions

View File

@ -0,0 +1,56 @@
/**
* This code is generated using [[http://www.scala-sbt.org/contraband/ sbt-contraband]].
*/
// DO NOT EDIT MANUALLY
package sbt.internal.librarymanagement
final class SemComparator private (
val op: sbt.internal.librarymanagement.SemSelOperator,
val major: Option[Long],
val minor: Option[Long],
val patch: Option[Long],
val tags: Seq[String]) extends sbt.internal.librarymanagement.SemComparatorExtra with Serializable {
def matches(version: sbt.librarymanagement.VersionNumber): Boolean = this.matchesImpl(version)
def expandWildcard: Seq[SemComparator] = {
if (op == sbt.internal.librarymanagement.SemSelOperator.Eq && !allFieldsSpecified) {
Seq(
this.withOp(sbt.internal.librarymanagement.SemSelOperator.Gte),
this.withOp(sbt.internal.librarymanagement.SemSelOperator.Lte)
)
} else { Seq(this) }
}
override def equals(o: Any): Boolean = o match {
case x: SemComparator => (this.op == x.op) && (this.major == x.major) && (this.minor == x.minor) && (this.patch == x.patch) && (this.tags == x.tags)
case _ => false
}
override def hashCode: Int = {
37 * (37 * (37 * (37 * (37 * (37 * (17 + "sbt.internal.librarymanagement.SemComparator".##) + op.##) + major.##) + minor.##) + patch.##) + tags.##)
}
override def toString: String = {
this.toStringImpl
}
private[this] def copy(op: sbt.internal.librarymanagement.SemSelOperator = op, major: Option[Long] = major, minor: Option[Long] = minor, patch: Option[Long] = patch, tags: Seq[String] = tags): SemComparator = {
new SemComparator(op, major, minor, patch, tags)
}
def withOp(op: sbt.internal.librarymanagement.SemSelOperator): SemComparator = {
copy(op = op)
}
def withMajor(major: Option[Long]): SemComparator = {
copy(major = major)
}
def withMinor(minor: Option[Long]): SemComparator = {
copy(minor = minor)
}
def withPatch(patch: Option[Long]): SemComparator = {
copy(patch = patch)
}
def withTags(tags: Seq[String]): SemComparator = {
copy(tags = tags)
}
}
object SemComparator extends sbt.internal.librarymanagement.SemComparatorFunctions {
def apply(comparator: String): SemComparator = parse(comparator)
def apply(op: sbt.internal.librarymanagement.SemSelOperator, major: Option[Long], minor: Option[Long], patch: Option[Long], tags: Seq[String]): SemComparator = new SemComparator(op, major, minor, patch, tags)
}

View File

@ -0,0 +1,32 @@
/**
* This code is generated using [[http://www.scala-sbt.org/contraband/ sbt-contraband]].
*/
// DO NOT EDIT MANUALLY
package sbt.internal.librarymanagement
final class SemSelAndChunk private (
val comparators: Seq[sbt.internal.librarymanagement.SemComparator]) extends Serializable {
def matches(version: sbt.librarymanagement.VersionNumber): Boolean = comparators.forall(_.matches(version))
override def equals(o: Any): Boolean = o match {
case x: SemSelAndChunk => (this.comparators == x.comparators)
case _ => false
}
override def hashCode: Int = {
37 * (37 * (17 + "sbt.internal.librarymanagement.SemSelAndChunk".##) + comparators.##)
}
override def toString: String = {
comparators.map(_.toString).mkString(" ")
}
private[this] def copy(comparators: Seq[sbt.internal.librarymanagement.SemComparator] = comparators): SemSelAndChunk = {
new SemSelAndChunk(comparators)
}
def withComparators(comparators: Seq[sbt.internal.librarymanagement.SemComparator]): SemSelAndChunk = {
copy(comparators = comparators)
}
}
object SemSelAndChunk extends sbt.internal.librarymanagement.SemSelAndChunkFunctions {
def apply(andClauseToken: String): SemSelAndChunk = parse(andClauseToken)
def apply(comparators: Seq[sbt.internal.librarymanagement.SemComparator]): SemSelAndChunk = new SemSelAndChunk(comparators)
}

View File

@ -0,0 +1,74 @@
/**
* This code is generated using [[http://www.scala-sbt.org/contraband/ sbt-contraband]].
*/
// DO NOT EDIT MANUALLY
package sbt.librarymanagement
/**
* Semantic version selector API to check if the VersionNumber satisfies
* conditions described by semantic version selector.
*
* A `comparator` generally consist of an operator and version specifier.
* The set of operators is
* - `<`: Less than
* - `<=`: Less than or equal to
* - `>`: Greater than
* - `>=`: Greater than or equal to
* - `=`: Equal
* If no operator is specified, `=` is assumed.
*
* If minor or patch versions are not specified, some numbers are assumed.
* - `<=1.0` is equivalent to `<1.1.0`.
* - `<1.0` is equivalent to `<1.0.0`.
* - `>=1.0` is equivalent to `>=1.0.0`.
* - `>1.0` is equivalent to `>=1.1.0`.
* - `=1.0` is equivalent to `>=1.0 <=1.0` (so `>=1.0.0 <1.1.0`).
*
* Comparators can be combined by spaces to form the intersection set of the comparators.
* For example, `>1.2.3 <4.5.6` matches versions that are `greater than 1.2.3 AND less than 4.5.6`.
*
* The (intersection) set of comparators can combined by ` || ` (spaces are required) to form the
* union set of the intersection sets. So the semantic selector is in disjunctive normal form.
*
* Metadata and pre-release of VersionNumber are ignored.
* So `1.0.0` matches any versions that have `1.0.0` as normal version with any pre-release version
* or any metadata like `1.0.0-alpha`, `1.0.0+metadata`.
*
* Wildcard (`x`, `X`, `*`) can be used to match any number of minor or patch version.
* Actually, `1.0.x` is equivalent to `=1.0` (that is equivalent to `>=1.0.0 <1.1.0`)
*
* The hyphen range like `1.2.3 - 4.5.6` matches inclusive set of versions.
* So `1.2.3 - 4.5.6` is equivalent to `>=1.2.3 <=4.5.6`.
* Both sides of comparators around - are required and they can not have any operators.
* For example, `>=1.2.3 - 4.5.6` is invalid.
*/
final class SemanticSelector private (
val selectors: Seq[sbt.internal.librarymanagement.SemSelAndChunk]) extends Serializable {
def matches(versionNumber: VersionNumber): Boolean = selectors.exists(_.matches(versionNumber))
override def equals(o: Any): Boolean = o match {
case x: SemanticSelector => (this.selectors == x.selectors)
case _ => false
}
override def hashCode: Int = {
37 * (37 * (17 + "sbt.librarymanagement.SemanticSelector".##) + selectors.##)
}
override def toString: String = {
selectors.map(_.toString).mkString(" || ")
}
private[this] def copy(selectors: Seq[sbt.internal.librarymanagement.SemSelAndChunk] = selectors): SemanticSelector = {
new SemanticSelector(selectors)
}
def withSelectors(selectors: Seq[sbt.internal.librarymanagement.SemSelAndChunk]): SemanticSelector = {
copy(selectors = selectors)
}
}
object SemanticSelector {
def apply(selector: String): SemanticSelector = {
val orChunkTokens = selector.split("\\s+\\|\\|\\s+").map(_.trim)
val orChunks = orChunkTokens.map { chunk => sbt.internal.librarymanagement.SemSelAndChunk(chunk) }
SemanticSelector(orChunks)
}
def apply(selectors: Seq[sbt.internal.librarymanagement.SemSelAndChunk]): SemanticSelector = new SemanticSelector(selectors)
}

View File

@ -17,6 +17,110 @@
{ "name": "includeTypes", "type": "Set[String]" }
],
"parentsCompanion": "sbt.librarymanagement.MakePomConfigurationFunctions"
},
{
"name": "SemanticSelector",
"namespace": "sbt.librarymanagement",
"target": "Scala",
"type": "record",
"doc": [
"Semantic version selector API to check if the VersionNumber satisfies",
"conditions described by semantic version selector.",
"",
"A `comparator` generally consist of an operator and version specifier.",
"The set of operators is",
"- `<`: Less than",
"- `<=`: Less than or equal to",
"- `>`: Greater than",
"- `>=`: Greater than or equal to",
"- `=`: Equal",
"If no operator is specified, `=` is assumed.",
"",
"If minor or patch versions are not specified, some numbers are assumed.",
"- `<=1.0` is equivalent to `<1.1.0`.",
"- `<1.0` is equivalent to `<1.0.0`.",
"- `>=1.0` is equivalent to `>=1.0.0`.",
"- `>1.0` is equivalent to `>=1.1.0`.",
"- `=1.0` is equivalent to `>=1.0 <=1.0` (so `>=1.0.0 <1.1.0`).",
"",
"Comparators can be combined by spaces to form the intersection set of the comparators.",
"For example, `>1.2.3 <4.5.6` matches versions that are `greater than 1.2.3 AND less than 4.5.6`.",
"",
"The (intersection) set of comparators can combined by ` || ` (spaces are required) to form the",
"union set of the intersection sets. So the semantic selector is in disjunctive normal form.",
"",
"Metadata and pre-release of VersionNumber are ignored.",
"So `1.0.0` matches any versions that have `1.0.0` as normal version with any pre-release version",
"or any metadata like `1.0.0-alpha`, `1.0.0+metadata`.",
"",
"Wildcard (`x`, `X`, `*`) can be used to match any number of minor or patch version.",
"Actually, `1.0.x` is equivalent to `=1.0` (that is equivalent to `>=1.0.0 <1.1.0`)",
"",
"The hyphen range like `1.2.3 - 4.5.6` matches inclusive set of versions.",
"So `1.2.3 - 4.5.6` is equivalent to `>=1.2.3 <=4.5.6`.",
"Both sides of comparators around - are required and they can not have any operators.",
"For example, `>=1.2.3 - 4.5.6` is invalid."
],
"generateCodec": false,
"fields": [
{ "name": "selectors", "type": "Seq[sbt.internal.librarymanagement.SemSelAndChunk]" }
],
"toString": "selectors.map(_.toString).mkString(\" || \")",
"extra": "def matches(versionNumber: VersionNumber): Boolean = selectors.exists(_.matches(versionNumber))",
"extraCompanion": [
"def apply(selector: String): SemanticSelector = {",
" val orChunkTokens = selector.split(\"\\\\s+\\\\|\\\\|\\\\s+\").map(_.trim)",
" val orChunks = orChunkTokens.map { chunk => sbt.internal.librarymanagement.SemSelAndChunk(chunk) }",
" SemanticSelector(orChunks)",
"}"
]
},
{
"name": "SemSelAndChunk",
"namespace": "sbt.internal.librarymanagement",
"target": "Scala",
"type": "record",
"generateCodec": false,
"parentsCompanion": "sbt.internal.librarymanagement.SemSelAndChunkFunctions",
"fields": [
{ "name": "comparators", "type": "Seq[sbt.internal.librarymanagement.SemComparator]" }
],
"toString": "comparators.map(_.toString).mkString(\" \")",
"extra": "def matches(version: sbt.librarymanagement.VersionNumber): Boolean = comparators.forall(_.matches(version))",
"extraCompanion": [
"def apply(andClauseToken: String): SemSelAndChunk = parse(andClauseToken)"
]
},
{
"name": "SemComparator",
"namespace": "sbt.internal.librarymanagement",
"target": "Scala",
"type": "record",
"generateCodec": false,
"toString": "this.toStringImpl",
"parents": "sbt.internal.librarymanagement.SemComparatorExtra",
"parentsCompanion": "sbt.internal.librarymanagement.SemComparatorFunctions",
"fields": [
{ "name": "op", "type": "sbt.internal.librarymanagement.SemSelOperator" },
{ "name": "major", "type": "Option[Long]" },
{ "name": "minor", "type": "Option[Long]" },
{ "name": "patch", "type": "Option[Long]" },
{ "name": "tags", "type": "Seq[String]" }
],
"extra": [
"def matches(version: sbt.librarymanagement.VersionNumber): Boolean = this.matchesImpl(version)",
"def expandWildcard: Seq[SemComparator] = {",
" if (op == sbt.internal.librarymanagement.SemSelOperator.Eq && !allFieldsSpecified) {",
" Seq(",
" this.withOp(sbt.internal.librarymanagement.SemSelOperator.Gte),",
" this.withOp(sbt.internal.librarymanagement.SemSelOperator.Lte)",
" )",
" } else { Seq(this) }",
"}"
],
"extraCompanion": [
"def apply(comparator: String): SemComparator = parse(comparator)"
]
}
]
}

View File

@ -0,0 +1,17 @@
package sbt.internal.librarymanagement
sealed abstract class SemSelOperator {
override def toString: String = this match {
case SemSelOperator.Lte => "<="
case SemSelOperator.Lt => "<"
case SemSelOperator.Gte => ">="
case SemSelOperator.Gt => ">"
case SemSelOperator.Eq => "="
}
}
object SemSelOperator {
case object Lte extends SemSelOperator
case object Lt extends SemSelOperator
case object Gte extends SemSelOperator
case object Gt extends SemSelOperator
case object Eq extends SemSelOperator
}

View File

@ -0,0 +1,209 @@
package sbt.internal.librarymanagement
import sbt.librarymanagement.VersionNumber
import sbt.internal.librarymanagement.SemSelOperator.{ Lt, Lte, Gt, Gte, Eq }
import scala.annotation.tailrec
private[librarymanagement] abstract class SemSelAndChunkFunctions {
protected def parse(andClauseToken: String): SemSelAndChunk = {
val comparatorTokens = andClauseToken.split("\\s+")
val hyphenIndex = comparatorTokens.indexWhere(_ == "-")
val comparators = if (hyphenIndex == -1) {
comparatorTokens.map(SemComparator.apply)
} else {
// interpret `A.B.C - D.E.F` to `>=A.B.C <=D.E.F`
val (before, after) = comparatorTokens.splitAt(hyphenIndex)
(before.lastOption, after.drop(1).headOption) match {
case (Some(fromStr), Some(toStr)) =>
// from and to can not have an operator.
if (hasOperator(fromStr) || hasOperator(toStr)) {
throw new IllegalArgumentException(
s"Invalid ' - ' range, both side of comparators can not have an operator: $fromStr - $toStr")
}
val from = SemComparator(fromStr)
val to = SemComparator(toStr)
val comparatorsBefore = before.dropRight(1).map(SemComparator.apply)
val comparatorsAfter = after.drop(2) match {
case tokens if !tokens.isEmpty =>
parse(tokens.mkString(" ")).comparators
case _ => Seq.empty
}
from.withOp(Gte) +: to.withOp(Lte) +:
(comparatorsBefore ++ comparatorsAfter)
case _ =>
throw new IllegalArgumentException(
s"Invalid ' - ' range position, both side of versions must be specified: $andClauseToken")
}
}
SemSelAndChunk(comparators.flatMap(_.expandWildcard))
}
private[this] def hasOperator(comparator: String): Boolean = {
comparator.startsWith("<") ||
comparator.startsWith(">") ||
comparator.startsWith("=")
}
}
private[librarymanagement] abstract class SemComparatorExtra {
val op: SemSelOperator
val major: Option[Long]
val minor: Option[Long]
val patch: Option[Long]
val tags: Seq[String]
protected def toStringImpl: String = {
val versionStr = Seq(major, minor, patch)
.collect {
case Some(v) => v.toString
}
.mkString(".")
val tagsStr = if (tags.nonEmpty) s"-${tags.mkString("-")}" else ""
s"$op$versionStr$tagsStr"
}
protected def matchesImpl(version: VersionNumber): Boolean = {
// Fill empty fields of version specifier with 0 or max value of Long.
// By filling them, SemComparator realize the properties below
// `<=1.0` is equivalent to `<1.1.0` (`<=1.0.${Long.MaxValue}`)
// `<1.0` is equivalent to `<1.0.0`
// `>=1.0` is equivalent to `>=1.0.0`
// `>1.0` is equivalent to `>=1.1.0` (`>1.0.${Long.MaxValue}`)
//
// However this fills 0 for a comparator that have `=` operator,
// a comparator that have empty part of version and `=` operator won't appear
// because of expanding it to and clause of comparators.
val assumed = op match {
case Lte => Long.MaxValue
case Lt => 0L
case Gte => 0L
case Gt => Long.MaxValue
case Eq => 0L
}
// empty fields of the version number are assumed to be 0.
val versionNumber =
(version._1.getOrElse(0L), version._2.getOrElse(0L), version._3.getOrElse(0L))
val selector = (major.getOrElse(assumed), minor.getOrElse(assumed), patch.getOrElse(assumed))
val normalVersionCmp =
implicitly[Ordering[(Long, Long, Long)]].compare(versionNumber, selector)
val cmp =
if (normalVersionCmp == 0) comparePreReleaseTags(version.tags, tags)
else normalVersionCmp
op match {
case Lte if cmp <= 0 => true
case Lt if cmp < 0 => true
case Gte if cmp >= 0 => true
case Gt if cmp > 0 => true
case Eq if cmp == 0 => true
case _ => false
}
}
private[this] def comparePreReleaseTags(ts1: Seq[String], ts2: Seq[String]): Int = {
// > When major, minor, and patch are equal, a pre-release version has lower precedence than a normal version.
if (ts1.isEmpty && ts2.isEmpty) 0
else if (ts1.nonEmpty && ts2.isEmpty) -1 // ts1 is pre-release version
else if (ts1.isEmpty && ts2.nonEmpty) 1 // ts2 is pre-release version
else compareTags(ts1, ts2)
}
@tailrec
private[this] def compareTags(ts1: Seq[String], ts2: Seq[String]): Int = {
// > A larger set of pre-release fields has a higher precedence than a smaller set,
// > if all of the preceding identifiers are equal.
if (ts1.isEmpty && ts2.isEmpty) 0
else if (ts1.nonEmpty && ts2.isEmpty) 1
else if (ts1.isEmpty && ts2.nonEmpty) -1
else {
val ts1head = ts1.head
val ts2head = ts2.head
val cmp = (ts1head.matches("\\d+"), ts2head.matches("\\d+")) match {
// Identifiers consisting of only digits are compared numerically.
// Numeric identifiers always have lower precedence than non-numeric identifiers.
// Identifiers with letters are compared case insensitive lexical order.
case (true, true) => implicitly[Ordering[Long]].compare(ts1head.toLong, ts2head.toLong)
case (false, true) => 1
case (true, false) => -1
case (false, false) => ts1head.toLowerCase.compareTo(ts2head.toLowerCase)
}
if (cmp == 0) compareTags(ts1.tail, ts2.tail)
else cmp
}
}
// Expand wildcard with `=` operator to and clause of comparators.
// `=1.0` is equivalent to `>=1.0 <=1.0`
protected def allFieldsSpecified: Boolean =
major.isDefined && minor.isDefined && patch.isDefined
}
private[librarymanagement] abstract class SemComparatorFunctions {
private[this] val ComparatorRegex = """(?x)^
([<>]=?|=)?
(?:(\d+|[xX*])
(?:\.(\d+|[xX*])
(?:\.(\d+|[xX*]))?
)?
)((?:-\w+)*)$
""".r
protected def parse(comparator: String): SemComparator = {
comparator match {
case ComparatorRegex(rawOp, rawMajor, rawMinor, rawPatch, ts) =>
val opStr = Option(rawOp)
val major = Option(rawMajor)
val minor = Option(rawMinor)
val patch = Option(rawPatch)
val tags = splitDash(ts)
// Trim wildcard(x, X, *) and re-parse it.
// By trimming it, comparator realize the property like
// `=1.2.x` is equivalent to `=1.2`.
val hasXrangeSelector = Seq(major, minor, patch).exists {
case Some(str) => str.matches("[xX*]")
case None => false
}
if (hasXrangeSelector) {
if (tags.nonEmpty)
throw new IllegalArgumentException(
s"Pre-release version requires major, minor, patch versions to be specified: $comparator")
val numbers = Seq(major, minor, patch).takeWhile {
case Some(str) => str.matches("\\d+")
case None => false
}
parse(
numbers
.collect {
case Some(v) => v.toString
}
.mkString(".")
)
} else {
if (tags.nonEmpty && (major.isEmpty || minor.isEmpty || patch.isEmpty))
throw new IllegalArgumentException(
s"Pre-release version requires major, minor, patch versions to be specified: $comparator")
val operator = opStr match {
case Some("<") => Lt
case Some("<=") => Lte
case Some(">") => Gt
case Some(">=") => Gte
case Some("=") => Eq
case None => Eq
case Some(_) =>
throw new IllegalArgumentException(s"Invalid operator: $opStr")
}
SemComparator(
operator,
major.map(_.toLong),
minor.map(_.toLong),
patch.map(_.toLong),
tags
)
}
case _ => throw new IllegalArgumentException(s"Invalid comparator: $comparator")
}
}
private[this] def splitOn[A](s: String, sep: Char): Vector[String] =
if (s eq null) Vector()
else s.split(sep).filterNot(_ == "").toVector
private[this] def splitDash(s: String) = splitOn(s, '-')
}

View File

@ -27,6 +27,10 @@ final class VersionNumber private[sbt] (
case _ => false
}
def matchesSemVer(selsem: SemanticSelector): Boolean = {
selsem.matches(this)
}
/** 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)

View File

@ -0,0 +1,383 @@
package sbt.librarymanagement
import org.scalatest.{ FreeSpec, Matchers }
class SemanticSelectorSpec extends FreeSpec with Matchers {
semsel("<=1.2.3") { sel =>
assertMatches(sel, "1.2.3")
assertMatches(sel, "1.2-beta")
assertMatches(sel, "1.2.3-beta")
assertMatches(sel, "1.2")
assertMatches(sel, "1")
assertNotMatches(sel, "1.2.4-alpha")
assertNotMatches(sel, "1.2.4")
assertNotMatches(sel, "1.3")
assertNotMatches(sel, "1.3.0")
assertNotMatches(sel, "2")
}
semsel("<=1.2") { sel =>
assertMatches(sel, "1.2.345-beta")
assertMatches(sel, "1.2.3-beta")
assertMatches(sel, "1.2.3")
assertMatches(sel, "1.2")
assertMatches(sel, "1")
assertNotMatches(sel, "1.3.0")
assertNotMatches(sel, "1.3.0-alpha")
}
semsel("<=1") { sel =>
assertMatches(sel, "1.234.567-alpha")
assertMatches(sel, "1.234.567")
assertMatches(sel, "1.234")
assertMatches(sel, "1.0.0-alpha")
assertMatches(sel, "1.0.0")
assertMatches(sel, "1.0")
assertMatches(sel, "1")
assertNotMatches(sel, "2.0.0")
assertNotMatches(sel, "2.0.0-alpha")
}
semsel("<1.2.3") { sel =>
assertMatches(sel, "1.2.3-alpha")
assertMatches(sel, "1.2.2")
assertMatches(sel, "1.2")
assertMatches(sel, "1")
assertNotMatches(sel, "1.2.4-beta")
assertNotMatches(sel, "1.2.3")
assertNotMatches(sel, "1.3")
assertNotMatches(sel, "2")
}
semsel("<1.2") { sel =>
assertMatches(sel, "1.2.0-alpha")
assertMatches(sel, "1.1.23")
assertMatches(sel, "1.1")
assertMatches(sel, "1")
assertNotMatches(sel, "1.3-beta")
assertNotMatches(sel, "1.2.0")
assertNotMatches(sel, "1.2")
assertNotMatches(sel, "2")
}
semsel("<1") { sel =>
assertMatches(sel, "1.0.0-beta")
assertMatches(sel, "0.9.9-beta")
assertMatches(sel, "0.9.12")
assertMatches(sel, "0.8")
assertMatches(sel, "0")
assertNotMatches(sel, "1.0.1-beta")
assertNotMatches(sel, "1")
assertNotMatches(sel, "1.0")
assertNotMatches(sel, "1.0.0")
}
semsel(">=1.2.3") { sel =>
assertMatches(sel, "1.2.4-beta")
assertMatches(sel, "1.2.3")
assertMatches(sel, "1.3")
assertMatches(sel, "2")
assertNotMatches(sel, "1.2.3-beta")
assertNotMatches(sel, "1.2.2")
assertNotMatches(sel, "1.2")
assertNotMatches(sel, "1")
}
semsel(">=1.2") { sel =>
assertMatches(sel, "1.2.1-beta")
assertMatches(sel, "1.2.0")
assertMatches(sel, "1.2")
assertMatches(sel, "2")
assertNotMatches(sel, "1.2.0-beta")
assertNotMatches(sel, "1.1.23")
assertNotMatches(sel, "1.1")
assertNotMatches(sel, "1")
}
semsel(">=1") { sel =>
assertMatches(sel, "1.0.1-beta")
assertMatches(sel, "1.0.0")
assertMatches(sel, "1.0")
assertMatches(sel, "1")
assertNotMatches(sel, "1.0.0-beta")
assertNotMatches(sel, "0.9.9")
assertNotMatches(sel, "0.1")
assertNotMatches(sel, "0")
}
semsel(">1.2.3") { sel =>
assertMatches(sel, "1.2.4")
assertMatches(sel, "1.2.4-alpha")
assertMatches(sel, "1.3")
assertMatches(sel, "2")
assertNotMatches(sel, "1.2.3-alpha")
assertNotMatches(sel, "1.2.3")
assertNotMatches(sel, "1.2")
assertNotMatches(sel, "1")
}
semsel(">1.2") { sel =>
assertMatches(sel, "1.3.0")
assertMatches(sel, "1.3.0-alpha")
assertMatches(sel, "1.3")
assertMatches(sel, "2")
assertNotMatches(sel, "1.2.0-alpha")
assertNotMatches(sel, "1.2.9")
assertNotMatches(sel, "1.2")
assertNotMatches(sel, "1")
}
semsel(">1") { sel =>
assertMatches(sel, "2.0.0-alpha")
assertMatches(sel, "2.0.0")
assertMatches(sel, "2.0")
assertMatches(sel, "2")
assertNotMatches(sel, "1.2.3-alpha")
assertNotMatches(sel, "1.2.3")
assertNotMatches(sel, "1.2")
assertNotMatches(sel, "1")
}
semsel("1.2.3") { sel =>
assertMatches(sel, "1.2.3")
assertNotMatches(sel, "1.2.3-alpha")
assertNotMatches(sel, "1.2")
assertNotMatches(sel, "1.2.4")
}
Seq(".x", ".X", ".*", ".x.x", "").foreach { xrange =>
semsel(s"1$xrange") { sel =>
assertMatches(sel, "1.2.3-alpha")
assertMatches(sel, "1.0.0")
assertMatches(sel, "1.0.1")
assertMatches(sel, "1.1.1")
assertNotMatches(sel, "1.0.0-alpha")
assertNotMatches(sel, "2.0.0-alpha")
assertNotMatches(sel, "2.0.0")
assertNotMatches(sel, "0.1.0")
}
}
Seq(".x", ".X", ".*", "").foreach { xrange =>
semsel(s"1.2$xrange") { sel =>
assertMatches(sel, "1.2.0")
assertMatches(sel, "1.2.3")
assertNotMatches(sel, "1.2.0-alpha")
assertNotMatches(sel, "1.2.0-beta")
assertNotMatches(sel, "1.3.0-beta")
assertNotMatches(sel, "1.3.0")
assertNotMatches(sel, "1.1.1")
}
}
semsel("=1.2.3") { sel =>
assertMatches(sel, "1.2.3")
assertNotMatches(sel, "1.2.3-alpha")
assertNotMatches(sel, "1.2")
assertNotMatches(sel, "1.2.4")
}
semsel("=1.2") { sel =>
assertMatches(sel, "1.2.0")
assertMatches(sel, "1.2")
assertMatches(sel, "1.2.1")
assertMatches(sel, "1.2.4")
assertNotMatches(sel, "1.1.0")
assertNotMatches(sel, "1.3.0")
assertNotMatches(sel, "1.2.0-alpha")
assertNotMatches(sel, "1.3.0-alpha")
}
semsel("=1") { sel =>
assertMatches(sel, "1.0.0")
assertMatches(sel, "1.0")
assertMatches(sel, "1.0.1")
assertMatches(sel, "1.2.3")
assertNotMatches(sel, "1.0.0-alpha")
assertNotMatches(sel, "2.0.0")
}
semsel("1.2.3 || 2.0.0") { sel =>
assertMatches(sel, "1.2.3")
assertMatches(sel, "2.0.0")
assertNotMatches(sel, "1.2")
assertNotMatches(sel, "2.0.1")
}
semsel("<=1.2.3 || >=2.0.0 || 1.3.x") { sel =>
assertMatches(sel, "1.0")
assertMatches(sel, "1.2.3")
assertMatches(sel, "2.0.0")
assertMatches(sel, "2.0")
assertMatches(sel, "1.3.0")
assertMatches(sel, "1.3.3")
assertNotMatches(sel, "1.2.4")
assertNotMatches(sel, "1.4.0")
}
semsel(">=1.2.3 <2.0.0") { sel =>
assertMatches(sel, "1.2.3")
assertMatches(sel, "1.9.9")
assertNotMatches(sel, "1.2")
assertNotMatches(sel, "2.0.0")
}
semsel(">=1.2.3 <2.0.0 || >3.0.0 <=3.2.0") { sel =>
assertMatches(sel, "1.2.3")
assertMatches(sel, "1.9.9")
assertMatches(sel, "3.0.1")
assertMatches(sel, "3.2.0")
assertNotMatches(sel, "1.2")
assertNotMatches(sel, "2.0.0")
assertNotMatches(sel, "3.0.0")
assertNotMatches(sel, "3.2.1")
}
semsel("1.2.3 - 2.0.0") { sel =>
assertMatches(sel, "1.2.3")
assertMatches(sel, "1.9.9")
assertMatches(sel, "2.0.0")
assertNotMatches(sel, "1.2")
assertNotMatches(sel, "2.0.1")
}
semsel("1.2 - 2") { sel =>
assertMatches(sel, "1.2.0")
assertMatches(sel, "1.9.9")
assertMatches(sel, "2.0.0")
assertMatches(sel, "2.0.1")
assertNotMatches(sel, "1.1")
assertNotMatches(sel, "3.0.0")
}
semsel("1.2.3 - 2.0.0 1.5.0 - 2.4.0") { sel =>
assertMatches(sel, "1.5.0")
assertMatches(sel, "1.9.9")
assertMatches(sel, "2.0.0")
assertNotMatches(sel, "1.2.3")
assertNotMatches(sel, "1.4")
assertNotMatches(sel, "2.0.1")
assertNotMatches(sel, "2.4.0")
}
semsel("1.2.3 - 2.0 || 2.4.0 - 3") { sel =>
assertMatches(sel, "1.2.3")
assertMatches(sel, "1.5.0")
assertMatches(sel, "2.0.0")
assertMatches(sel, "2.4.0")
assertMatches(sel, "2.9")
assertMatches(sel, "3.0.0")
assertMatches(sel, "2.0.1")
assertMatches(sel, "3.0.1")
assertMatches(sel, "3.1.0")
assertNotMatches(sel, "2.1")
assertNotMatches(sel, "2.3.9")
assertNotMatches(sel, "4.0.0")
}
semsel(">=1.x") { sel =>
assertMatches(sel, "1.0.0")
assertMatches(sel, "1.0")
assertMatches(sel, "1")
assertNotMatches(sel, "1.0.0-beta")
assertNotMatches(sel, "0.9.9")
assertNotMatches(sel, "0.1")
assertNotMatches(sel, "0")
}
semsel(">=1.2.3-beta") { sel =>
assertMatches(sel, "1.3-alpha")
assertMatches(sel, "1.2.3")
assertMatches(sel, "1.2.3-beta")
assertMatches(sel, "1.2.3-beta-2")
assertMatches(sel, "1.2.3-beta-gamma")
assertMatches(sel, "1.2.4")
assertMatches(sel, "1.3")
assertNotMatches(sel, "1.2.3-alpha")
assertNotMatches(sel, "1.2.2")
}
semsel(">=1.2.3-beta-2") { sel =>
assertMatches(sel, "1.3-alpha")
assertMatches(sel, "1.2.3")
assertMatches(sel, "1.2.3-beta-2")
assertMatches(sel, "1.2.3-beta-2-3")
assertMatches(sel, "1.2.3-beta-3")
assertMatches(sel, "1.2.3-beta-gamma")
assertMatches(sel, "1.2.4")
assertMatches(sel, "1.3")
assertNotMatches(sel, "1.2.3-alpha-3")
assertNotMatches(sel, "1.2.3-beta-1")
assertNotMatches(sel, "1.2.3-beta")
assertNotMatches(sel, "1.2.2")
}
Seq(
// invalid operator
"~1.2.3",
"<~1.2.3",
"+1.2.3",
"!1.0.0",
">~1.2.3",
// too much version fields
"1.2.3.4",
"1.2.3.4.5",
"1.2.3.x",
// invalid version specifier
"string.!?",
"1.y",
"1.2x",
"1.1.c",
"-1",
"x",
"",
// || without spaces
"1.2.3|| 2.3.4",
"1.2.3 ||2.3.4",
"1.2.3||2.3.4",
// invalid - operator
"- 1.1.1",
"2.0.0 -",
"1.0.0 - 2.0.0 - 3.0.0",
">=1.0.0 - 2.0.0",
"1.0.0 - =3.0.0",
"=1.0.0 - =3.0.0",
"1.0.0 - 2.0.0 || - 2.0.0",
"1.0.0- 2.0.0",
"1.0.0 -2.0.0",
"1.0.0-2.0.0",
"-",
// minor and patch versions are required for pre-release version
"1.2-alpha-beta",
"1-beta",
"<=1.2-beta",
"<=1-beta",
"1.2-beta - 1.3-alpha",
"1.2.x-beta",
"1.x.*-beta",
// cannot specify metadata
"1.2.3+meta"
).foreach { selectorStr =>
semsel(selectorStr) { sel =>
assertParsesToError(sel)
}
}
private[this] final class SemanticSelectorString(val value: String)
private[this] def semsel(s: String)(f: SemanticSelectorString => Unit): Unit =
s"""SemanticSelector "$s"""" - {
f(new SemanticSelectorString(s))
}
private[this] def assertMatches(
s: SemanticSelectorString,
v: String
): Unit = s"""should match "$v"""" in {
SemanticSelector(s.value).matches(VersionNumber(v)) shouldBe true
}
private[this] def assertNotMatches(
s: SemanticSelectorString,
v: String
): Unit = s"""should not match "$v"""" in {
SemanticSelector(s.value).matches(VersionNumber(v)) shouldBe false
}
private[this] def assertParsesToError(s: SemanticSelectorString): Unit =
s"""should parse as an error""" in {
an[IllegalArgumentException] should be thrownBy SemanticSelector(s.value)
}
}