diff --git a/core/stdlib/std.ncl b/core/stdlib/std.ncl index 2c15ab2ea..7e197ecfc 100644 --- a/core/stdlib/std.ncl +++ b/core/stdlib/std.ncl @@ -2957,126 +2957,215 @@ package = let rec - # https://semver.org is kind enough to supply this "official" semver regex. - semver_re = m%"^(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-]+)*))?$"%, - # Just the major.minor.patch part, with minor and patch being optional. - partial_semver_re = m%"^(0|[1-9]\d*)(\.(0|[1-9]\d*))?(\.(0|[1-9]\d*))?$"%, - # An exact version constraint. This one is required to have minor and patch versions, and it's allowed to have a prerelease. - semver_equals_req_re = m%"^=(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-]*))*))?$"%, - semver_req_re = "(%{partial_semver_re})|(%{semver_equals_req_re})", - Semver = { - major | Number, - minor | Number, - patch | Number, - pre - | String - | default - = "", - build - | String - | default - = "", - }, - in { - NormalizeSemver - | doc m%" - ```nickel - "1.2.3-pre1+build" | NormalizeSemver - # => { major = 1, minor = 2, patch = 3, pre = "", build = "build" } - ``` - "% - = - %contract/custom% (fun _label value => - if %typeof% value == 'Record then - 'Ok (value | Semver) - else if %typeof% value == 'String then - let matches = string.find semver_re value in - if matches.index == -1 then - 'Error { message = "invalid semver" } - else - let gs = matches.groups in - 'Ok { - major = gs |> array.at 0 |> string.to_number, - minor = gs |> array.at 1 |> string.to_number, - patch = gs |> array.at 2 |> string.to_number, - pre = gs |> array.at 3, - build = gs |> array.at 4 - } - else - 'Error { message = "expected a string or a record" } - ), - is_semver_req - : String -> Bool - | doc m%" - Returns true if a string is a valid version requirement in Nickel. - - See the `SemverReq` contract for more details. - "% - = std.string.is_match semver_req_re, - is_semver - : String -> Bool - | doc m%" - Returns true if a string is a valid semantic version. - - # Examples - - ```nickel multiline - std.package.is_semver "1.2.0-pre1" - # => true + semver_re = m%"^(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-]+)*))?$"%, + # Just the major.minor.patch part, with minor and patch being optional. + partial_semver_re = m%"^(0|[1-9]\d*)(?:\.(0|[1-9]\d*))?(?:\.(0|[1-9]\d*))?$"%, + # An exact version constraint. This one is required to have minor and patch versions, and it's allowed to have a prerelease. + semver_equals_req_re = m%"^=(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-]*))*))?$"%, + semver_req_re = "(%{partial_semver_re})|(%{semver_equals_req_re})", + in + { + structured = { + Semver + | doc m%" + A contract for semantic version ("semver") identifiers, structured as a record. - std.package.is_semver "1.foo" - # => false - ``` - "% - = std.string.is_match semver_re, - is_semver_prefix - : String -> Bool - | doc m%" - Returns true if a string is a valid semantic version prefix, - containing a major version and then optional minor and patch versions. + See also `std.package.Semver`, which can produce one of these by parsing a string. + "% + = { + major + | number.Nat + | doc "The major version number.", + minor + | number.Nat + | doc "The minor version number.", + patch + | number.Nat + | doc "The patch version.", + pre + | String + | doc m%" + The prerelease identifier. - # Examples + When comparing versions for compatibility, non-empty prerelease + strings only match one another if they are exactly equal. See + `std.package.SemverReq` for more details + "% + | optional, + build + | String + | doc m%" + The build metadata. - ```nickel multiline - std.package.is_semver_prefix "1.2" - # => true + This is completely ignored while comparing versions for compatibility. + "% + | optional, + }, + ExactSemverReq + | doc m%" + A contract for exact semantic version ("semver") requirements, structured as a record. - std.package.is_semver_prefix "1.foo" - # => false - ``` - "% - = std.string.is_match partial_semver_re, + This differs from `std.package.structured.Semver` in that it lacks a build metadata field. + "% + = { + major + | number.Nat + | doc "The major version number.", + minor + | number.Nat + | doc "The minor version number.", + patch + | number.Nat + | doc "The patch version.", + pre + | String + | doc "The prerelease identifier." + | optional, + }, + SemverPrefix + | doc m%" + A contract for semantic version ("semver") prefixes, structured as a record. + "% + = { + major + | number.Nat + | doc "The major version number.", + minor + | number.Nat + | doc "The minor version number." + | optional, + patch + | number.Nat + | doc "The patch version." + | optional, + }, + SemverReq = [| 'Compatible SemverPrefix, 'Exact ExactSemverReq |] + }, Semver | doc m%" A contract for semantic version ("semver") identifiers. + This is a normalizing contract, which accepts either a string to be parsed + or a record. If a string is provided, it will be parsed into a record (in + the `SemverRecord` format). + # Examples ```nickel multiline "1.2.0-pre1" | std.package.Semver - # => "1.2.0-pre1" + # => { major = 1, minor = 2, patch = 0, pre = "pre1", } + + { major = 1, minor = 2, patch = 0 } | std.package.Semver + # => { major = 1, minor = 2, patch = 0, } + + { major = 1, minor = 2 } | std.package.Semver + # => error: missing definition "1.foo" | std.package.Semver # => error: contract broken by a value ``` "% - = std.contract.from_predicate is_semver, + = + %contract/custom% (fun _label value => + if %typeof% value == 'Record then + 'Ok (value | structured.Semver) + else if %typeof% value == 'String then + let matches = string.find semver_re value in + if matches.index == -1 then + 'Error { message = "invalid semver" } + else + let gs = matches.groups in + let + pre_ = gs |> array.at 3, + build_ = gs |> array.at 4, + in + 'Ok ( + ( + { + major = gs |> array.at 0 |> string.to_number, + minor = gs |> array.at 1 |> string.to_number, + patch = gs |> array.at 2 |> string.to_number, + } + & (if pre_ != "" then { pre = pre_ } else {}) + & (if build_ != "" then { build = build_ } else {}) + ) | structured.Semver + ) + else + 'Error { message = "expected a string or a record" } + ), SemverPrefix | doc m%" - A contract for semantic version ("semver") prefixes, - containing a major version and then optional minor and patch versions. + A contract for semantic version ("semver") prefixes. + + This prefix must contain a major version number. It may contain a minor + version and a patch version, but it must not contain a prerelease version + or build metadata. # Examples ```nickel multiline "1.2" | std.package.SemverPrefix - # => "1.2" + # => { major = 1, minor = 2 } + + { major = 1, minor = 2 } | std.package.SemverPrefix + # => { major = 1, minor = 2 } "1.foo" | std.package.SemverPrefix # => error: contract broken by a value ``` "% - = std.contract.from_predicate is_semver_prefix, + = + %contract/custom% (fun _label value => + if %typeof% value == 'Record then + 'Ok (value | structured.SemverPrefix) + else if %typeof% value == 'String then + let matches = string.find partial_semver_re value in + if matches.index == -1 then + 'Error { message = "invalid semver" } + else + let gs = matches.groups in + let + minor_ = gs |> array.at 1, + patch_ = gs |> array.at 2, + in + 'Ok ( + ( + { major = gs |> array.at 0 |> string.to_number } + & (if minor_ == "" then {} else { minor = string.to_number minor_ }) + & (if patch_ == "" then {} else { patch = string.to_number patch_ }) + ) | structured.SemverPrefix + ) + else + 'Error { message = "expected a string or a record" } + ), + ExactSemverReq + | doc m%" + A contract for exact semantic version ("semver") requirements, structured as a record. + "% + = + %contract/custom% (fun _label value => + if %typeof% value == 'Record then + 'Ok (value | structured.ExactSemverReq) + else if %typeof% value == 'String then + let matches = string.find semver_equals_req_re value in + if matches.index == -1 then + 'Error { message = "invalid semver" } + else + let gs = matches.groups in + let pre_ = array.at 3 gs in + 'Ok ( + ( + { + major = gs |> array.at 0 |> string.to_number, + minor = gs |> array.at 1 |> string.to_number, + patch = gs |> array.at 2 |> string.to_number, + } + & (if pre_ == "" then {} else { pre = pre_ }) + ) | structured.ExactSemverReq + ) + else + 'Error { message = "expected a string or a record" } + ), SemverReq | doc m%" A contract for semantic version ("semver") requirements. @@ -3100,143 +3189,190 @@ ```nickel multiline "1.2" | SemverReq - # => "1.2" + # => 'Compatible { major = 1, minor = 2 } "=1.2" | SemverReq # => error: contract broken by a value "1.2.0" | SemverReq - # => "1.2.0" + # => 'Compatible { major = 1, minor = 2, patch = 0 } "=1.2.0" | SemverReq - # => "=1.2.0" + # => 'Exact { major = 1, minor = 2, patch = 0, } "1.2.0-pre1" | SemverReq # => error: contract broken by a value "=1.2.0-pre1" | SemverReq - # => "=1.2.0-pre1" + # => 'Exact { major = 1, minor = 2, patch = 0, pre = "pre1", } ``` "% - = std.contract.from_predicate is_semver_req, - # TODO: bikeshedding opportunity: which fields should be optional? - Manifest = { - name - | String - | doc m%" + = + %contract/custom% (fun _label value => + if %typeof% value == 'Record then + 'Ok (value | structured.SemverReq) + else if %typeof% value == 'String then + if string.is_match semver_equals_req_re value then + 'Ok ('Exact (value | ExactSemverReq)) + else + 'Ok ('Compatible (value | SemverPrefix)) + else + 'Error { message = "expected a string or a record" } + ), + + GitDependency + | doc m%" + A contract identifying a dependency that can be fetched from a git repository. + "% + = { + url + | String + | doc m%" + The url of a git repository. + + This supports local file paths, https urls like `https://example.com/example-repo`, + and ssh urls like `user@example.com:repo`. + "%, + ref + | [| 'Head, 'Branch String, 'Tag String, 'Commit String |] + | optional + | doc m%" + The git ref to fetch from the repository. + + If not provided, defaults to 'Head. + "%, + path + | String + | optional + | doc m%" + The path of the nickel package within the git repository. If omitted, the nickel package + is at the root of the git repository. + "%, + }, + + IndexDependency + | doc m%" + A contract identifying a dependency that can be fetched from a package index. + "% + = { + package + | String + | doc m%" + The dependency's identifier within the package index, in the format "github//". + "%, + version + | SemverReq + | doc m%" + The required version of the package. + + Nickel supports two kinds of requirements: semver-compatible + requirements and exact version requirements. Semver-compatible + requirements take the form "major.minor.patch", where minor and patch + are optional. Their semantics are: + + - "1.2.3" will match all versions having major version 1, minor version 2, + and patch version at least 3. + - "1.2" will match all versions having major version 1 and minor version + at least 2. + - "1" will match all versions having major version 1. + - a semver-compatible requirement will never match a prerelease version. + + Exact version requirements take the form "=major.minor.patch-pre", where + the prerelease tag is optional, but major, minor, and patch are all required. + "%, + }, + + Manifest + | doc m%" + A contract for a Nickel package manifest. + + # Example + + ```nickel + { + name = "my-package", + version = "1.0.0", + minimal_nickel_version = "1.10", + authors = ["Me "], + description = "My great package", + + dependencies = { + my_local_dep = 'Path "../somewhere", + git_dep = 'Git { url = "https://example.com/repo", ref = 'Tag "v1.0" }, + index_dep = 'Index { package = "github/nickel-lang/example", version = "1.0" }, + }, + } | std.package.Manifest + ``` + "% + = { + name + | String + | doc m%" The name of this package. "%, - version - | String - | Semver - | doc m%" + version + | String + | Semver + | doc m%" The version of this package. Any semantic version is accepted, but the build metadata field has no effect when matching versions. "%, - minimal_nickel_version - | String - | SemverPrefix - | doc m%" + minimal_nickel_version + | String + | SemverPrefix + | doc m%" The minimal nickel version required for this package. "%, - authors - | Array String - | doc m%" + authors + | Array String + | doc m%" The authors of this package. "%, - description - | String - | doc m%" + description + | String + | optional + | doc m%" A description of this package. "%, - keywords - | Array String - | optional - | doc m%" + keywords + | Array String + | optional + | doc m%" A list of keywords to help people find this package. "%, - # TODO: maybe restrict this to be a valid SPDX 2.3 license expression? - # Cargo allows anything here, but applies restrictions when trying to - # publish to crates.io. - license - | String - | optional - | doc m%" + license + | String + | optional + | doc m%" The name of the license that this package is available under. + + This is a completely free-form string, but some tooling may impose + restrictions. For example, if you want to publish your package in + Nickel's global package registry, the license field needs to be a + valid SPDX license expression that allows redistribution. "%, - dependencies - | { - _ : [| - 'Path String, - 'Git { - url - | String - | doc m%" - The url of a git repository. - - This supports local file paths, https urls like `https://example.com/example-repo`, - and ssh urls like `user@example.com:repo`. - "%, - ref - | [| 'Head, 'Branch String, 'Tag String, 'Commit String |] - | optional - | doc m%" - The git ref to fetch from the repository. - - If not provided, defaults to 'Head. - "%, - path - | String - | optional - | doc m%" - The path of the nickel package within the git repository. If omitted, the nickel package - is at the root of the git repository. - "%, - }, - 'Index { - package - | String - | doc m%" - The dependency's identifier within the nickel index, in the format "github//" - "%, - version - | String - | SemverReq - | doc m%" - The required version of the package. - - Nickel supports two kinds of requirements: semver-compatible - requirements and exact version requirements. Semver-compatible - requirements take the form "major.minor.patch", where minor and patch - are optional. Their semantics are: - - - "1.2.3" will match all versions having major version 1, minor version 2, - and patch version at least 3. - - "1.2" will match all versions having major version 1 and minor version - at least 2. - - "1" will match all versions having major version 1. - - a semver-compatible requirement will never match a prerelease version. - - Exact version requirements take the form "=major.minor.patch-pre", where - the prerelease tag is optional, but major, minor, and patch are all required. - "%, - }, - |] - } - | doc m%" + dependencies + | { + _ : [| + 'Path String, + 'Git std.package.GitDependency, + 'Index std.package.IndexDependency, + |] + } + | doc m%" A dictionary of package dependencies, keyed by the name that this package uses to refer to them locally. "% - | default - = {}, - }, + | default + = {}, + }, }, record = {