From 9555c431000ba23a054b46c0d48f2a879dd0ab31 Mon Sep 17 00:00:00 2001 From: NthPortal Date: Sun, 13 Sep 2020 17:02:36 -0400 Subject: [PATCH] Add missing SemVer APIs Add implicit `Ordering` for `SemVer`. Add methods to obtain the next major, minor and patch versions of a `Core`. Remove string extractor from `SemVer`, for consistency with other `Version` implementations. --- .scalafmt.conf | 1 + build.sbt | 24 +++-- .../lgbt/princess/v/VersionFactory.scala | 2 +- .../princess/v/VersionFormatException.scala | 2 +- .../scala/lgbt/princess/v/semver/Core.scala | 47 +++++++++- .../scala/lgbt/princess/v/semver/SemVer.scala | 15 ++- .../lgbt/princess/v/semver/extractors.scala | 70 ++++++++++++++ .../lgbt/princess/v/semver/package.scala | 94 +------------------ .../lgbt/princess/v/semver/CoreTest.scala | 15 +++ .../lgbt/princess/v/semver/SemVerTest.scala | 56 ++++++++--- 10 files changed, 203 insertions(+), 123 deletions(-) create mode 100644 semver/src/main/scala/lgbt/princess/v/semver/extractors.scala diff --git a/.scalafmt.conf b/.scalafmt.conf index f4effba..f45e318 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -4,3 +4,4 @@ docstrings = JavaDoc assumeStandardLibraryStripMargin = true project.git = true maxColumn = 120 +trailingCommas = multiple diff --git a/build.sbt b/build.sbt index 3985579..2d1ff96 100644 --- a/build.sbt +++ b/build.sbt @@ -12,16 +12,16 @@ inThisBuild( "NthPortal", "April | Princess", "dev@princess.lgbt", - url("https://nthportal.com") + url("https://nthportal.com"), ) ), scmInfo := Some( ScmInfo( url("https://github.com/NthPortal/v"), "scm:git:git@github.com:NthPortal/v.git", - "scm:git:git@github.com:NthPortal/v.git" + "scm:git:git@github.com:NthPortal/v.git", ) - ) + ), ) ) @@ -29,10 +29,16 @@ lazy val sharedSettings = Seq( libraryDependencies ++= Seq( "org.scalatest" %% "scalatest" % "3.2.2" % Test ), + scalacOptions ++= Seq( + "-deprecation", + "-feature", + "-Xlint", + "-Werror", + ), scalacOptions ++= { if (isSnapshot.value) Nil else Seq("-opt:l:inline", "-opt-inline-from:lgbt.princess.v.**") - } + }, ) lazy val core = project @@ -40,7 +46,7 @@ lazy val core = project .settings(sharedSettings) .settings( name := "v-core", - mimaPreviousArtifacts := Set().map(organization.value %% name.value % _) + mimaPreviousArtifacts := Set().map(organization.value %% name.value % _), ) lazy val coreTest = core % "test->test" @@ -48,21 +54,21 @@ lazy val semver = project .in(file("semver")) .dependsOn( core, - coreTest + coreTest, ) .settings(sharedSettings) .settings( name := "v-semver", - mimaPreviousArtifacts := Set().map(organization.value %% name.value % _) + mimaPreviousArtifacts := Set().map(organization.value %% name.value % _), ) lazy val root = project .in(file(".")) .aggregate( core, - semver + semver, ) .settings( name := "v", - mimaPreviousArtifacts := Set.empty + mimaPreviousArtifacts := Set.empty, ) diff --git a/core/src/main/scala/lgbt/princess/v/VersionFactory.scala b/core/src/main/scala/lgbt/princess/v/VersionFactory.scala index 7e3a443..1e9937a 100644 --- a/core/src/main/scala/lgbt/princess/v/VersionFactory.scala +++ b/core/src/main/scala/lgbt/princess/v/VersionFactory.scala @@ -179,7 +179,7 @@ trait VersionFactory[+V <: Version] { throw new VersionFormatException( badVersion = version, targetTypeDescription = versionTypeDescription, - cause = e + cause = e, ) } val seq = ArraySeq.unsafeWrapArray(ints) diff --git a/core/src/main/scala/lgbt/princess/v/VersionFormatException.scala b/core/src/main/scala/lgbt/princess/v/VersionFormatException.scala index d62b2bd..63215f8 100644 --- a/core/src/main/scala/lgbt/princess/v/VersionFormatException.scala +++ b/core/src/main/scala/lgbt/princess/v/VersionFormatException.scala @@ -13,7 +13,7 @@ package lgbt.princess.v class VersionFormatException(badVersion: String, targetTypeDescription: String, cause: Throwable) extends IllegalArgumentException( s"cannot create a $targetTypeDescription from ${VersionFormatException.renderBadVersion(badVersion)}", - cause + cause, ) { /** diff --git a/semver/src/main/scala/lgbt/princess/v/semver/Core.scala b/semver/src/main/scala/lgbt/princess/v/semver/Core.scala index 505e069..5c02b29 100644 --- a/semver/src/main/scala/lgbt/princess/v/semver/Core.scala +++ b/semver/src/main/scala/lgbt/princess/v/semver/Core.scala @@ -4,6 +4,7 @@ package semver import lgbt.princess.v.semver.Identifiers.{Build, PreRelease} import scala.collection.immutable.ArraySeq +import scala.language.implicitConversions /** * A SemVer version core. @@ -13,10 +14,33 @@ import scala.collection.immutable.ArraySeq * @param patch the patch number */ final case class Core(major: Int, minor: Int, patch: Int) extends Version with Ordered[Core] { + import Core._ + require(major >= 0 && minor >= 0 && patch >= 0, "SemVer core identifiers must be non-negative") type Self = Core + /** + * @return a version core with the major version number incremented as + * described in [[https://semver.org/#spec-item-8 item 8]] of the + * SemVer specification. + */ + def nextMajorVersion: Core = copy(major = major + 1, minor = 0, patch = 0) + + /** + * @return a version core with the minor version number incremented as + * described in [[https://semver.org/#spec-item-7 item 7]] of the + * SemVer specification. + */ + def nextMinorVersion: Core = copy(minor = minor + 1, patch = 0) + + /** + * @return a version core with the patch version number incremented as + * described in [[https://semver.org/#spec-item-6 item 6]] of the + * SemVer specification. + */ + def nextPatchVersion: Core = copy(patch = patch + 1) + /** * @return a pre-release SemVer version with the given identifiers * to which build identifiers can be added using the @@ -35,7 +59,7 @@ final case class Core(major: Int, minor: Int, patch: Int) extends Version with O def factory: VersionFactory[Core] = Core - override def compare(that: Core): Int = Core.ordering.compare(this, that) + override def compare(that: Core): Int = ordering.compare(this, that) override def equals(that: Any): Boolean = that match { @@ -70,4 +94,25 @@ object Core extends VersionFactory[Core] with VersionFactory.FixedSize { protected[this] def isValidSeq(seq: IndexedSeq[Int]): Boolean = seq.forall(_ >= 0) protected[this] def uncheckedFromSeq(seq: IndexedSeq[Int]): Core = apply(seq(0), seq(1), seq(2)) + /** + * The core and pre-release identifiers of a SemVer version, + * without build identifiers. This is an intermediate representation + * for convenience when creating a [[SemVer]] using symbolic methods. + */ + final class SemVerPreReleaseIntermediate private[Core] (private val self: SemVer) extends AnyVal { + + /** @return a pre-release SemVer version with the given build identifiers. */ + def +(build: Build): SemVer = self.copy(build = Some(build)) + + /** @return this pre-release SemVer version with no build identifiers. */ + @inline def toSemVer: SemVer = self + + /** @return this pre-release SemVer version with no build identifiers. */ + @inline def withoutMetadata: SemVer = toSemVer + } + + object SemVerPreReleaseIntermediate { + implicit def intermediateToSemVer(intermediate: SemVerPreReleaseIntermediate): SemVer = + intermediate.toSemVer + } } diff --git a/semver/src/main/scala/lgbt/princess/v/semver/SemVer.scala b/semver/src/main/scala/lgbt/princess/v/semver/SemVer.scala index c815c7f..532786a 100644 --- a/semver/src/main/scala/lgbt/princess/v/semver/SemVer.scala +++ b/semver/src/main/scala/lgbt/princess/v/semver/SemVer.scala @@ -14,9 +14,11 @@ import scala.collection.mutable.{StringBuilder => SStringBuilder} * @param preRelease the pre-release identifiers, if any * @param build the build identifiers, if any */ -final case class SemVer(core: Core, preRelease: Option[PreRelease], build: Option[Build]) { +final case class SemVer(core: Core, preRelease: Option[PreRelease], build: Option[Build]) extends Ordered[SemVer] { import SemVer._ + def compare(that: SemVer): Int = ordering.compare(this, that) + override def toString: String = { val sb = new JStringBuilder() sb.append(core) @@ -27,6 +29,12 @@ final case class SemVer(core: Core, preRelease: Option[PreRelease], build: Optio } object SemVer { + implicit val ordering: Ordering[SemVer] = (x, y) => { + val res1 = x.core compare y.core + if (res1 != 0) res1 + else java.lang.Boolean.compare(x.preRelease.isEmpty, y.preRelease.isEmpty) + } + private def appendPrefixed(sb: JStringBuilder, prefix: Char, identifiers: Option[Identifiers]): Unit = { if (identifiers.isDefined) { sb.append(prefix) @@ -103,13 +111,10 @@ object SemVer { apply( Core unsafeParse core, preRelease map PreRelease.unsafeParse, - build map Build.unsafeParse + build map Build.unsafeParse, ) } catch { case e: IllegalArgumentException => throw new VersionFormatException(version, "SemVer version", e) } } - - def unapply(version: String): Option[(Core, Option[PreRelease], Option[Build])] = - parse(version) flatMap unapply } diff --git a/semver/src/main/scala/lgbt/princess/v/semver/extractors.scala b/semver/src/main/scala/lgbt/princess/v/semver/extractors.scala new file mode 100644 index 0000000..a14a8c3 --- /dev/null +++ b/semver/src/main/scala/lgbt/princess/v/semver/extractors.scala @@ -0,0 +1,70 @@ +package lgbt.princess.v.semver + +import lgbt.princess.v.semver.Identifiers._ + +/** + * This extractor is for extracting the version core and pre-release identifiers + * of a SemVer version. If you want to extract build identifiers as well, + * use the [[:- `:-`]] extractor instead. Sadly, there is no way to have both + * functionalities in the same extractor. + */ +object - { + + /** + * Extracts the version core and pre-release identifiers from a SemVer + * version. + */ + def unapply(sv: SemVer): Option[(Core, PreRelease)] = { + if (sv.build.isDefined || sv.preRelease.isEmpty) None + else Some((sv.core, sv.preRelease.get)) + } +} + +/** + * This extractor is for extracting the version core of a SemVer version, + * and is for use specifically with the [[+ `+`]] extractor (which + * extracts the pre-release identifiers and build identifiers). If you + * only want to extract the core and pre-release identifiers, use the + * [[- `-`]] extractor instead. + */ +object :- { + + /** + * Extracts the version core from a SemVer version and leaves the + * pre-release identifiers and build identifiers partially extracted. + */ + def unapply(sv: SemVer): Option[(Core, (PreRelease, Option[Build]))] = + sv match { + case SemVer(core, Some(preRelease), build) => Some((core, (preRelease, build))) + case _ => None + } +} + +/** + * This extractor is for extracting the version core and build + * identifiers of a SemVer version, or for extracting the pre-release + * identifiers and build-identifiers when the version core has already + * been extracted by the [[:- `:-`]] extractor. + */ +object + { + + /** + * Extracts the version core and build identifiers from a SemVer + * version. + */ + def unapply(sv: SemVer): Option[(Core, Build)] = + sv match { + case SemVer(core, None, Some(build)) => Some((core, build)) + case _ => None + } + + /** + * Extracts the pre-release identifiers and build identifiers from + * a partially extracted SemVer version. + */ + def unapply(arg: (PreRelease, Option[Build])): Option[(PreRelease, Build)] = + arg match { + case (preRelease, Some(build)) => Some((preRelease, build)) + case _ => None + } +} diff --git a/semver/src/main/scala/lgbt/princess/v/semver/package.scala b/semver/src/main/scala/lgbt/princess/v/semver/package.scala index c8a6b8e..2b35c3d 100644 --- a/semver/src/main/scala/lgbt/princess/v/semver/package.scala +++ b/semver/src/main/scala/lgbt/princess/v/semver/package.scala @@ -1,101 +1,9 @@ package lgbt.princess.v -import scala.language.implicitConversions - /** * This package contains the [[SemVer]] class for representing * [[https://semver.org/ SemVer versions]], as well as symbolic * methods for conveniently constructing and extracting SemVer * versions. */ -package object semver { - import Identifiers._ - - /** - * This extractor is for extracting the version core and pre-release identifiers - * of a SemVer version. If you want to extract build identifiers as well, - * use the [[:- `:-`]] extractor instead. Sadly, there is no way to have both - * functionalities in the same extractor. - */ - object - { - - /** - * Extracts the version core and pre-release identifiers from a SemVer - * version. - */ - def unapply(sv: SemVer): Option[(Core, PreRelease)] = { - if (sv.build.isDefined || sv.preRelease.isEmpty) None - else Some((sv.core, sv.preRelease.get)) - } - } - - /** - * This extractor is for extracting the version core of a SemVer version, - * and is for use specifically with the [[+ `+`]] extractor (which - * extracts the pre-release identifiers and build identifiers). If you - * only want to extract the core and pre-release identifiers, use the - * [[- `-`]] extractor instead. - */ - object :- { - - /** - * Extracts the version core from a SemVer version and leaves the - * pre-release identifiers and build identifiers partially extracted. - */ - def unapply(sv: SemVer): Option[(Core, (PreRelease, Option[Build]))] = - sv match { - case SemVer(core, Some(preRelease), build) => Some((core, (preRelease, build))) - case _ => None - } - } - - /** - * This extractor is for extracting the version core and build - * identifiers of a SemVer version, or for extracting the pre-release - * identifiers and build-identifiers when the version core has already - * been extracted by the [[:- `:-`]] extractor. - */ - object + { - - /** - * Extracts the version core and build identifiers from a SemVer - * version. - */ - def unapply(sv: SemVer): Option[(Core, Build)] = - sv match { - case SemVer(core, None, Some(build)) => Some((core, build)) - case _ => None - } - - /** - * Extracts the pre-release identifiers and build identifiers from - * a partially extracted SemVer version. - */ - def unapply(arg: (PreRelease, Option[Build])): Option[(PreRelease, Build)] = - arg match { - case (preRelease, Some(build)) => Some((preRelease, build)) - case _ => None - } - } - - /** - * The core and pre-release identifiers of a SemVer version, - * without build identifiers. - */ - final class SemVerPreReleaseIntermediate private[semver] (private val self: SemVer) extends AnyVal { - - /** @return a pre-release SemVer version with the given build identifiers. */ - def +(build: Build): SemVer = self.copy(build = Some(build)) - - /** @return this pre-release SemVer version with no build identifiers. */ - @inline def toSemVer: SemVer = self - - /** @return this pre-release SemVer version with no build identifiers. */ - @inline def withoutMetadata: SemVer = toSemVer - } - - object SemVerPreReleaseIntermediate { - implicit def intermediateToSemVer(intermediate: SemVerPreReleaseIntermediate): SemVer = - intermediate.toSemVer - } -} +package object semver diff --git a/semver/src/test/scala/lgbt/princess/v/semver/CoreTest.scala b/semver/src/test/scala/lgbt/princess/v/semver/CoreTest.scala index 8974bd7..86386a5 100644 --- a/semver/src/test/scala/lgbt/princess/v/semver/CoreTest.scala +++ b/semver/src/test/scala/lgbt/princess/v/semver/CoreTest.scala @@ -156,4 +156,19 @@ class CoreTest extends BaseSpec { Core(1, 2, 3) + B("12") shouldEqual SemVer(Core(1, 2, 3), B("12")) Core(1, 2, 3) - PR("alpha") + B("12") shouldEqual SemVer(Core(1, 2, 3), PR("alpha"), B("12")) } + + it should "create the next major version" in { + Core(0, 1, 0).nextMajorVersion shouldEqual Core(1, 0, 0) + Core(1, 2, 3).nextMajorVersion shouldEqual Core(2, 0, 0) + } + + it should "create the next minor version" in { + Core(0, 1, 0).nextMinorVersion shouldEqual Core(0, 2, 0) + Core(1, 2, 3).nextMinorVersion shouldEqual Core(1, 3, 0) + } + + it should "create the next patch version" in { + Core(0, 1, 0).nextPatchVersion shouldEqual Core(0, 1, 1) + Core(1, 2, 3).nextPatchVersion shouldEqual Core(1, 2, 4) + } } diff --git a/semver/src/test/scala/lgbt/princess/v/semver/SemVerTest.scala b/semver/src/test/scala/lgbt/princess/v/semver/SemVerTest.scala index 5f1f16e..dde6667 100644 --- a/semver/src/test/scala/lgbt/princess/v/semver/SemVerTest.scala +++ b/semver/src/test/scala/lgbt/princess/v/semver/SemVerTest.scala @@ -2,10 +2,21 @@ package lgbt.princess.v package semver import lgbt.princess.v.semver.Identifiers.{Build => B, PreRelease => PR} +import org.scalactic.source.Position +import org.scalatest.Assertion class SemVerTest extends BaseSpec { behavior of nameOf[SemVer] + private implicit final class ShouldBeEquiv[A](self: A) { + def shouldBeEquiv(that: A)(implicit ord: Ordering[A], pos: Position): Assertion = + ord.equiv(self, that) shouldBe true + } + + private implicit final class OrderedEquiv[A](self: Ordered[A]) { + def equiv(that: A): Boolean = (self compare that) == 0 + } + it should "construct correctly using secondary constructors" in { SemVer(Core(1, 2, 3)) shouldEqual SemVer(Core(1, 2, 3), None, None) SemVer(Core(1, 2, 3), PR("alpha")) shouldEqual SemVer(Core(1, 2, 3), Some(PR("alpha")), None) @@ -13,6 +24,38 @@ class SemVerTest extends BaseSpec { SemVer(Core(1, 2, 3), PR("alpha"), B("12")) shouldEqual SemVer(Core(1, 2, 3), Some(PR("alpha")), Some(B("12"))) } + it should "be ordered correctly" in { + SemVer(Core(1, 2, 3)) should be < SemVer(Core(1, 2, 4), PR("alpha")) + SemVer(Core(1, 2, 3)) should be < SemVer(Core(1, 3, 0), PR("alpha")) + SemVer(Core(1, 2, 3)) should be < SemVer(Core(2, 0, 0), PR("alpha")) + SemVer(Core(1, 2, 3)) should be < SemVer(Core(1, 2, 4)) + SemVer(Core(1, 2, 3)) should be < SemVer(Core(1, 3, 0)) + SemVer(Core(1, 2, 3)) should be < SemVer(Core(2, 0, 0)) + SemVer(Core(1, 2, 3), PR("alpha")) should be < SemVer(Core(1, 2, 3)) + + SemVer(Core(1, 2, 3), PR("alpha")) shouldBeEquiv SemVer(Core(1, 2, 3), PR("beta")) + SemVer(Core(1, 2, 3)) shouldBeEquiv SemVer(Core(1, 2, 3), B("12")) + SemVer(Core(1, 2, 3), B("11")) shouldBeEquiv SemVer(Core(1, 2, 3), B("12")) + SemVer(Core(1, 2, 3), PR("alpha"), B("11")) shouldBeEquiv SemVer(Core(1, 2, 3), PR("alpha"), B("12")) + SemVer(Core(1, 2, 3), PR("alpha"), B("11")) shouldBeEquiv SemVer(Core(1, 2, 3), PR("beta"), B("12")) + SemVer(Core(1, 2, 3), PR("alpha"), B("12")) shouldBeEquiv SemVer(Core(1, 2, 3), PR("beta"), B("12")) + + SemVer(Core(1, 2, 3)) < SemVer(Core(1, 2, 4), PR("alpha")) shouldBe true + SemVer(Core(1, 2, 3)) < SemVer(Core(1, 3, 0), PR("alpha")) shouldBe true + SemVer(Core(1, 2, 3)) < SemVer(Core(2, 0, 0), PR("alpha")) shouldBe true + SemVer(Core(1, 2, 3)) < SemVer(Core(1, 2, 4)) shouldBe true + SemVer(Core(1, 2, 3)) < SemVer(Core(1, 3, 0)) shouldBe true + SemVer(Core(1, 2, 3)) < SemVer(Core(2, 0, 0)) shouldBe true + SemVer(Core(1, 2, 3), PR("alpha")) < SemVer(Core(1, 2, 3)) shouldBe true + + SemVer(Core(1, 2, 3), PR("alpha")) equiv SemVer(Core(1, 2, 3), PR("beta")) shouldBe true + SemVer(Core(1, 2, 3)) equiv SemVer(Core(1, 2, 3), B("12")) shouldBe true + SemVer(Core(1, 2, 3), B("11")) equiv SemVer(Core(1, 2, 3), B("12")) shouldBe true + SemVer(Core(1, 2, 3), PR("alpha"), B("11")) equiv SemVer(Core(1, 2, 3), PR("alpha"), B("12")) shouldBe true + SemVer(Core(1, 2, 3), PR("alpha"), B("11")) equiv SemVer(Core(1, 2, 3), PR("beta"), B("12")) shouldBe true + SemVer(Core(1, 2, 3), PR("alpha"), B("12")) equiv SemVer(Core(1, 2, 3), PR("beta"), B("12")) shouldBe true + } + it should "render as a string correctly" in { SemVer(Core(1, 2, 3)).toString shouldBe "1.2.3" SemVer(Core(1, 2, 3), PR("alpha")).toString shouldBe "1.2.3-alpha" @@ -46,19 +89,6 @@ class SemVerTest extends BaseSpec { a[VersionFormatException] should be thrownBy SemVer.unsafeParse("1.2.3-alpha+foo_bar") } - it should "extract from a string correctly" in { - "1.2.3" shouldMatch { case SemVer(Core(1, 2, 3), None, None) => } - "1.2.3-alpha" shouldMatch { case SemVer(Core(1, 2, 3), Some(PR("alpha")), None) => } - "1.2.3+12" shouldMatch { case SemVer(Core(1, 2, 3), None, Some(B("12"))) => } - "1.2.3-alpha+12" shouldMatch { case SemVer(Core(1, 2, 3), Some(_), Some(B("12"))) => } - - "-1.2.3" shouldNotMatch { case SemVer(_, _, _) => } - "1.-2.3" shouldNotMatch { case SemVer(_, _, _) => } - "1.2.3-01" shouldNotMatch { case SemVer(_, _, _) => } - "1.2.3+foo..bar" shouldNotMatch { case SemVer(_, _, _) => } - "1.2.3-alpha+foo_bar" shouldNotMatch { case SemVer(_, _, _) => } - } - it should "extract using symbols correctly" in { SemVer(Core(1, 2, 3), PR("alpha")) shouldMatch { case Core(1, 2, 3) - PR("alpha") => } SemVer(Core(1, 2, 3), B("12")) shouldMatch { case Core(1, 2, 3) + B("12") => }