Parsing
The core library provides robust parsing for SemVer 2.0.0 strings via two type classes:
- version.Version.Read
[A]— converts input types toVersion - version.PreRelease.Resolver — maps pre-release identifiers to structured
PreRelease
Basic Usage
import version.*
// Extension methods require a Read[String] and Resolver in scope.
// Both are provided by default via `import version.{given, *}`.
"1.2.3".toVersion // Right(Version(1, 2, 3))
"1.2.3-alpha.1".toVersion // Right(Version(1, 2, 3, alpha.1))
"invalid".toVersion // Left(ParseError(...))
// Unsafe variant throws on invalid input
"1.2.3".toVersionUnsafe // Version(1, 2, 3)
"invalid".toVersionUnsafe // throws ParseError
// Factory methods (equivalent)
Version.from("1.2.3") // Right(Version(1, 2, 3))
Version.fromUnsafe("1.2.3") // Version(1, 2, 3)
Accepted Formats
| Format | Example | Notes |
|---|---|---|
| Core | 1.2.3 |
Required |
| With prefix | v1.2.3, V1.2.3 |
Optional v stripped |
| Pre-release | 1.2.3-alpha.1 |
After - |
| Metadata | 1.2.3+build.456 |
After + |
| Full | 1.2.3-alpha.1+build.456 |
Pre-release before metadata |
Pre-release Formats
The default resolver recognises these classifiers:
| Input | Parsed As |
|---|---|
1.0.0-alpha.1 |
Alpha, number 1 |
1.0.0-a.1 |
Alpha, number 1 |
1.0.0-beta.2 |
Beta, number 2 |
1.0.0-b.2 |
Beta, number 2 |
1.0.0-rc.3 |
Release Candidate, number 3 |
1.0.0-cr.3 |
Release Candidate, number 3 |
1.0.0-m.1 |
Milestone, number 1 |
1.0.0-dev.1 |
Dev, number 1 |
1.0.0-SNAPSHOT |
Snapshot (canonical) |
1.0.0-snapshot |
Snapshot (case-insensitive) |
Common variations are normalised:
| Input | Normalised |
|---|---|
1.0.0-RC1 |
rc.1 |
1.0.0-alpha1 |
alpha.1 |
Build Metadata
Metadata identifiers must match [0-9A-Za-z-]+:
"1.2.3+sha.abc123".toVersion
// Right(Version(1, 2, 3, None, Some(Metadata(sha, abc123))))
"1.2.3+build.456.dirty".toVersion
// Right(..., Some(Metadata(build, 456, dirty)))
Custom Pre-release Mapping
Implement version.PreRelease.Resolver to handle non-standard pre-release formats. The resolver receives dot-separated identifier tokens and returns Some(PreRelease) on success or None to reject.
import version.*
// Define a custom resolver
val customResolver: PreRelease.Resolver = new PreRelease.Resolver:
extension (identifiers: List[String])
def resolve: Option[PreRelease] =
identifiers match
// Map "nightly" to snapshot
case List("nightly") =>
Some(PreRelease.snapshot)
// Map "preview.N" to alpha
case List("preview", n) =>
n.toIntOption
.flatMap(i => PreReleaseNumber.from(i).toOption)
.map(PreRelease.alpha)
// Delegate unrecognised formats to the default resolver
case _ =>
PreRelease.Resolver.given_Resolver.resolve(identifiers)
// Use the custom resolver
given PreRelease.Resolver = customResolver
"1.0.0-nightly".toVersion // Right(Version(1, 0, 0, SNAPSHOT))
"1.0.0-preview.3".toVersion // Right(Version(1, 0, 0, alpha.3))
"1.0.0-beta.1".toVersion // Right(Version(1, 0, 0, beta.1)) — delegated
Resolver API
trait Resolver:
extension (identifiers: List[String]) def resolve: Option[PreRelease]
The identifiers parameter contains the pre-release string split by .. For example, alpha.1 becomes List("alpha", "1").
Default Resolver
Access the default resolver via PreRelease.Resolver.given_Resolver:
// Delegate to default for standard formats
PreRelease.Resolver.given_Resolver.resolve(List("rc", "2"))
// Some(PreRelease(ReleaseCandidate, Some(2)))
Custom Read Instances
Implement version.Version.Read[A] to parse custom input types. The type class requires a contextual version.PreRelease.Resolver for pre-release mapping.
import version.*
import version.errors.InvalidVersionFormat
case class MyVersionFormat(major: Int, minor: Int, patch: Int)
given Version.Read[MyVersionFormat] with
extension (m: MyVersionFormat)
def toVersion(using PreRelease.Resolver): Either[errors.ParseError, Version] =
for
major <- MajorVersion.from(m.major).left.map(_ =>
InvalidVersionFormat(s"${m.major}.${m.minor}.${m.patch}"))
minor <- MinorVersion.from(m.minor).left.map(_ =>
InvalidVersionFormat(s"${m.major}.${m.minor}.${m.patch}"))
patch <- PatchNumber.from(m.patch).left.map(_ =>
InvalidVersionFormat(s"${m.major}.${m.minor}.${m.patch}"))
yield Version(major, minor, patch)
def toVersionUnsafe(using PreRelease.Resolver): Version =
toVersion match
case Right(v) => v
case Left(e) => throw e
// Now works with factory methods
Version.from(MyVersionFormat(1, 2, 3)) // Right(Version(1, 2, 3))
Read API
trait Read[A]:
extension (a: A)
def toVersion(using PreRelease.Resolver): Either[errors.ParseError, Version]
def toVersionUnsafe(using PreRelease.Resolver): Version
String Read with Custom Tag Prefix
A common use case is supporting alternative tag prefixes (e.g., release- instead of v):
import version.*
val releaseReader: Version.Read[String] = new Version.Read[String]:
extension (s: String)
def toVersion(using PreRelease.Resolver): Either[errors.ParseError, Version] =
val normalised = if s.startsWith("release-") then s.stripPrefix("release-") else s
Version.Read.ReadString.toVersion(normalised)
def toVersionUnsafe(using PreRelease.Resolver): Version =
toVersion match
case Right(v) => v
case Left(e) => throw e
given Version.Read[String] = releaseReader
"release-1.2.3".toVersion // Right(Version(1, 2, 3))
Combining Read and Resolver
When both custom parsing and custom pre-release mapping are required:
import version.*
// Custom resolver: map "nightly" to SNAPSHOT
val customResolver: PreRelease.Resolver = new PreRelease.Resolver:
extension (ids: List[String])
def resolve: Option[PreRelease] = ids match
case List("nightly") => Some(PreRelease.snapshot)
case _ => PreRelease.Resolver.given_Resolver.resolve(ids)
// Custom reader: strip "release-" prefix
val customReader: Version.Read[String] = new Version.Read[String]:
extension (s: String)
def toVersion(using PreRelease.Resolver): Either[errors.ParseError, Version] =
val normalised = if s.startsWith("release-") then s.stripPrefix("release-") else s
Version.Read.ReadString.toVersion(normalised)
def toVersionUnsafe(using PreRelease.Resolver): Version =
toVersion.fold(e => throw e, identity)
// Bring both into scope
given PreRelease.Resolver = customResolver
given Version.Read[String] = customReader
// Both customisations apply
"release-2.0.0-nightly".toVersion // Right(Version(2, 0, 0, SNAPSHOT))
Error Handling
Parse errors provide context:
import version.errors.*
"abc".toVersion match
case Left(err: ParseError) =>
err.input // "abc"
err.message // Descriptive error
case Right(v) =>
// ...
Validation Summary
| Component | Constraint | Error |
|---|---|---|
| Major | >= 0 | InvalidMajorVersion |
| Minor | >= 0 | InvalidMinorVersion |
| Patch | >= 0 | InvalidPatchNumber |
| Pre-release number | >= 1 | InvalidPreReleaseNumber |
| Build metadata | Non-empty, valid chars | InvalidMetadata |