///|
/// A SemVer 2.0.0 version.
///
/// The three numbers are arbitrary-precision integers, so `18446744073709551616.0.0`
/// parses, prints and compares exactly, well past 64 bits (node-semver stops at the
/// JS safe integer, Rust's semver crate at u64).
///
/// The fields are private and only `Version::new` / `Version::parse` can build one,
/// so a version never carries a leading zero or a malformed identifier list.
pub struct Version {
major : @bigint.BigInt
minor : @bigint.BigInt
patch : @bigint.BigInt
pre : Array[Identifier]
build : Array[String]
}
///|
/// Build a version from three `Int`s, with no prerelease and no build metadata.
pub fn Version::new(major : Int32, minor : Int32, patch : Int32) -> Version {
{
major: @bigint.BigInt::from_int(major),
minor: @bigint.BigInt::from_int(minor),
patch: @bigint.BigInt::from_int(patch),
pre: [],
build: [],
}
}
///|
/// Build a version from three arbitrary-precision integers.
pub fn Version::from_bigint(
major : @bigint.BigInt,
minor : @bigint.BigInt,
patch : @bigint.BigInt,
) -> Version {
{ major, minor, patch, pre: [], build: [], }
}
///|
/// The major number.
pub fn Version::major(self : Version) -> @bigint.BigInt {
self.major
}
///|
/// The minor number.
pub fn Version::minor(self : Version) -> @bigint.BigInt {
self.minor
}
///|
/// The patch number.
pub fn Version::patch(self : Version) -> @bigint.BigInt {
self.patch
}
///|
/// The prerelease identifiers: `["alpha", 1]` for `1.2.3-alpha.1`.
pub fn Version::prerelease(self : Version) -> ArrayView[Identifier] {
self.pre[:]
}
///|
/// The build metadata identifiers: `["build", "5"]` for `1.2.3+build.5`.
pub fn Version::build(self : Version) -> ArrayView[String] {
self.build[:]
}
///|
/// Whether the version carries a prerelease.
pub fn Version::is_prerelease(self : Version) -> Bool {
!self.pre.is_empty()
}
///|
/// Whether this is a stable release. Build metadata does not make it unstable.
pub fn Version::is_stable(self : Version) -> Bool {
self.pre.is_empty()
}
///|
/// Whether the three numbers match, ignoring prerelease and build metadata.
pub fn Version::same_core(self : Version, other : Version) -> Bool {
self.major == other.major &&
self.minor == other.minor &&
self.patch == other.patch
}
///|
/// Keep the three numbers only, dropping prerelease and build metadata.
pub fn Version::core(self : Version) -> Version {
{
major: self.major,
minor: self.minor,
patch: self.patch,
pre: [],
build: [],
}
}
///|
/// The `major.minor.patch` form, without prerelease or build metadata.
pub fn Version::core_string(self : Version) -> String {
let sb = StringBuilder()
sb.write_string(self.major.to_string())
sb.write_string(".")
sb.write_string(self.minor.to_string())
sb.write_string(".")
sb.write_string(self.patch.to_string())
sb.to_string()
}
///|
/// Parse a version.
///
/// The npm grammar is the default, so `v1.2.3`, `=1.2.3` and surrounding whitespace
/// are all fine. Pass `syntax=Syntax::strict()` to hold to the spec exactly.
pub fn Version::parse(
s : String,
syntax~ : Syntax = Syntax::npm(),
) -> Version raise Refused {
parse_version_full(s, syntax)
}
///|
fn parse_version_full(s : String, syn : Syntax) -> Version raise Refused {
let cur = Cursor::new(s)
if syn.trim {
cur.skip_spaces()
}
if cur.eof() {
fail(EmptyInput, "empty version string", 0)
}
let v = parse_version_at(cur, syn)
if syn.trim {
cur.skip_spaces()
}
if !cur.eof() {
let what = match cur.peek() {
Some(c) => describe_char(c)
None => "end of input"
}
fail(UnexpectedChar, "unexpected \{what} after the version", cur.pos)
}
v
}
///|
fn parse_version_at(cur : Cursor, syn : Syntax) -> Version raise Refused {
if syn.allow_v_prefix {
match cur.peek() {
Some('v') | Some('V') => cur.bump()
_ => ()
}
}
if syn.allow_equals_prefix {
if cur.eat('=') {
()
}
}
let major = parse_number(cur, "major")
if !cur.eat('.') {
if cur.eof() {
fail(
UnexpectedEnd,
"version ends early: expected '.' and a minor number",
cur.pos,
)
} else {
fail(UnexpectedChar, "expected the first '.' separator", cur.pos)
}
}
let minor = parse_number(cur, "minor")
if !cur.eat('.') {
if cur.eof() {
fail(
UnexpectedEnd,
"version ends early: expected '.' and a patch number",
cur.pos,
)
} else {
fail(UnexpectedChar, "expected the second '.' separator", cur.pos)
}
}
let patch = parse_number(cur, "patch")
let mut pre : Array[Identifier] = []
if cur.peek() == Some('-') {
cur.bump()
pre = parse_prerelease_ids(cur)
}
let mut build : Array[String] = []
if cur.peek() == Some('+') {
if !syn.allow_build_metadata {
fail(
UnsupportedSyntax,
"this grammar does not accept build metadata",
cur.pos,
)
}
cur.bump()
build = parse_build_ids(cur)
}
{ major, minor, patch, pre, build, }
}
///|
/// Parse a numeric component: decimal digits, no leading zero unless the value is `0`.
fn parse_number(cur : Cursor, what : String) -> @bigint.BigInt raise Refused {
let start = cur.pos
while true {
match cur.peek() {
Some(c) => if is_digit(c) { cur.bump() } else { break }
None => break
}
}
if cur.pos == start {
fail(InvalidNumber, "expected a decimal \{what} number here", cur.pos)
}
let text = cur.slice(start, cur.pos)
if text.length() > 1 && text.get_char(0) == Some('0') {
fail(LeadingZero, "\{what} number has a leading zero: \{text}", start)
}
@bigint.BigInt::from_string(text)
}
///|
/// Parse the prerelease identifiers after `-` (the `-` is already consumed).
fn parse_prerelease_ids(cur : Cursor) -> Array[Identifier] raise Refused {
let ids : Array[Identifier] = []
while true {
let start = cur.pos
while true {
match cur.peek() {
Some(c) => if is_identifier_char(c) { cur.bump() } else { break }
None => break
}
}
if cur.pos == start {
fail(EmptyIdentifier, "prerelease identifier is empty", cur.pos)
}
let text = cur.slice(start, cur.pos)
ids.push(parse_identifier(text, true, start))
if cur.eat('.') {
continue
} else {
break
}
}
ids
}
///|
/// Parse the build metadata identifiers after `+` (the `+` is already consumed).
fn parse_build_ids(cur : Cursor) -> Array[String] raise Refused {
let ids : Array[String] = []
while true {
let start = cur.pos
while true {
match cur.peek() {
Some(c) => if is_identifier_char(c) { cur.bump() } else { break }
None => break
}
}
if cur.pos == start {
fail(EmptyIdentifier, "build metadata identifier is empty", cur.pos)
}
ids.push(cur.slice(start, cur.pos))
if cur.eat('.') {
continue
} else {
break
}
}
ids
}
///|
/// Turn a run of identifier text into an `Identifier`.
///
/// All-digit text becomes numeric. A numeric prerelease identifier may not carry a
/// leading zero; build metadata may, since it never takes part in precedence.
fn parse_identifier(
text : String,
is_prerelease : Bool,
position : Int32,
) -> Identifier raise Refused {
let numeric = text.all(fn(c) { is_digit(c) })
if numeric {
if is_prerelease && text.length() > 1 && text.get_char(0) == Some('0') {
fail(
LeadingZero,
"numeric prerelease identifier has a leading zero: \{text}",
position,
)
}
Identifier::Num(@bigint.BigInt::from_string(text))
} else {
Identifier::Alpha(text)
}
}
///|
/// Compare precedence as laid out in SemVer 2.0.0 ยง11.
///
/// Returns `-1`, `0` or `1`. **Build metadata takes no part**: `1.0.0+a` and
/// `1.0.0+b` share the same precedence.
pub fn Version::precedence_cmp(self : Version, other : Version) -> Int32 {
let c = self.major.compare(other.major)
if c != 0 {
return c
}
let c = self.minor.compare(other.minor)
if c != 0 {
return c
}
let c = self.patch.compare(other.patch)
if c != 0 {
return c
}
compare_identifiers(self.pre, other.pre)
}
///|
fn compare_build(a : Array[String], b : Array[String]) -> Int32 {
if a.is_empty() && b.is_empty() {
return 0
}
if a.is_empty() {
return -1
}
if b.is_empty() {
return 1
}
let n = if a.length() < b.length() { a.length() } else { b.length() }
for i in 0.. b.length() {
1
} else {
0
}
}
///|
/// Build metadata ordering: numbers without a leading zero compare numerically,
/// everything else in ASCII order, numeric before alphanumeric. Ties only.
fn compare_build_identifier(a : String, b : String) -> Int32 {
let an = is_canonical_number(a)
let bn = is_canonical_number(b)
if an && bn {
@bigint.BigInt::from_string(a).compare(@bigint.BigInt::from_string(b))
} else if an {
-1
} else if bn {
1
} else {
a.lexical_compare(b)
}
}
///|
fn is_canonical_number(s : String) -> Bool {
if s.is_empty() {
return false
}
if !s.all(fn(c) { is_digit(c) }) {
return false
}
s.length() == 1 || s.get_char(0) != Some('0')
}
///|
/// The canonical form: `major.minor.patch[-prerelease][+build]`.
pub extend Version with Show::{to_string, output}
///|
pub impl Show for Version with fn to_string(self : Version) -> String {
let sb = StringBuilder()
sb.write_string(self.major.to_string())
sb.write_string(".")
sb.write_string(self.minor.to_string())
sb.write_string(".")
sb.write_string(self.patch.to_string())
if !self.pre.is_empty() {
sb.write_string("-")
for i in 0.. 0 {
sb.write_string(".")
}
sb.write_string(self.pre[i].to_string())
}
}
if !self.build.is_empty() {
sb.write_string("+")
for i in 0.. 0 {
sb.write_string(".")
}
sb.write_string(self.build[i])
}
}
sb.to_string()
}
///|
pub extend Version with @debug.Debug::{to_repr}
///|
pub impl @debug.Debug for Version with fn to_repr(self : Version) -> @debug.Repr {
@debug.Repr(self.to_string())
}
///|
/// Full equality: the numbers, the prerelease and the build metadata all match.
///
/// That differs from precedence: `1.0.0+a` and `1.0.0+b` are not equal, yet rank alike.
pub extend Version with Eq::{equal, not_equal}
///|
pub impl Eq for Version with fn equal(self : Version, other : Version) -> Bool {
self.major == other.major &&
self.minor == other.minor &&
self.patch == other.patch &&
self.pre == other.pre &&
self.build == other.build
}
///|
pub extend Version with Compare::{compare, op_lt, op_le, op_gt, op_ge}
///|
/// A total order: precedence first, then build metadata so sorting is deterministic.
///
/// `Array::sort` gives a reproducible order while `precedence_cmp` stays true to the spec.
pub impl Compare for Version with fn compare(self : Version, other : Version) -> Int32 {
let c = self.precedence_cmp(other)
if c != 0 {
return c
}
compare_build(self.build, other.build)
}
///|
pub extend Version with Hash::{hash_combine, hash}
///|
pub impl Hash for Version with fn hash_combine(self : Version, hasher : Hasher) -> Unit {
hasher.combine_string(self.to_string())
}
///|
pub extend Version with @json.ToJson::{to_json}
///|
pub impl @json.ToJson for Version with fn to_json(self : Version) -> Json {
Json::string(self.to_string())
}
///|
pub extend Version with @json.FromJson::{from_json}
///|
pub impl @json.FromJson for Version with fn from_json(
json : Json,
path : @json.JsonPath,
) -> Version raise @json.JsonDecodeError {
let s = if json is String(s) {
s
} else {
raise @json.JsonDecodeError((path, "moonsver: a version must be a string"))
}
Version::parse(s) catch {
e => raise @json.JsonDecodeError((path, "moonsver: " + e.to_string()))
}
}