mirror of https://github.com/sbt/sbt.git
Add semantic version number selector API.
This semantic version number selector API is based on - https://draftin.com/documents/375100?token=rR30GmJJzi4l3BRlD-cHs8lcAcdDAXH4oTzqOWeL0CT0BNv3PZEx0g8pBkI13sQgYXTBqShZ0Ucsqek3Fn3d-aU - https://docs.npmjs.com/misc/semver
This commit is contained in:
parent
6e4ad6ebd4
commit
c0c88eda9f
|
|
@ -0,0 +1,259 @@
|
|||
package sbt.librarymanagement
|
||||
|
||||
/**
|
||||
* Semantic version selector API to check if the VersionNumber satisfies
|
||||
* conditions described by semantic version selector.
|
||||
*/
|
||||
sealed abstract case class SemanticSelector(
|
||||
private val selectors: Seq[SemanticSelector.SemSelAndChunk]) {
|
||||
|
||||
/**
|
||||
* Check if the version number satisfies the conditions described by semantic version selector.
|
||||
*
|
||||
* The empty fields of the version number are assumed to be 0, for example, `1` is treated as `1.0.0`.
|
||||
*
|
||||
* @param versionNumber The Version Number to be checked if it satisfies the conditions.
|
||||
* @return The result of checking the version number satisfies the conditions or not.
|
||||
*/
|
||||
def matches(versionNumber: VersionNumber): Boolean = {
|
||||
selectors.exists(_.matches(versionNumber))
|
||||
}
|
||||
override def toString: String = selectors.map(_.toString).mkString(" || ")
|
||||
}
|
||||
object SemanticSelector {
|
||||
|
||||
/**
|
||||
* Build a SemanticSelector that can match specific semantic versions.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* @param selector A string that represents semantic version selector.
|
||||
* @return A `SemanticSelector` that can match only onto specific semantic versions.
|
||||
* @throws java.lang.IllegalAccessException when selector is in invalid format of semantic version selector.
|
||||
*/
|
||||
def apply(selector: String): SemanticSelector = {
|
||||
val orChunkTokens = selector.split("\\s+\\|\\|\\s+").map(_.trim)
|
||||
val orChunks = orChunkTokens.map { chunk =>
|
||||
SemSelAndChunk(chunk)
|
||||
}
|
||||
new SemanticSelector(orChunks) {}
|
||||
}
|
||||
|
||||
private[this] sealed trait SemSelOperator
|
||||
private[this] case object Lte extends SemSelOperator {
|
||||
override def toString: String = "<="
|
||||
}
|
||||
private[this] case object Lt extends SemSelOperator {
|
||||
override def toString: String = "<"
|
||||
}
|
||||
private[this] case object Gte extends SemSelOperator {
|
||||
override def toString: String = ">="
|
||||
}
|
||||
private[this] case object Gt extends SemSelOperator {
|
||||
override def toString: String = ">"
|
||||
}
|
||||
private[this] case object Eq extends SemSelOperator {
|
||||
override def toString: String = "="
|
||||
}
|
||||
|
||||
private[SemanticSelector] final case class SemSelAndChunk(comparators: Seq[SemComparator]) {
|
||||
def matches(version: VersionNumber): Boolean = {
|
||||
comparators.forall(_.matches(version))
|
||||
}
|
||||
override def toString: String = comparators.map(_.toString).mkString(" ")
|
||||
}
|
||||
private[SemanticSelector] object SemSelAndChunk {
|
||||
def apply(andClauseToken: String): SemSelAndChunk = parse(andClauseToken)
|
||||
private[this] 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.copy(op = Gte) +:
|
||||
to.copy(op = 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[SemanticSelector] final case class SemComparator private (
|
||||
op: SemSelOperator,
|
||||
major: Option[Long],
|
||||
minor: Option[Long],
|
||||
patch: Option[Long]
|
||||
) {
|
||||
def matches(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 cmp = implicitly[Ordering[(Long, Long, Long)]].compare(versionNumber, selector)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// Expand wildcard with `=` operator to and clause of comparators.
|
||||
// `=1.0` is equivalent to `>=1.0 <=1.0`
|
||||
def expandWildcard: Seq[SemComparator] = {
|
||||
if (op == Eq && !allFieldsSpecified) {
|
||||
Seq(this.copy(op = Gte), this.copy(op = Lte))
|
||||
} else {
|
||||
Seq(this)
|
||||
}
|
||||
}
|
||||
private[this] def allFieldsSpecified: Boolean =
|
||||
major.isDefined && minor.isDefined && patch.isDefined
|
||||
|
||||
override def toString: String = {
|
||||
val versionStr = Seq(major, minor, patch)
|
||||
.collect {
|
||||
case Some(v) => v.toString
|
||||
}
|
||||
.mkString(".")
|
||||
s"$op$versionStr"
|
||||
}
|
||||
}
|
||||
private[SemanticSelector] object SemComparator {
|
||||
def apply(comparator: String): SemComparator = parse(comparator)
|
||||
private[this] val ComparatorRegex = """(?x)^
|
||||
([<>]=?|=)?
|
||||
(?:(\d+|[xX*])
|
||||
(?:\.(\d+|[xX*])
|
||||
(?:\.(\d+|[xX*]))?
|
||||
)?
|
||||
)$
|
||||
""".r
|
||||
private[this] def parse(comparator: String): SemComparator = {
|
||||
comparator match {
|
||||
case ComparatorRegex(rawOp, rawMajor, rawMinor, rawPatch) =>
|
||||
val opStr = Option(rawOp)
|
||||
val major = Option(rawMajor)
|
||||
val minor = Option(rawMinor)
|
||||
val patch = Option(rawPatch)
|
||||
|
||||
// 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) {
|
||||
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 {
|
||||
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)
|
||||
)
|
||||
}
|
||||
case _ => throw new IllegalArgumentException(s"Invalid comparator: $comparator")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -27,6 +27,10 @@ final class VersionNumber private[sbt] (
|
|||
case _ => false
|
||||
}
|
||||
|
||||
def satisfies(selector: String): Boolean = {
|
||||
SemanticSelector(selector).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)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,315 @@
|
|||
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.3-beta")
|
||||
assertMatches(sel, "1.2")
|
||||
assertMatches(sel, "1")
|
||||
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.3")
|
||||
assertMatches(sel, "1.2.3-beta")
|
||||
assertMatches(sel, "1.2")
|
||||
assertMatches(sel, "1")
|
||||
assertNotMatches(sel, "1.3.0")
|
||||
}
|
||||
|
||||
semsel("<=1") { sel =>
|
||||
assertMatches(sel, "1.12.12")
|
||||
assertMatches(sel, "1.12.12-alpha")
|
||||
assertMatches(sel, "1.2")
|
||||
assertNotMatches(sel, "2.0.0")
|
||||
assertNotMatches(sel, "2.0.0-alpha")
|
||||
}
|
||||
|
||||
semsel("<1.2.3") { sel =>
|
||||
assertMatches(sel, "1.2.2")
|
||||
assertMatches(sel, "1.2")
|
||||
assertNotMatches(sel, "1.2.3-alpha")
|
||||
assertNotMatches(sel, "1.2.3")
|
||||
}
|
||||
|
||||
semsel("<1.2") { sel =>
|
||||
assertMatches(sel, "1.1.23")
|
||||
assertMatches(sel, "1.1")
|
||||
assertNotMatches(sel, "1.2")
|
||||
assertNotMatches(sel, "1.2.0-alpha")
|
||||
}
|
||||
|
||||
semsel("<1") { sel =>
|
||||
assertMatches(sel, "0.9.12")
|
||||
assertMatches(sel, "0.8")
|
||||
assertNotMatches(sel, "1")
|
||||
assertNotMatches(sel, "1.0")
|
||||
assertNotMatches(sel, "1.0.0")
|
||||
}
|
||||
|
||||
semsel(">=1.2.3") { sel =>
|
||||
assertMatches(sel, "1.2.3")
|
||||
assertMatches(sel, "1.2.3-beta")
|
||||
assertMatches(sel, "1.3")
|
||||
assertMatches(sel, "2")
|
||||
assertNotMatches(sel, "1.2.2")
|
||||
assertNotMatches(sel, "1.2")
|
||||
assertNotMatches(sel, "1")
|
||||
}
|
||||
|
||||
semsel(">=1.2") { sel =>
|
||||
assertMatches(sel, "1.2.0")
|
||||
assertMatches(sel, "1.2.0-beta")
|
||||
assertMatches(sel, "1.2")
|
||||
assertMatches(sel, "2")
|
||||
assertNotMatches(sel, "1.1.23")
|
||||
assertNotMatches(sel, "1.1")
|
||||
assertNotMatches(sel, "1")
|
||||
}
|
||||
|
||||
semsel(">=1") { sel =>
|
||||
assertMatches(sel, "1.0.0")
|
||||
assertMatches(sel, "1.0.0-beta")
|
||||
assertMatches(sel, "1.0")
|
||||
assertMatches(sel, "1")
|
||||
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")
|
||||
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.9")
|
||||
assertNotMatches(sel, "1.2")
|
||||
assertNotMatches(sel, "1")
|
||||
}
|
||||
|
||||
semsel(">1") { sel =>
|
||||
assertMatches(sel, "2.0.0")
|
||||
assertMatches(sel, "2.0")
|
||||
assertMatches(sel, "2")
|
||||
assertNotMatches(sel, "1.2.3")
|
||||
assertNotMatches(sel, "1.2")
|
||||
assertNotMatches(sel, "1")
|
||||
}
|
||||
|
||||
semsel("1.2.3") { sel =>
|
||||
assertMatches(sel, "1.2.3")
|
||||
assertMatches(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.0.0")
|
||||
assertMatches(sel, "1.0.1")
|
||||
assertMatches(sel, "1.1.1")
|
||||
assertMatches(sel, "1.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.0-beta")
|
||||
assertMatches(sel, "1.2.3")
|
||||
assertNotMatches(sel, "1.3.0")
|
||||
assertNotMatches(sel, "1.1.1")
|
||||
}
|
||||
}
|
||||
|
||||
semsel("=1.2.3") { sel =>
|
||||
assertMatches(sel, "1.2.3")
|
||||
assertMatches(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.0-alpha")
|
||||
assertMatches(sel, "1.2")
|
||||
assertMatches(sel, "1.2.1")
|
||||
assertMatches(sel, "1.2.4")
|
||||
}
|
||||
semsel("=1") { sel =>
|
||||
assertMatches(sel, "1.0.0")
|
||||
assertMatches(sel, "1.0.0-alpha")
|
||||
assertMatches(sel, "1.0")
|
||||
assertMatches(sel, "1.0.1")
|
||||
assertMatches(sel, "1.2.3")
|
||||
}
|
||||
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.0-beta")
|
||||
assertMatches(sel, "1.0")
|
||||
assertMatches(sel, "1")
|
||||
assertNotMatches(sel, "0.9.9")
|
||||
assertNotMatches(sel, "0.1")
|
||||
assertNotMatches(sel, "0")
|
||||
}
|
||||
|
||||
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",
|
||||
"-",
|
||||
// cannot specify pre-release or metadata
|
||||
"1.2.3-alpha",
|
||||
"1.2-alpha",
|
||||
"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)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue