Skip to content

Commit

Permalink
Add missing SemVer APIs
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
NthPortal committed Sep 13, 2020
1 parent 3e26ebc commit 9555c43
Show file tree
Hide file tree
Showing 10 changed files with 203 additions and 123 deletions.
1 change: 1 addition & 0 deletions .scalafmt.conf
Expand Up @@ -4,3 +4,4 @@ docstrings = JavaDoc
assumeStandardLibraryStripMargin = true
project.git = true
maxColumn = 120
trailingCommas = multiple
24 changes: 15 additions & 9 deletions build.sbt
Expand Up @@ -12,57 +12,63 @@ 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",
)
)
),
)
)

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
.in(file("core"))
.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"

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,
)
2 changes: 1 addition & 1 deletion core/src/main/scala/lgbt/princess/v/VersionFactory.scala
Expand Up @@ -179,7 +179,7 @@ trait VersionFactory[+V <: Version] {
throw new VersionFormatException(
badVersion = version,
targetTypeDescription = versionTypeDescription,
cause = e
cause = e,
)
}
val seq = ArraySeq.unsafeWrapArray(ints)
Expand Down
Expand Up @@ -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,
) {

/**
Expand Down
47 changes: 46 additions & 1 deletion semver/src/main/scala/lgbt/princess/v/semver/Core.scala
Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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
}
}
15 changes: 10 additions & 5 deletions semver/src/main/scala/lgbt/princess/v/semver/SemVer.scala
Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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
}
70 changes: 70 additions & 0 deletions 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
}
}
94 changes: 1 addition & 93 deletions 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

0 comments on commit 9555c43

Please sign in to comment.