Skip to content

SemVer

Canonical source: protos/lib/semver/SemVer.protos at locked revision e99b712ecf658a527b1fe82c8beba2c1286d8dc9.

// THE LICENSED WORK IS PROVIDED UNDER THE TERMS OF THE ADAPTIVE PUBLIC LICENSE
// ("LICENSE") AS FIRST COMPLETED BY: Guillermo Adrián Molina. ANY USE, PUBLIC
// DISPLAY, PUBLIC PERFORMANCE, REPRODUCTION OR DISTRIBUTION OF, OR PREPARATION OF
// DERIVATIVE WORKS BASED ON, THE LICENSED WORK CONSTITUTES RECIPIENT'S ACCEPTANCE
// OF THIS LICENSE AND ITS TERMS, WHETHER OR NOT SUCH RECIPIENT READS THE TERMS OF
// THE LICENSE. "LICENSED WORK" AND "RECIPIENT" ARE DEFINED IN THE LICENSE. A COPY
// OF THE LICENSE IS LOCATED IN THE TEXT FILE ENTITLED "LICENSE.TXT" ACCOMPANYING
// THE CONTENTS OF THIS FILE. IF A COPY OF THE LICENSE DOES NOT ACCOMPANY THIS
// FILE, A COPY OF THE LICENSE MAY ALSO BE OBTAINED AT THE FOLLOWING WEB SITE:
// https://github.com/guillermomolina/protos
//
// Software distributed under the License is distributed on an "AS IS" basis,
// WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
// the specific language governing rights and limitations under the License.
//! Strict Semantic Versioning 2.0.0 version values. A version is an ordinary
//! object with Integer `major`, `minor`, and `patch` slots, a `prerelease` Array
//! of Integer (numeric) and String (alphanumeric) identifiers, and a `build`
//! Array of String identifiers. Build metadata is preserved exactly.
//!
//! Versions answer complete semantic equality through `==`: two versions are
//! equal when their core components, prerelease identifiers, and build
//! identifiers are equal in order, with numeric and alphanumeric identifiers
//! never equal to each other. `hash` is coherent with that equality, so
//! independently parsed equal versions work as the same Map key. `===` remains
//! ordinary object identity. SemVer precedence (`comparePrecedence`) is a
//! distinct relation that ignores build metadata.
/// Parses `text` as exactly `MAJOR.MINOR.PATCH[-PRERELEASE][+BUILD]` under
/// Semantic Versioning 2.0.0 and answers a fresh version object. Core
/// components and numeric prerelease identifiers are unbounded Integers;
/// alphanumeric prerelease identifiers and every build identifier keep their
/// exact ASCII spelling as Strings, so `+001` stays `"001"`. A non-String
/// argument, a missing component, a leading zero in a core component or numeric
/// prerelease identifier, an empty identifier, or any character outside the
/// SemVer grammar signals an Error. No prefix, whitespace, or partial version
/// is accepted.
parse: (text) => {
String.recognizes(text).ifFalse(() => {
Error().signal()
})
source: Encoding.UTF8.encode(text)
isDigit: (octet) => {
(octet >= 48) && (octet <= 57)
}
isIdentifierOctet: (octet) => {
upper: (octet >= 65) && (octet <= 90)
lower: (octet >= 97) && (octet <= 122)
isDigit(octet) || upper || lower || (octet == 45)
}
// Answers a fresh Array of `count` nulls in O(count): Arrays do not grow,
// and doubling halves keeps the copying geometric.
sized: null
sized = (count) => {
result: Array()
(count > 0).ifTrue(() => {
half: sized(count.div(2))
result = Array(...half, ...half)
((count % 2) == 1).ifTrue(() => {
result = Array(...result, null)
})
})
result
}
// Identifiers are collected under consecutive Integer keys and
// materialized once, keeping parsing linear in the input length.
materialize: (entries) => {
result: sized(entries.size())
index: 0
result.each((ignored) => {
result[index] = entries[index]
index = index + 1
})
result
}
current: Encoding.UTF8.encode("")
currentNumeric: true
takeCurrent: () => {
(current.size() == 0).ifTrue(() => {
Error().signal()
})
taken: current
current = Encoding.UTF8.encode("")
currentNumeric = true
taken
}
// Numeric core components and prerelease identifiers reject leading zeros.
decimal: (bytes) => {
((bytes.size() > 1) && (bytes[0] == 48)).ifTrue(() => {
Error().signal()
})
value: 0
bytes.each((octet) => {
value = value * 10 + (octet - 48)
})
value
}
components: Map()
prerelease: Map()
build: Map()
finishCore: () => {
components[components.size()] = decimal(takeCurrent())
}
finishPrerelease: () => {
numeric: currentNumeric
bytes: takeCurrent()
identifier: null
numeric.ifTrue(() => {
identifier = decimal(bytes)
})
numeric.ifFalse(() => {
identifier = Encoding.UTF8.decode(bytes)
})
prerelease[prerelease.size()] = identifier
}
finishBuild: () => {
build[build.size()] = Encoding.UTF8.decode(takeCurrent())
}
// Sections: "core" (MAJOR.MINOR.PATCH), "prerelease", and "build".
section: "core"
source.each((octet) => {
previousSection: section
(previousSection == "core").ifTrue(() => {
handled: false
isDigit(octet).ifTrue(() => {
current.add(octet)
handled = true
})
handled.ifFalse(() => {
(octet == 46).ifTrue(() => {
(components.size() < 2).ifFalse(() => {
Error().signal()
})
finishCore()
handled = true
})
})
handled.ifFalse(() => {
((octet == 45) || (octet == 43)).ifTrue(() => {
(components.size() == 2).ifFalse(() => {
Error().signal()
})
finishCore()
(octet == 45).ifTrue(() => {
section = "prerelease"
})
(octet == 43).ifTrue(() => {
section = "build"
})
handled = true
})
})
handled.ifFalse(() => {
Error().signal()
})
})
(previousSection == "prerelease").ifTrue(() => {
handled: false
(octet == 46).ifTrue(() => {
finishPrerelease()
handled = true
})
handled.ifFalse(() => {
(octet == 43).ifTrue(() => {
finishPrerelease()
section = "build"
handled = true
})
})
handled.ifFalse(() => {
isIdentifierOctet(octet).ifFalse(() => {
Error().signal()
})
isDigit(octet).ifFalse(() => {
currentNumeric = false
})
current.add(octet)
})
})
(previousSection == "build").ifTrue(() => {
(octet == 46).ifTrue(() => {
finishBuild()
})
(octet == 46).ifFalse(() => {
isIdentifierOctet(octet).ifFalse(() => {
Error().signal()
})
current.add(octet)
})
})
})
(section == "core").ifTrue(() => {
(components.size() == 2).ifFalse(() => {
Error().signal()
})
finishCore()
})
(section == "prerelease").ifTrue(() => {
finishPrerelease()
})
(section == "build").ifTrue(() => {
finishBuild()
})
isVersion: (value) => {
(value !== null) &&
value.hasSlot("major") &&
value.hasSlot("minor") &&
value.hasSlot("patch") &&
value.hasSlot("prerelease") &&
value.hasSlot("build") &&
Integer.recognizes(value.major) &&
Integer.recognizes(value.minor) &&
Integer.recognizes(value.patch) &&
Array.recognizes(value.prerelease) &&
Array.recognizes(value.build)
}
// Identifiers are equal only within the same family: Integer 1 never
// equals String "1".
sameIdentifiers: (left, right) => {
same: left.size() == right.size()
index: 0
(() => same && (index < left.size())).whileTrue() {
first: left[index]
second: right[index]
same = (Integer.recognizes(first) == Integer.recognizes(second)) &&
(String.recognizes(first) == String.recognizes(second)) &&
(first == second)
index = index + 1
}
same
}
// Hashes are folded modulo a fixed prime so the accumulator stays bounded.
mix: (accumulated, value) => {
(accumulated * 31 + value) % 2305843009213693951
}
version: {
major: components[0]
minor: components[1]
patch: components[2]
prerelease: materialize(prerelease)
build: materialize(build)
equals: (other) => {
version: this
isVersion(other) &&
(version.major == other.major) &&
(version.minor == other.minor) &&
(version.patch == other.patch) &&
sameIdentifiers(version.prerelease, other.prerelease) &&
sameIdentifiers(version.build, other.build)
}
hash: () => {
version: this
result: mix(mix(mix(17, version.major.hash()), version.minor.hash()), version.patch.hash())
result = mix(result, version.prerelease.size())
version.prerelease.each((identifier) => {
family: 1
String.recognizes(identifier).ifTrue(() => {
family = 2
})
result = mix(mix(result, family), identifier.hash())
})
result = mix(result, version.build.size())
version.build.each((identifier) => {
result = mix(result, identifier.hash())
})
result
}
}
// The equality and hash Closures capture this activation; release the
// scratch state so a version retains only its own slots.
source = null
current = null
components = null
prerelease = null
build = null
version.alias("equals", "==")
}
/// Answers the canonical text `MAJOR.MINOR.PATCH[-PRERELEASE][+BUILD]` of a
/// version answered by `parse`. Integers are written in decimal and String
/// identifiers keep their exact spelling, so `format(parse(text)) == text` for
/// every accepted `text`. A value without that representation, an empty
/// String identifier, an alphanumeric prerelease identifier made only of
/// digits, or a character outside the SemVer identifier alphabet signals an
/// Error.
format: (version) => {
// Requires the representation answered by `parse`.
requireVersion: (version) => {
(Integer.recognizes(version.major) &&
Integer.recognizes(version.minor) &&
Integer.recognizes(version.patch) &&
Array.recognizes(version.prerelease) &&
Array.recognizes(version.build)).ifFalse(() => {
Error().signal()
})
((version.major >= 0) && (version.minor >= 0) && (version.patch >= 0)).ifFalse(() => {
Error().signal()
})
version.prerelease.each((identifier) => {
numeric: Integer.recognizes(identifier) && (identifier >= 0)
(numeric || String.recognizes(identifier)).ifFalse(() => {
Error().signal()
})
})
version.build.each((identifier) => {
String.recognizes(identifier).ifFalse(() => {
Error().signal()
})
})
}
requireVersion(version)
isDigit: (octet) => {
(octet >= 48) && (octet <= 57)
}
isIdentifierOctet: (octet) => {
upper: (octet >= 65) && (octet <= 90)
lower: (octet >= 97) && (octet <= 122)
isDigit(octet) || upper || lower || (octet == 45)
}
output: Encoding.UTF8.encode("")
// Digits are produced least significant first and written in reverse.
appendDecimal: (value) => {
digits: Map()
remaining: value
(value == 0).ifTrue(() => {
digits[0] = 48
})
(() => remaining > 0).whileTrue() {
digits[digits.size()] = 48 + (remaining % 10)
remaining = remaining.div(10)
}
index: digits.size() - 1
(() => index >= 0).whileTrue() {
output.add(digits[index])
index = index - 1
}
}
appendText: (text, alphanumeric) => {
bytes: Encoding.UTF8.encode(text)
(bytes.size() == 0).ifTrue(() => {
Error().signal()
})
hasNonDigit: false
bytes.each((octet) => {
isIdentifierOctet(octet).ifFalse(() => {
Error().signal()
})
isDigit(octet).ifFalse(() => {
hasNonDigit = true
})
output.add(octet)
})
(alphanumeric && (hasNonDigit == false)).ifTrue(() => {
Error().signal()
})
}
appendIdentifiers: (separator, identifiers, alphanumeric) => {
next: separator
identifiers.each((identifier) => {
output.add(next)
next = 46
Integer.recognizes(identifier).ifTrue(() => {
appendDecimal(identifier)
})
String.recognizes(identifier).ifTrue(() => {
appendText(identifier, alphanumeric)
})
})
}
appendDecimal(version.major)
output.add(46)
appendDecimal(version.minor)
output.add(46)
appendDecimal(version.patch)
appendIdentifiers(45, version.prerelease, true)
appendIdentifiers(43, version.build, false)
Encoding.UTF8.decode(output)
}
/// Answers `-1`, `0`, or `1` as version `left` has lower, equal, or higher
/// Semantic Versioning 2.0.0 precedence than version `right`. Core components
/// compare numerically; a version without prerelease identifiers outranks one
/// with them; prerelease identifiers compare left to right, numeric ones
/// numerically and below every alphanumeric one, alphanumeric ones by ASCII
/// octet order; a longer prerelease list outranks its prefix. Build metadata
/// never participates, so precedence `0` does not imply `==`. Arguments must
/// have the representation answered by `parse`; otherwise an Error is
/// signalled.
comparePrecedence: (left, right) => {
// Requires the representation answered by `parse`.
requireVersion: (version) => {
(Integer.recognizes(version.major) &&
Integer.recognizes(version.minor) &&
Integer.recognizes(version.patch) &&
Array.recognizes(version.prerelease) &&
Array.recognizes(version.build)).ifFalse(() => {
Error().signal()
})
((version.major >= 0) && (version.minor >= 0) && (version.patch >= 0)).ifFalse(() => {
Error().signal()
})
version.prerelease.each((identifier) => {
numeric: Integer.recognizes(identifier) && (identifier >= 0)
(numeric || String.recognizes(identifier)).ifFalse(() => {
Error().signal()
})
})
version.build.each((identifier) => {
String.recognizes(identifier).ifFalse(() => {
Error().signal()
})
})
}
requireVersion(left)
requireVersion(right)
compareIntegers: (first, second) => {
result: 0
(first < second).ifTrue(() => {
result = -1
})
(first > second).ifTrue(() => {
result = 1
})
result
}
compareText: (first, second) => {
firstBytes: Encoding.UTF8.encode(first)
secondBytes: Encoding.UTF8.encode(second)
limit: firstBytes.size()
(secondBytes.size() < limit).ifTrue(() => {
limit = secondBytes.size()
})
result: 0
index: 0
(() => (result == 0) && (index < limit)).whileTrue() {
result = compareIntegers(firstBytes[index], secondBytes[index])
index = index + 1
}
(result == 0).ifTrue(() => {
result = compareIntegers(firstBytes.size(), secondBytes.size())
})
result
}
compareIdentifiers: (first, second) => {
firstNumeric: Integer.recognizes(first)
secondNumeric: Integer.recognizes(second)
result: 0
(firstNumeric && secondNumeric).ifTrue(() => {
result = compareIntegers(first, second)
})
(firstNumeric && (!secondNumeric)).ifTrue(() => {
result = -1
})
((!firstNumeric) && secondNumeric).ifTrue(() => {
result = 1
})
((!firstNumeric) && (!secondNumeric)).ifTrue(() => {
result = compareText(first, second)
})
result
}
result: compareIntegers(left.major, right.major)
(result == 0).ifTrue(() => {
result = compareIntegers(left.minor, right.minor)
})
(result == 0).ifTrue(() => {
result = compareIntegers(left.patch, right.patch)
})
(result == 0).ifTrue(() => {
leftSize: left.prerelease.size()
rightSize: right.prerelease.size()
((leftSize == 0) || (rightSize == 0)).ifTrue(() => {
// A release outranks any prerelease of the same core.
result = compareIntegers(rightSize, leftSize)
})
((leftSize > 0) && (rightSize > 0)).ifTrue(() => {
limit: leftSize
(rightSize < limit).ifTrue(() => {
limit = rightSize
})
index: 0
(() => (result == 0) && (index < limit)).whileTrue() {
result = compareIdentifiers(left.prerelease[index], right.prerelease[index])
index = index + 1
}
(result == 0).ifTrue(() => {
result = compareIntegers(leftSize, rightSize)
})
})
})
result
}

← semver family · Standard-library source