use monch::*;
use thiserror::Error;
use crate::CowVec;
use crate::PackageTag;
use crate::RangeSetOrTag;
use crate::VersionPreOrBuild;
use crate::VersionReq;
use crate::range::Partial;
use crate::range::VersionRange;
use crate::range::VersionRangeSet;
use crate::range::XRange;
use crate::is_valid_tag;
#[derive(Error, Debug, Clone, deno_error::JsError, PartialEq, Eq)]
#[class(type)]
#[error("Invalid specifier version requirement")]
pub struct VersionReqSpecifierParseError {
#[source]
pub source: ParseErrorFailureError,
}
pub fn parse_version_req_from_specifier(
text: &str,
) -> Result<VersionReq, VersionReqSpecifierParseError> {
with_failure_handling(|input| {
map_res(version_range, |result| {
let (new_input, range_result) = match result {
Ok((input, range)) => (input, Ok(range)),
Err(err) => ("", Err(err)),
};
Ok((
new_input,
VersionReq::from_raw_text_and_inner(
crate::SmallStackString::from_str(input),
match range_result {
Ok(range) => {
RangeSetOrTag::RangeSet(VersionRangeSet(CowVec::from([range])))
}
Err(err) => {
if is_valid_tag(input) {
RangeSetOrTag::Tag(PackageTag::from_str(input))
} else if input.trim().is_empty() {
return ParseError::fail(input, "Empty version constraint.");
} else {
return Err(err);
}
}
},
),
))
})(input)
})(text)
.map_err(|err| VersionReqSpecifierParseError { source: err })
}
fn version_range(input: &str) -> ParseResult<'_, VersionRange> {
or3(
map(preceded(ch('~'), partial), |partial| {
partial.as_tilde_version_range()
}),
map(preceded(ch('^'), partial), |partial| {
partial.as_caret_version_range()
}),
map(partial, |partial| partial.as_equal_range()),
)(input)
}
fn partial(input: &str) -> ParseResult<'_, Partial> {
let (input, major) = xr()(input)?;
let (input, maybe_minor) = maybe(preceded(ch('.'), xr()))(input)?;
let (input, maybe_patch) = if maybe_minor.is_some() {
maybe(preceded(ch('.'), xr()))(input)?
} else {
(input, None)
};
let (input, qual) = if maybe_patch.is_some() {
maybe(qualifier)(input)?
} else {
(input, None)
};
let qual = qual.unwrap_or_default();
Ok((
input,
Partial {
major,
minor: maybe_minor.unwrap_or(XRange::Wildcard),
patch: maybe_patch.unwrap_or(XRange::Wildcard),
pre: qual.pre,
build: qual.build,
},
))
}
fn xr<'a>() -> impl Fn(&'a str) -> ParseResult<'a, XRange> {
or(
map(or3(tag("x"), tag("X"), tag("*")), |_| XRange::Wildcard),
map(nr, XRange::Val),
)
}
fn nr(input: &str) -> ParseResult<'_, u64> {
or(map(tag("0"), |_| 0), move |input| {
let (input, result) = if_not_empty(substring(pair(
if_true(next_char, |c| c.is_ascii_digit() && *c != '0'),
skip_while_byte(|b| b.is_ascii_digit()),
)))(input)?;
let val = match result.parse::<u64>() {
Ok(val) => val,
Err(err) => {
return ParseError::fail(
input,
format!("Error parsing '{result}' to u64.\n\n{err:#}"),
);
}
};
Ok((input, val))
})(input)
}
#[derive(Debug, Clone, Default)]
struct Qualifier {
pre: CowVec<VersionPreOrBuild>,
build: CowVec<VersionPreOrBuild>,
}
fn qualifier(input: &str) -> ParseResult<'_, Qualifier> {
let (input, pre_parts) = maybe(pre)(input)?;
let (input, build_parts) = maybe(build)(input)?;
Ok((
input,
Qualifier {
pre: pre_parts.unwrap_or_default(),
build: build_parts.unwrap_or_default(),
},
))
}
fn pre(input: &str) -> ParseResult<'_, CowVec<VersionPreOrBuild>> {
preceded(ch('-'), parts)(input)
}
fn build(input: &str) -> ParseResult<'_, CowVec<VersionPreOrBuild>> {
preceded(ch('+'), parts)(input)
}
fn parts(input: &str) -> ParseResult<'_, CowVec<VersionPreOrBuild>> {
let (rest, parts) = separated_fold(
part,
ch('.'),
CowVec::<VersionPreOrBuild>::new(),
|mut acc, s| {
acc.push(VersionPreOrBuild::from_str(s));
acc
},
)(input)?;
if parts.is_empty() {
return ParseError::backtrace();
}
Ok((rest, parts))
}
fn part(input: &str) -> ParseResult<'_, &str> {
let (rest, result) =
take_while_byte(|b| b.is_ascii_alphanumeric() || b == b'-')(input)?;
if result.is_empty() {
return ParseError::backtrace();
}
Ok((rest, result))
}
#[cfg(test)]
mod tests {
use super::super::Version;
use super::*;
struct VersionReqTester(VersionReq);
impl VersionReqTester {
fn new(text: &str) -> Self {
Self(parse_version_req_from_specifier(text).unwrap())
}
fn matches(&self, version: &str) -> bool {
self.0.matches(&Version::parse_from_npm(version).unwrap())
}
}
#[test]
fn version_req_exact() {
let tester = VersionReqTester::new("1.0.1");
assert!(!tester.matches("1.0.0"));
assert!(tester.matches("1.0.1"));
assert!(!tester.matches("1.0.2"));
assert!(!tester.matches("1.1.1"));
let tester = VersionReqTester::new("1.0.0-alpha.13");
assert!(tester.matches("1.0.0-alpha.13"));
}
#[test]
fn version_req_minor() {
let tester = VersionReqTester::new("1.1");
assert!(!tester.matches("1.0.0"));
assert!(tester.matches("1.1.0"));
assert!(tester.matches("1.1.1"));
assert!(!tester.matches("1.2.0"));
assert!(!tester.matches("1.2.1"));
}
#[test]
fn version_req_caret() {
let tester = VersionReqTester::new("^1.1.1");
assert!(!tester.matches("1.1.0"));
assert!(tester.matches("1.1.1"));
assert!(tester.matches("1.1.2"));
assert!(tester.matches("1.2.0"));
assert!(!tester.matches("2.0.0"));
let tester = VersionReqTester::new("^0.1.1");
assert!(!tester.matches("0.0.0"));
assert!(!tester.matches("0.1.0"));
assert!(tester.matches("0.1.1"));
assert!(tester.matches("0.1.2"));
assert!(!tester.matches("0.2.0"));
assert!(!tester.matches("1.0.0"));
let tester = VersionReqTester::new("^0.0.1");
assert!(!tester.matches("0.0.0"));
assert!(tester.matches("0.0.1"));
assert!(!tester.matches("0.0.2"));
assert!(!tester.matches("0.1.0"));
assert!(!tester.matches("1.0.0"));
}
#[test]
fn version_req_tilde() {
let tester = VersionReqTester::new("~1.1.1");
assert!(!tester.matches("1.1.0"));
assert!(tester.matches("1.1.1"));
assert!(tester.matches("1.1.2"));
assert!(!tester.matches("1.2.0"));
assert!(!tester.matches("2.0.0"));
let tester = VersionReqTester::new("~0.1.1");
assert!(!tester.matches("0.0.0"));
assert!(!tester.matches("0.1.0"));
assert!(tester.matches("0.1.1"));
assert!(tester.matches("0.1.2"));
assert!(!tester.matches("0.2.0"));
assert!(!tester.matches("1.0.0"));
let tester = VersionReqTester::new("~0.0.1");
assert!(!tester.matches("0.0.0"));
assert!(tester.matches("0.0.1"));
assert!(tester.matches("0.0.2")); assert!(!tester.matches("0.1.0"));
assert!(!tester.matches("1.0.0"));
}
#[test]
fn version_req_pre_release() {
let tester = VersionReqTester::new("^1.0.1-pre-release");
assert!(!tester.matches("1.0.0"));
assert!(tester.matches("1.0.1"));
assert!(tester.matches("1.0.1-pre-release"));
let tester = VersionReqTester::new("^0.0.0-pre-release");
assert!(tester.matches("0.0.0"));
assert!(!tester.matches("0.0.1"));
assert!(tester.matches("0.0.0-pre-release"));
}
#[test]
fn parses_tag() {
let latest_tag = VersionReq::parse_from_specifier("latest").unwrap();
assert_eq!(latest_tag.tag().unwrap(), "latest");
}
}