Skip to content

Commit d488241

Browse files
committed
Improve the manifest contract
Adds more fields and makes the semver contracts more accurate
1 parent 47835e1 commit d488241

File tree

13 files changed

+148
-25
lines changed

13 files changed

+148
-25
lines changed

core/stdlib/std.ncl

+124-23
Original file line numberDiff line numberDiff line change
@@ -2801,59 +2801,132 @@
28012801
},
28022802

28032803
package =
2804-
let
2804+
let rec
28052805
# https://semver.org is kind enough to supply this "official" semver regex.
2806-
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-]+)*))?$"% in let
2807-
# An expression representing a range of semver versions, like `<1.2`.
2808-
# Unlike semver itself, semver range expressions don't seem to have an official standard.
2809-
semver_main_range_re = m%"(~|=|\^|<|>|<=|>=)?(0|[1-9]\d*)(\.(0|[1-9]\d*))?(\.(0|[1-9]\d*))?"% in let
2810-
# For requirements with non-empty prereleases, we support exactly two flavors: 0.1.2-pre3 or =0.1.2-pre3.
2811-
# We could also support inequalities, but we should avoid ^ and ~ because prereleases have no guaranteed
2812-
# compatibility semantics.
2813-
semver_prerelease_range_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-]*))*)"% in let semver_range_re = "(%{semver_main_range_re})|(%{semver_prerelease_range_re})" in
2814-
let semver_req_re = m%"^(%{semver_range_re}(,\s*%{semver_range_re})*)$"% in
2815-
{
2806+
semver_re_unanchored = 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-]+)*))?"%,
2807+
semver_re = "^%{semver_re_unanchored}$",
2808+
# Just the major.minor.patch part, with minor and patch being optional.
2809+
partial_semver_re_unanchored = m%"(0|[1-9]\d*)(\.(0|[1-9]\d*))?(\.(0|[1-9]\d*))?"%,
2810+
partial_semver_re = "^%{partial_semver_re_unanchored}$",
2811+
# An exact version constraint. This one is required to have minor and patch versions, and it's allowed to have a prerelease.
2812+
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-]*))*))?$"%,
2813+
semver_req_re = "(%{partial_semver_re})|(%{semver_equals_req_re})",
2814+
in {
28162815
is_semver_req
28172816
: String -> Bool
28182817
| doc m%"
2819-
Returns true if a string is a valid semantic version requirement.
2818+
Returns true if a string is a valid version requirement in Nickel.
2819+
2820+
See the `SemverReq` contract for more details.
28202821
"%
28212822
= std.string.is_match semver_req_re,
28222823
is_semver
28232824
: String -> Bool
28242825
| doc m%"
2825-
Returns true if a string is a valid semantic version identifier.
2826+
Returns true if a string is a valid semantic version.
2827+
2828+
# Examples
2829+
2830+
```nickel multiline
2831+
std.package.is_semver "1.2.0-pre1"
2832+
# => true
2833+
2834+
std.package.is_semver "1.foo"
2835+
# => false
2836+
```
28262837
"%
28272838
= std.string.is_match semver_re,
2839+
is_semver_prefix
2840+
: String -> Bool
2841+
| doc m%"
2842+
Returns true if a string is a valid semantic version prefix,
2843+
containing a major version and then optional minor and patch versions.
2844+
2845+
# Examples
2846+
2847+
```nickel multiline
2848+
std.package.is_semver_prefix "1.2"
2849+
# => true
2850+
2851+
std.package.is_semver_prefix "1.foo"
2852+
# => false
2853+
```
2854+
"%
2855+
= std.string.is_match partial_semver_re,
28282856
Semver
28292857
| doc m%"
28302858
A contract for semantic version ("semver") identifiers.
28312859

28322860
# Examples
28332861

2834-
```nickel
2862+
```nickel multiline
28352863
"1.2.0-pre1" | std.package.Semver
2836-
=> "1.2.0-pre1"
2864+
# => "1.2.0-pre1"
28372865

28382866
"1.foo" | std.package.Semver
2839-
=> error: contract broken by a value
2867+
# => error: contract broken by a value
28402868
```
28412869
"%
28422870
= std.contract.from_predicate is_semver,
2871+
SemverPrefix
2872+
| doc m%"
2873+
A contract for semantic version ("semver") prefixes,
2874+
containing a major version and then optional minor and patch versions.
2875+
2876+
# Examples
2877+
2878+
```nickel multiline
2879+
"1.2" | std.package.SemverPrefix
2880+
# => "1.2"
2881+
2882+
"1.foo" | std.package.SemverPrefix
2883+
# => error: contract broken by a value
2884+
```
2885+
"%
2886+
= std.contract.from_predicate is_semver_prefix,
28432887
SemverReq
28442888
| doc m%"
28452889
A contract for semantic version ("semver") requirements.
28462890

2891+
Nickel supports two kinds of requirements: semver-compatible
2892+
requirements and exact version requirements. Semver-compatible
2893+
requirements take the form "major.minor.patch", where minor and patch
2894+
are optional. Their semantics are:
2895+
2896+
- "1.2.3" will match all versions having major version 1, minor version 2,
2897+
and patch version at least 3.
2898+
- "1.2" will match all versions having major version 1 and minor version
2899+
at least 2.
2900+
- "1" will match all versions having major version 1.
2901+
- a semver-compatible requirement will never match a prerelease version.
2902+
2903+
Exact version requirements take the form "=major.minor.patch-pre", where
2904+
the prerelease tag is optional, but major, minor, and patch are all required.
2905+
28472906
# Examples
28482907

2849-
```nickel
2850-
"^1.2" | std.package.SemverReq
2851-
=> "^1.2"
2908+
```nickel multiline
2909+
"1.2" | SemverReq
2910+
# => "1.2"
28522911

2853-
">=1.2, <1.4" | std.package.SemverReq
2854-
=> ">=1.2, <1.4"
2912+
"=1.2" | SemverReq
2913+
# => error: contract broken by a value
2914+
2915+
"1.2.0" | SemverReq
2916+
# => "1.2.0"
2917+
2918+
"=1.2.0" | SemverReq
2919+
# => "=1.2.0"
2920+
2921+
"1.2.0-pre1" | SemverReq
2922+
# => error: contract broken by a value
2923+
2924+
"=1.2.0-pre1" | SemverReq
2925+
# => "=1.2.0-pre1"
2926+
```
28552927
"%
28562928
= std.contract.from_predicate is_semver_req,
2929+
# TODO: bikeshedding opportunity: which fields should be optional?
28572930
Manifest = {
28582931
name
28592932
| String
@@ -2870,12 +2943,40 @@
28702943

28712944
nickel_version
28722945
| String
2873-
# TODO: this is too strict, as it requires a full major.minor.patch. We should also allow major.minor
2874-
| Semver
2946+
| SemverPrefix
28752947
| doc m%"
28762948
The minimal nickel version required for this package.
28772949
"%,
28782950

2951+
authors
2952+
| Array String
2953+
| doc m%"
2954+
The authors of this package.
2955+
"%,
2956+
2957+
description
2958+
| String
2959+
| doc m%"
2960+
A description of this package.
2961+
"%,
2962+
2963+
keywords
2964+
| Array String
2965+
| optional
2966+
| doc m%"
2967+
A list of keywords to help people find this package.
2968+
"%,
2969+
2970+
# TODO: maybe restrict this to be a valid SPDX 2.3 license expression?
2971+
# We can also allow arbitrary strings, but only accept index packages
2972+
# with clear licenses
2973+
license
2974+
| String
2975+
| optional
2976+
| doc m%"
2977+
The name of the license that this package is available under.
2978+
"%,
2979+
28792980
dependencies
28802981
| {
28812982
_ : [|

package/src/manifest.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -378,7 +378,7 @@ mod tests {
378378
#[test]
379379
fn manifest() {
380380
let manifest = ManifestFile::from_contents(
381-
r#"{name = "foo", version = "1.0.0", nickel_version = "1.8.0"}"#.as_bytes(),
381+
r#"{name = "foo", version = "1.0.0", nickel_version = "1.8.0", authors = [], description = "hi"}"#.as_bytes(),
382382
)
383383
.unwrap();
384384
assert_eq!(
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
{
22
name = "branch-leaf",
3+
description = "A package with no dependencies, available at a git branch",
34
version = "0.1.0",
5+
authors = ["Joe"],
46
nickel_version = "1.9.0",
57
dependencies = {},
68
} | std.package.Manifest
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
{
22
name = "leaf",
3+
description = "A package with no dependencies",
34
version = "0.1.0",
45
nickel_version = "1.9.0",
6+
authors = ["Joe"],
57
dependencies = {},
68
} | std.package.Manifest
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
{
22
name = "leaf",
3+
description = "A package with no dependencies, available at a git tag",
34
version = "0.1.0",
5+
authors = ["Joe"],
46
nickel_version = "1.9.0",
57
dependencies = {},
68
} | std.package.Manifest
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
{
22
name = "leaf-subdir",
3+
description = "A package in a subdirectory of a git repo",
34
version = "0.1.0",
5+
authors = ["Joe"],
46
nickel_version = "1.9.0",
57
dependencies = {},
68
} | std.package.Manifest
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
{
22
name = "with-subdirs",
3+
description = "A package in a git repo with other packages in subdirectories",
34
version = "0.1.0",
5+
authors = ["Joe"],
46
nickel_version = "1.9.0",
57
dependencies = {},
68
} | std.package.Manifest

package/tests/integration/inputs/git/with-subdirs/subdir-with-path-dep/package.ncl

+2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
{
22
name = "subdir-with-path-dep",
3+
description = "A package in a subdir of a git repo, depending on packages in other directories",
34
version = "0.1.0",
5+
authors = ["Joe"],
46
nickel_version = "1.9.0",
57
dependencies = {
68
leaf = 'Path "../leaf-subdir",

package/tests/integration/inputs/path/git-branch-and-tag-dep/package.ncl

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
{
2-
name = "single-git-dep",
2+
name = "branch-and-tag-git-dep",
3+
description = "A package depending on a git branch and a git tag",
34
version = "0.1.0",
5+
authors = ["Joe"],
46
nickel_version = "1.9.0",
57
dependencies = {
68
branch = 'Git { url = "https://example.com/branch-leaf", ref = 'Branch "cành" },

package/tests/integration/inputs/path/git-path-dep/package.ncl

+2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
{
22
name = "git-path-dep",
3+
description = "A package whose deps live in subdirs of a git repo",
34
version = "0.1.0",
5+
authors = ["Joe"],
46
nickel_version = "1.9.0",
57
dependencies = {
68
git-root = 'Git { url = "https://example.com/with-subdirs" },
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
{
22
name = "leaf",
3+
description = "A package with no dependencies",
34
version = "0.1.0",
45
nickel_version = "1.9.0",
6+
authors = ["Joe"],
57
dependencies = {},
68
} | std.package.Manifest

package/tests/integration/inputs/path/single-git-dep/package.ncl

+2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
{
22
name = "single-git-dep",
3+
description = "A package with a git dependency",
34
version = "0.1.0",
5+
authors = ["Joe"],
46
nickel_version = "1.9.0",
57
dependencies = {
68
leaf = 'Git { url = "https://example.com/leaf" }

package/tests/integration/inputs/path/single-path-dep/package.ncl

+2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
{
22
name = "single-path-dep",
3+
description = "A package with a single path dependency",
34
version = "0.1.0",
5+
authors = ["Joe"],
46
nickel_version = "1.9.0",
57
dependencies = {
68
leaf = 'Path "../leaf"

0 commit comments

Comments
 (0)