Skip to content

Commit af3e183

Browse files
authored
fix: deserialize package reqs as normalized (#49)
1 parent fe7c48e commit af3e183

6 files changed

Lines changed: 601 additions & 232 deletions

File tree

src/common.rs

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
2+
3+
use monch::*;
4+
5+
use crate::CowVec;
6+
use crate::Partial;
7+
use crate::VersionBoundKind;
8+
use crate::VersionPreOrBuild;
9+
use crate::VersionRange;
10+
use crate::XRange;
11+
12+
// logical-or ::= ( ' ' ) * '||' ( ' ' ) *
13+
pub fn logical_or(input: &str) -> ParseResult<&str> {
14+
delimited(skip_whitespace, tag("||"), skip_whitespace)(input)
15+
}
16+
17+
// logical-and ::= ( ' ' ) * '&&' ( ' ' ) *
18+
pub fn logical_and(input: &str) -> ParseResult<&str> {
19+
delimited(skip_whitespace, tag("&&"), skip_whitespace)(input)
20+
}
21+
22+
// partial ::= xr ( '.' xr ( '.' xr qualifier ? )? )?
23+
pub fn partial<'a>(
24+
xr: impl Fn(&'a str) -> ParseResult<'a, XRange>,
25+
) -> impl Fn(&'a str) -> ParseResult<'a, Partial> {
26+
move |input| {
27+
let (input, major) = xr(input)?;
28+
let (input, maybe_minor) = maybe(preceded(ch('.'), &xr))(input)?;
29+
let (input, maybe_patch) = if maybe_minor.is_some() {
30+
maybe(preceded(ch('.'), &xr))(input)?
31+
} else {
32+
(input, None)
33+
};
34+
let (input, qual) = if maybe_patch.is_some() {
35+
maybe(qualifier)(input)?
36+
} else {
37+
(input, None)
38+
};
39+
let qual = qual.unwrap_or_default();
40+
Ok((
41+
input,
42+
Partial {
43+
major,
44+
minor: maybe_minor.unwrap_or(XRange::Wildcard),
45+
patch: maybe_patch.unwrap_or(XRange::Wildcard),
46+
pre: qual.pre,
47+
build: qual.build,
48+
},
49+
))
50+
}
51+
}
52+
53+
#[derive(Debug, Clone, Default)]
54+
pub struct Qualifier {
55+
pub pre: CowVec<VersionPreOrBuild>,
56+
pub build: CowVec<VersionPreOrBuild>,
57+
}
58+
59+
// qualifier ::= ( '-' pre )? ( '+' build )?
60+
pub fn qualifier(input: &str) -> ParseResult<Qualifier> {
61+
let (input, pre_parts) = maybe(pre)(input)?;
62+
let (input, build_parts) = maybe(build)(input)?;
63+
Ok((
64+
input,
65+
Qualifier {
66+
pre: pre_parts.unwrap_or_default(),
67+
build: build_parts.unwrap_or_default(),
68+
},
69+
))
70+
}
71+
72+
// pre ::= parts
73+
fn pre(input: &str) -> ParseResult<CowVec<VersionPreOrBuild>> {
74+
preceded(maybe(ch('-')), parts)(input)
75+
}
76+
77+
// build ::= parts
78+
fn build(input: &str) -> ParseResult<CowVec<VersionPreOrBuild>> {
79+
preceded(ch('+'), parts)(input)
80+
}
81+
82+
// parts ::= part ( '.' part ) *
83+
fn parts(input: &str) -> ParseResult<CowVec<VersionPreOrBuild>> {
84+
if_true(
85+
map(separated_list(part, ch('.')), |text| {
86+
text
87+
.into_iter()
88+
.map(VersionPreOrBuild::from_str)
89+
.collect::<CowVec<_>>()
90+
}),
91+
|items| !items.is_empty(),
92+
)(input)
93+
}
94+
95+
// part ::= nr | [-0-9A-Za-z]+
96+
fn part(input: &str) -> ParseResult<&str> {
97+
// nr is in the other set, so don't bother checking for it
98+
if_true(
99+
take_while(|c| c.is_ascii_alphanumeric() || c == '-'),
100+
|result| !result.is_empty(),
101+
)(input)
102+
}
103+
104+
#[derive(Debug, Clone, Copy)]
105+
pub enum PrimitiveKind {
106+
GreaterThan,
107+
LessThan,
108+
GreaterThanOrEqual,
109+
LessThanOrEqual,
110+
Equal,
111+
}
112+
113+
pub fn primitive_kind(input: &str) -> ParseResult<PrimitiveKind> {
114+
or5(
115+
map(tag(">="), |_| PrimitiveKind::GreaterThanOrEqual),
116+
map(tag("<="), |_| PrimitiveKind::LessThanOrEqual),
117+
map(ch('<'), |_| PrimitiveKind::LessThan),
118+
map(ch('>'), |_| PrimitiveKind::GreaterThan),
119+
map(ch('='), |_| PrimitiveKind::Equal),
120+
)(input)
121+
}
122+
123+
#[derive(Debug, Clone)]
124+
pub struct Primitive {
125+
pub kind: PrimitiveKind,
126+
pub partial: Partial,
127+
}
128+
129+
impl Primitive {
130+
pub fn into_version_range(self) -> VersionRange {
131+
let partial = self.partial;
132+
match self.kind {
133+
PrimitiveKind::Equal => partial.as_equal_range(),
134+
PrimitiveKind::GreaterThan => {
135+
partial.as_greater_than(VersionBoundKind::Exclusive)
136+
}
137+
PrimitiveKind::GreaterThanOrEqual => {
138+
partial.as_greater_than(VersionBoundKind::Inclusive)
139+
}
140+
PrimitiveKind::LessThan => {
141+
partial.as_less_than(VersionBoundKind::Exclusive)
142+
}
143+
PrimitiveKind::LessThanOrEqual => {
144+
partial.as_less_than(VersionBoundKind::Inclusive)
145+
}
146+
}
147+
}
148+
}
149+
150+
pub fn primitive<'a>(
151+
partial: impl Fn(&'a str) -> ParseResult<'a, Partial>,
152+
) -> impl Fn(&'a str) -> ParseResult<'a, Primitive> {
153+
move |input| {
154+
let (input, kind) = primitive_kind(input)?;
155+
let (input, _) = skip_whitespace(input)?;
156+
let (input, partial) = partial(input)?;
157+
Ok((input, Primitive { kind, partial }))
158+
}
159+
}

src/jsr.rs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ impl<'de> Deserialize<'de> for JsrDepPackageReq {
213213
D: serde::Deserializer<'de>,
214214
{
215215
let text: Cow<'de, str> = Deserialize::deserialize(deserializer)?;
216-
match Self::from_str_loose(&text) {
216+
match Self::from_str_normalized(&text) {
217217
Ok(req) => Ok(req),
218218
Err(err) => Err(serde::de::Error::custom(err)),
219219
}
@@ -240,13 +240,18 @@ impl JsrDepPackageReq {
240240
Self::from_str_inner(text, PackageReq::from_str)
241241
}
242242

243-
#[allow(clippy::should_implement_trait)]
244243
pub fn from_str_loose(
245244
text: &str,
246245
) -> Result<Self, JsrDepPackageReqParseError> {
247246
Self::from_str_inner(text, PackageReq::from_str_loose)
248247
}
249248

249+
pub fn from_str_normalized(
250+
text: &str,
251+
) -> Result<Self, JsrDepPackageReqParseError> {
252+
Self::from_str_inner(text, PackageReq::from_str_normalized)
253+
}
254+
250255
fn from_str_inner(
251256
text: &str,
252257
parse_req: impl FnOnce(&str) -> Result<PackageReq, PackageReqParseError>,
@@ -443,4 +448,16 @@ mod test {
443448
run_test("jsr:a@^1.0", "jsr:a@1");
444449
run_test("jsr:a@1.2.3 || 1.4.5", "jsr:a@1.2.3 || 1.4.5"); // note: this is the serialized form--it's not a url
445450
}
451+
452+
#[test]
453+
fn serialize_deserialize_tag_package_req_with_v() {
454+
// note: this specifier is a tag and not a version
455+
let package_req = JsrDepPackageReq::from_str("npm:test@v1.0").unwrap();
456+
assert!(package_req.req.version_req.tag().is_some());
457+
let json = serde_json::to_string(&package_req).unwrap();
458+
assert_eq!(json, "\"npm:test@v1.0\"");
459+
let result = serde_json::from_str::<JsrDepPackageReq>(&json).unwrap();
460+
assert!(result.req.version_req.tag().is_some());
461+
assert_eq!(result, package_req);
462+
}
446463
}

src/lib.rs

Lines changed: 26 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,12 @@ use serde::Deserialize;
1717
use serde::Serialize;
1818
use thiserror::Error;
1919

20+
mod common;
2021
pub mod jsr;
2122
pub mod npm;
2223
pub mod package;
2324
mod range;
25+
mod range_set_or_tag;
2426
mod specifier;
2527
mod string;
2628

@@ -38,6 +40,8 @@ pub use self::range::VersionBoundKind;
3840
pub use self::range::VersionRange;
3941
pub use self::range::VersionRangeSet;
4042
pub use self::range::XRange;
43+
pub use self::range_set_or_tag::PackageTag;
44+
pub use self::range_set_or_tag::RangeSetOrTag;
4145

4246
/// Specifier that points to the wildcard version.
4347
pub static WILDCARD_VERSION_REQ: Lazy<VersionReq> =
@@ -225,39 +229,12 @@ pub(crate) fn is_valid_tag(value: &str) -> bool {
225229
npm::is_valid_npm_tag(value)
226230
}
227231

228-
pub type PackageTag = SmallStackString;
229-
230-
#[derive(
231-
Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, CapacityDisplay,
232-
)]
233-
pub enum RangeSetOrTag {
234-
RangeSet(VersionRangeSet),
235-
Tag(PackageTag),
236-
}
237-
238-
impl<'a> StringAppendable<'a> for &'a RangeSetOrTag {
239-
fn append_to_builder<TString: StringType>(
240-
self,
241-
builder: &mut StringBuilder<'a, TString>,
242-
) {
243-
match self {
244-
RangeSetOrTag::RangeSet(range_set) => builder.append(range_set),
245-
RangeSetOrTag::Tag(tag) => builder.append(tag),
246-
}
247-
}
248-
}
249-
250-
impl RangeSetOrTag {
251-
pub fn intersects(&self, other: &RangeSetOrTag) -> bool {
252-
match (self, other) {
253-
(RangeSetOrTag::RangeSet(a), RangeSetOrTag::RangeSet(b)) => {
254-
a.intersects_set(b)
255-
}
256-
(RangeSetOrTag::RangeSet(_), RangeSetOrTag::Tag(_))
257-
| (RangeSetOrTag::Tag(_), RangeSetOrTag::RangeSet(_)) => false,
258-
(RangeSetOrTag::Tag(a), RangeSetOrTag::Tag(b)) => a == b,
259-
}
260-
}
232+
#[derive(Error, Debug, Clone, deno_error::JsError, PartialEq, Eq)]
233+
#[class(type)]
234+
#[error("Invalid normalized version requirement")]
235+
pub struct VersionReqNormalizedParseError {
236+
#[source]
237+
source: monch::ParseErrorFailureError,
261238
}
262239

263240
/// A version constraint.
@@ -290,6 +267,22 @@ impl VersionReq {
290267
Self { raw_text, inner }
291268
}
292269

270+
pub fn parse_from_normalized(
271+
text: &str,
272+
) -> Result<Self, VersionReqNormalizedParseError> {
273+
use monch::*;
274+
275+
with_failure_handling(|input| {
276+
map(RangeSetOrTag::parse, |inner| {
277+
VersionReq::from_raw_text_and_inner(
278+
crate::SmallStackString::from_str(input),
279+
inner,
280+
)
281+
})(input)
282+
})(text)
283+
.map_err(|err| VersionReqNormalizedParseError { source: err })
284+
}
285+
293286
pub fn parse_from_specifier(
294287
specifier: &str,
295288
) -> Result<Self, VersionReqSpecifierParseError> {

0 commit comments

Comments
 (0)