Skip to content

Commit 4af5e47

Browse files
committed
Use deserialize to simplify manifest construction
1 parent e3aea30 commit 4af5e47

File tree

3 files changed

+93
-141
lines changed

3 files changed

+93
-141
lines changed

core/stdlib/std.ncl

+46-6
Original file line numberDiff line numberDiff line change
@@ -2582,18 +2582,42 @@
25822582
= fun x n => %pow% x n,
25832583
},
25842584

2585-
package = {
2585+
package =
2586+
let
2587+
# https://semver.org is kind enough to supply this "official" semver regex.
2588+
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-]+)*))?$"%
2589+
in
2590+
let
2591+
# An expression representing a range of semver versions, like `<1.2`.
2592+
# Unlike semver itself, semver range expressions don't seem to have an official standard.
2593+
semver_main_range_re = m%"(~|=|\^|<|>|<=|>=)?(0|[1-9]\d*)(\.(0|[1-9]\d*))?(\.(0|[1-9]\d*))?"%
2594+
in
2595+
let
2596+
# For requirements with non-empty prereleases, we support exactly two flavors: 0.1.2-pre3 or =0.1.2-pre3.
2597+
# We could also support inequalities, but we should avoid ^ and ~ because prereleases have no guaranteed
2598+
# compatibility semantics.
2599+
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-]*))*)"%
2600+
in
2601+
let semver_range_re = "(%{semver_main_range_re})|(%{semver_prerelease_range_re})" in
2602+
let semver_req_re = m%"^(%{semver_range_re}(,\s*%{semver_range_re})*)$"% in
2603+
{
2604+
is_semver_req
2605+
: String -> Bool
2606+
| doc m%"
2607+
Returns true if a string is a valid semantic version requirement.
2608+
"%
2609+
= std.string.is_match semver_req_re
2610+
,
25862611
is_semver
25872612
: String -> Bool
25882613
| doc m%"
25892614
Returns true if a string is a valid semantic version identifier.
25902615
"%
2591-
# This lovely expression is the officially recommended semver regex from https://semver.org
2592-
= std.string.is_match 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-]+)*))?$"%
2616+
= std.string.is_match semver_re
25932617
,
25942618
Semver
25952619
| doc m%"
2596-
A contract that checks whether a string represents a valid semantic version ("semver") identifier.
2620+
A contract for semantic version ("semver") identifiers.
25972621

25982622
# Examples
25992623

@@ -2606,6 +2630,20 @@
26062630
```
26072631
"%
26082632
= std.contract.from_predicate is_semver,
2633+
SemverReq
2634+
| doc m%"
2635+
A contract for semantic version ("semver") requirements.
2636+
2637+
# Examples
2638+
2639+
```nickel
2640+
"^1.2" | std.package.SemverReq
2641+
=> "^1.2"
2642+
2643+
">=1.2, <1.4" | std.package.SemverReq
2644+
=> ">=1.2, <1.4"
2645+
"%
2646+
= std.contract.from_predicate is_semver_req,
26092647
Manifest = {
26102648
name
26112649
| String
@@ -2614,12 +2652,14 @@
26142652
"%,
26152653

26162654
version
2655+
| String
26172656
| Semver
26182657
| doc m%"
26192658
The version of this package.
26202659
"%,
26212660

2622-
nickel-version
2661+
nickel_version
2662+
| String
26232663
| Semver
26242664
| doc m%"
26252665
The minimal nickel version required for this package.
@@ -2630,7 +2670,7 @@
26302670
[|
26312671
'Path String,
26322672
'Git { url | String, },
2633-
'Index { name | String, version | String | Semver },
2673+
'Index { name | String, version | String | SemverReq },
26342674
|]
26352675
}
26362676
| doc m%"

