mirror of
https://github.com/apple/pkl.git
synced 2026-01-11 14:20:35 +01:00
229 lines
9.9 KiB
Plaintext
229 lines
9.9 KiB
Plaintext
//===----------------------------------------------------------------------===//
|
||
// Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
|
||
//
|
||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||
// you may not use this file except in compliance with the License.
|
||
// You may obtain a copy of the License at
|
||
//
|
||
// https://www.apache.org/licenses/LICENSE-2.0
|
||
//
|
||
// Unless required by applicable law or agreed to in writing, software
|
||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
// See the License for the specific language governing permissions and
|
||
// limitations under the License.
|
||
//===----------------------------------------------------------------------===//
|
||
|
||
/// Parsing, comparison, and manipulation of [semantic version](https://semver.org/spec/v2.0.0.html) numbers.
|
||
@ModuleInfo { minPklVersion = "0.31.0" }
|
||
module pkl.semver
|
||
|
||
/// Tells whether [version] is a valid semantic version number.
|
||
function isValid(version: String) = parseOrNull(version) != null
|
||
|
||
/// Parses [version] as a semantic version number.
|
||
///
|
||
/// Throws if [version] is not a valid semantic version number.
|
||
///
|
||
/// Examples:
|
||
/// ```
|
||
/// semver.Version("1.2.3")
|
||
/// semver.Version("1.2.3-alpha")
|
||
/// semver.Version("1.2.3+456")
|
||
/// semver.Version("1.2.3-alpha+456")
|
||
/// ```
|
||
function Version(version: String): Version =
|
||
parseOrNull(version) ?? throw("`\(version)` is not a valid semantic version number.")
|
||
|
||
/// Parses [version] as a semantic version number.
|
||
///
|
||
/// Returns [null] if [version] is not a valid semantic version number.
|
||
///
|
||
/// Facts:
|
||
/// ```
|
||
/// semver.parseOrNull("1.2.3") == semver.Version("1.2.3")
|
||
/// semver.parseOrNull("1.2.3-alpha+456") == semver.Version("1.2.3-alpha+456")
|
||
/// semver.parseOrNull("1") == null
|
||
/// ```
|
||
function parseOrNull(version: String): Version? =
|
||
let (groups = versionRegex.matchEntire(version)?.groups)
|
||
if (groups == null)
|
||
null
|
||
else
|
||
new Version {
|
||
major = groups[1].value.toInt()
|
||
minor = groups[2].value.toInt()
|
||
patch = groups[3].value.toInt()
|
||
preRelease = groups[4]?.value
|
||
build = groups[5]?.value
|
||
}
|
||
|
||
/// A version comparator for use with methods such as [List.minWith()].
|
||
comparator: (Version, Version) -> Boolean = (v1: Version, v2: Version) -> v1.isLessThan(v2)
|
||
|
||
/// A [semantic version](https://semver.org/spec/v2.0.0.html).
|
||
///
|
||
/// To test if two versions are equal according to semantic versioning rules, use [equals()] instead of `==`.
|
||
class Version {
|
||
/// Major version zero (0.y.z) is for initial development.
|
||
/// Anything MAY change at any time.
|
||
/// The public API SHOULD NOT be considered stable.
|
||
///
|
||
/// Version 1.0.0 defines the public API.
|
||
/// The way in which the version number is incremented after this release is dependent on this public API and how it changes.
|
||
///
|
||
/// Major version X (X.y.z | X > 0) MUST be incremented if any backwards incompatible changes are introduced to the public API.
|
||
/// It MAY also include minor and patch level changes.
|
||
/// Patch and minor version MUST be reset to 0 when major version is incremented.
|
||
major: UInt
|
||
|
||
/// Minor version Y (x.Y.z | x > 0) MUST be incremented if new, backwards compatible functionality is introduced to the public API.
|
||
/// It MUST be incremented if any public API functionality is marked as deprecated.
|
||
/// It MAY be incremented if substantial new functionality or improvements are introduced within the private code.
|
||
/// It MAY include patch level changes.
|
||
/// Patch version MUST be reset to 0 when minor version is incremented.
|
||
minor: UInt
|
||
|
||
/// Patch version Z (x.y.Z | x > 0) MUST be incremented if only backwards compatible bug fixes are introduced.
|
||
/// A bug fix is defined as an internal change that fixes incorrect behavior.
|
||
patch: UInt
|
||
|
||
/// A pre-release version MAY be denoted by appending a hyphen and a series of dot separated identifiers immediately following the patch version.
|
||
/// Identifiers MUST comprise only ASCII alphanumerics and hyphens `[0-9A-Za-z-]`.
|
||
/// Identifiers MUST NOT be empty. Numeric identifiers MUST NOT include leading zeroes.
|
||
/// Pre-release versions have a lower precedence than the associated normal version.
|
||
/// A pre-release version indicates that the version is unstable and might not satisfy the intended compatibility requirements as denoted by its associated normal version.
|
||
///
|
||
/// Examples:
|
||
/// - `"1.0.0-alpha"`
|
||
/// - `"1.0.0-alpha.1"`
|
||
/// - `"1.0.0-0.3.7"`
|
||
/// - `"1.0.0-x.7.z.92"`
|
||
/// - `"1.0.0-x-y-z.–"`
|
||
preRelease: String(
|
||
matches(
|
||
Regex(
|
||
#"(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*"#,
|
||
),
|
||
),
|
||
)?
|
||
|
||
hidden fixed preReleaseIdentifiers: List<Int | String> =
|
||
if (preRelease == null) List() else preRelease.split(".").map((it) -> it.toIntOrNull() ?? it)
|
||
|
||
/// Build metadata MAY be denoted by appending a plus sign and a series of dot separated identifiers immediately following the patch or pre-release version.
|
||
/// Identifiers MUST comprise only ASCII alphanumerics and hyphens `[0-9A-Za-z-]`.
|
||
/// Identifiers MUST NOT be empty.
|
||
/// Build metadata MUST be ignored when determining version precedence.
|
||
/// Thus two versions that differ only in the build metadata, have the same precedence.
|
||
///
|
||
/// Examples:
|
||
/// - `"1.0.0-alpha+001"
|
||
/// - `"1.0.0+20130313144700"`
|
||
/// - `"1.0.0-beta+exp.sha.5114f85"`
|
||
/// - `"1.0.0+21AF26D3—-117B344092BD"`
|
||
///
|
||
/// Note: Unlike `==`, [equals()] and comparison methods such as [isLessThan()] ignore [build].
|
||
build: String(matches(Regex(#"[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*"#)))?
|
||
|
||
hidden fixed buildIdentifiers: List<String> = if (build == null) List() else build.split(".")
|
||
|
||
/// Tells whether this version is equal to [other] according to semantic versioning rules.
|
||
///
|
||
/// Facts:
|
||
/// ```
|
||
/// semver.Version("1.0.0").equals(semver.Version("1.0.0"))
|
||
/// !(semver.Version("1.0.0").equals(semver.Version("1.0.1")))
|
||
/// semver.Version("1.0.0-alpha+001").equals(semver.Version("1.0.0-alpha+999"))
|
||
/// ```
|
||
///
|
||
/// Note: `version1.equals(version2)` differs from `version1 == version2` in that it ignores [build].
|
||
function equals(other: Version): Boolean =
|
||
major == other.major
|
||
&& minor == other.minor
|
||
&& patch == other.patch
|
||
&& preRelease == other.preRelease
|
||
|
||
/// Tells whether this version is less than [other] according to semantic versioning rules.
|
||
///
|
||
/// Facts:
|
||
/// ```
|
||
/// semver.Version("1.0.0").isLessThan(semver.Version("2.0.0"))
|
||
/// semver.Version("2.0.0").isLessThan(semver.Version("2.1.0"))
|
||
/// semver.Version("2.1.0").isLessThan(semver.Version("2.1.1"))
|
||
///
|
||
/// semver.Version("1.0.0-alpha").isLessThan("1.0.0")
|
||
///
|
||
/// semver.Version("1.0.0-alpha").isLessThan(semver.Version("1.0.0-alpha.1"))
|
||
/// semver.Version("1.0.0-alpha.1").isLessThan(semver.Version("1.0.0-alpha.beta"))
|
||
/// semver.Version("1.0.0-alpha.beta").isLessThan(semver.Version("1.0.0-beta"))
|
||
/// semver.Version("1.0.0-beta").isLessThan(semver.Version("1.0.0-beta.2"))
|
||
/// semver.Version("1.0.0-beta.2").isLessThan(semver.Version("1.0.0-beta.11"))
|
||
/// semver.Version("1.0.0-beta.11").isLessThan(semver.Version("1.0.0-rc.1"))
|
||
/// semver.Version("1.0.0-rc.1").isLessThan(semver.Version("1.0.0"))
|
||
/// ```
|
||
function isLessThan(other: Version): Boolean =
|
||
major < other.major
|
||
|| major == other.major && minor < other.minor
|
||
|| major == other.major && minor == other.minor && patch < other.patch
|
||
|| major == other.major
|
||
&& minor == other.minor
|
||
&& patch == other.patch
|
||
&& isPreReleaseLessThan(other)
|
||
|
||
/// Tells whether this version is less than or equal to [other] according to semantic versioning rules.
|
||
function isLessThanOrEquals(other: Version): Boolean = isLessThan(other) || equals(other)
|
||
|
||
/// Tells whether this version is greater than [other] according to semantic versioning rules.
|
||
function isGreaterThan(other: Version): Boolean = other.isLessThan(this)
|
||
|
||
/// Tells whether this version is greater than or equal to [other] according to semantic versioning rules.
|
||
function isGreaterThanOrEquals(other: Version): Boolean = other.isLessThanOrEquals(this)
|
||
|
||
/// A normal version number MUST take the form X.Y.Z where X, Y, and Z are non-negative integers, and MUST NOT contain leading zeroes.
|
||
function isNormal(): Boolean = preRelease == null && build == null
|
||
|
||
/// Tells if this version has a non-zero [major] and no [preRelease].
|
||
function isStable(): Boolean = major > 0 && preRelease == null
|
||
|
||
/// Strips [preRelease] and [build] from this version.
|
||
function toNormal(): Version = (this) { preRelease = null; build = null }
|
||
|
||
function toString() =
|
||
"\(major).\(minor).\(patch)\(if (preRelease != null) "-\(preRelease)" else "")\(if (build != null) "+\(build)" else "")"
|
||
|
||
local function isPreReleaseLessThan(other: Version): Boolean =
|
||
if (preRelease == null)
|
||
false
|
||
else if (other.preRelease == null)
|
||
true
|
||
else if (preRelease == other.preRelease)
|
||
false
|
||
else
|
||
let (
|
||
result =
|
||
preReleaseIdentifiers
|
||
.zip(other.preReleaseIdentifiers)
|
||
.fold(null, (result, next) ->
|
||
if (result != null)
|
||
result
|
||
else if (next.first == next.second)
|
||
null
|
||
else if (next.first.getClass() == next.second.getClass())
|
||
next.first < next.second
|
||
else
|
||
next.first is Int
|
||
)
|
||
)
|
||
if (result != null)
|
||
result
|
||
else
|
||
preReleaseIdentifiers.length < other.preReleaseIdentifiers.length
|
||
}
|
||
|
||
// https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
|
||
local versionRegex =
|
||
Regex(
|
||
#"(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?"#,
|
||
)
|