package/src/lib.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,11 @@ use util::cache_dir;
1616
/// A source includes the place to fetch a package from (e.g. git or a registry),
1717
/// along with possibly some narrowing-down of the allowed versions (e.g. a range
1818
/// of versions, or a git commit id).
19-
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
19+
#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize)]
2020
pub enum Dependency {
2121
// TODO: allow targeting branches or revisions, and allow supplying a relative path
2222
Git {
23+
#[serde(with = "serde_url")]
2324
url: gix::Url,
2425
//tree: Option<git2::Oid>,
2526
},

package/src/manifest.rs

+45-134
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ use std::{
33
path::{Path, PathBuf},
44
};
55

6-
use gix::Url;
76
use nickel_lang_core::{
87
cache::normalize_rel_path,
98
eval::cache::CacheImpl,
@@ -24,7 +23,15 @@ use crate::{
2423
Dependency, Precise,
2524
};
2625

27-
#[derive(Clone, Debug)]
26+
#[derive(Clone, Debug, Deserialize)]
27+
struct ManifestFileFormat {
28+
pub name: Ident,
29+
pub version: semver::Version,
30+
pub nickel_version: semver::Version,
31+
pub dependencies: HashMap<Ident, Dependency>,
32+
}
33+
34+
#[derive(Clone, Debug, PartialEq)]
2835
pub struct ManifestFile {
2936
// The directory containing the manifest file. Path deps are resolved relative to this.
3037
// If `None`, path deps aren't allowed.
@@ -148,142 +155,23 @@ impl ManifestFile {
148155
fn from_term(rt: &RichTerm) -> Result<Self, Error> {
149156
// This is only ever called with terms that have passed the `std.package.Manifest`
150157
// contract, so we can assume that they have the right fields.
151-
fn err(s: &str) -> Error {
152-
Error::InternalManifestError { msg: s.to_owned() }
153-
}
154-
155-
let Term::Record(data) = rt.as_ref() else {
156-
return Err(err("manifest not a record"));
157-
};
158-
159-
// FIXME: yuck
160-
let name = data
161-
.fields
162-
.get(&Ident::new("name"))
163-
.ok_or_else(|| err("no name"))?
164-
.value
165-
.as_ref()
166-
.ok_or_else(|| err("name has no value"))?;
167-
let Term::Str(name) = name.as_ref() else {
168-
return Err(err("name not a string"));
169-
};
170-
171-
let version = data
172-
.fields
173-
.get(&Ident::new("version"))
174-
.ok_or_else(|| err("no version"))?
175-
.value
176-
.as_ref()
177-
.ok_or_else(|| err("version has no value"))?;
178-
let Term::Str(version) = version.as_ref() else {
179-
return Err(err("version not a string"));
180-
};
181-
182-
let nickel_version = data
183-
.fields
184-
.get(&Ident::new("nickel-version"))
185-
.ok_or_else(|| err("no nickel-version"))?
186-
.value
187-
.as_ref()
188-
.ok_or_else(|| err("nickel-version has no value"))?;
189-
let Term::Str(nickel_version) = nickel_version.as_ref() else {
190-
return Err(err("nickel-version not a string"));
191-
};
192-
193-
let deps = data
194-
.fields
195-
.get(&Ident::new("dependencies"))
196-
.ok_or_else(|| err("no dependencies"))?
197-
.value
198-
.as_ref()
199-
.ok_or_else(|| err("dependencies has no value"))?;
200-
let Term::Record(deps) = deps.as_ref() else {
201-
return Err(err("dependencies not a record"));
202-
};
203-
204-
let mut ret = Self {
205-
dependencies: HashMap::new(),
158+
let ManifestFileFormat {
159+
name,
160+
version,
161+
nickel_version,
162+
dependencies,
163+
} = ManifestFileFormat::deserialize(rt.clone())
164+
.map_err(|e| Error::InternalManifestError { msg: e.to_string() })?;
165+
Ok(Self {
206166
parent_dir: None,
207-
version: version.parse().map_err(|_| err("invalid version"))?,
208-
nickel_version: nickel_version
209-
.parse()
210-
.map_err(|_| err("invalid nickel version"))?,
211-
name: Ident::new(name),
212-
};
213-
214-
for (name, dep) in &deps.fields {
215-
let Term::EnumVariant { tag, arg, .. } = dep
216-
.value
217-
.as_ref()
218-
.ok_or_else(|| err("dependency has no value"))?
219-
.as_ref()
220-
else {
221-
return Err(err("dependency not an enum"));
222-
};
223-
224-
match tag.ident().label() {
225-
"Git" => {
226-
let Term::Record(data) = arg.as_ref() else {
227-
return Err(err("payload wasn't a record"));
228-
};
229-
230-
let url = data
231-
.fields
232-
.get(&Ident::new("url"))
233-
.ok_or_else(|| err("no url"))?
234-
.value
235-
.as_ref()
236-
.ok_or_else(|| err("url has no value"))?;
237-
let Term::Str(url) = url.as_ref() else {
238-
return Err(err("url wasn't a string"));
239-
};
240-
241-
ret.dependencies.insert(
242-
name.ident(),
243-
Dependency::Git {
244-
url: Url::try_from(url.to_string()).map_err(|e| Error::InvalidUrl {
245-
url: url.to_string(),
246-
msg: e.to_string(),
247-
})?,
248-
},
249-
);
250-
}
251-
"Path" => {
252-
let Term::Str(path) = arg.as_ref() else {
253-
return Err(err("payload wasn't a string"));
254-
};
255-
256-
ret.dependencies.insert(
257-
name.ident(),
258-
Dependency::Path {
259-
path: PathBuf::from(path.to_string()),
260-
},
261-
);
262-
}
263-
"Index" => {
264-
let payload: IndexPayload = serde_json::from_value(
265-
serde_json::to_value(arg.as_ref()).map_err(|_| err("bad payload"))?,
266-
)
267-
.map_err(|_| err("bad payload"))?;
268-
269-
let id: crate::index::Id = payload.name.parse().unwrap();
270-
let version: semver::VersionReq = payload.version.parse().unwrap();
271-
ret.dependencies
272-
.insert(name.ident(), Dependency::Index { id, version });
273-
}
274-
_ => return Err(err("bad tag")),
275-
}
276-
}
277-
Ok(ret)
167+
name,
168+
version,
169+
nickel_version,
170+
dependencies,
171+
})
278172
}
279173
}
280174

281-
#[derive(Deserialize)]
282-
struct IndexPayload {
283-
name: String,
284-
version: String,
285-
}
286-
287175
#[derive(Clone, Debug)]
288176
pub struct RealizedDependency {
289177
/// Either `Git` or `Path`.
@@ -408,3 +296,26 @@ impl Realization {
408296
Ok(id)
409297
}
410298
}
299+
300+
#[cfg(test)]
301+
mod tests {
302+
use super::*;
303+
304+
#[test]
305+
fn manifest() {
306+
let manifest = ManifestFile::from_contents(
307+
r#"{name = "foo", version = "1.0.0", nickel_version = "1.8.0"}"#.as_bytes(),
308+
)
309+
.unwrap();
310+
assert_eq!(
311+
manifest,
312+
ManifestFile {
313+
parent_dir: None,
314+
name: "foo".into(),
315+
version: semver::Version::new(1, 0, 0),
316+
nickel_version: semver::Version::new(1, 8, 0),
317+
dependencies: HashMap::default()
318+
}
319+
)
320+
}
321+
}

0 commit comments

Comments
 (0)