use crate::prelude::*;
use std::ops::Range;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Specifier {
pub op: CompareOp,
pub value: String,
}
impl Specifier {
pub fn satisfied_by(&self, version: &Version) -> Result<bool> {
Ok(self.to_ranges()?.into_iter().any(|r| r.contains(version)))
}
pub fn to_ranges(&self) -> Result<Vec<Range<Version>>> {
self.op.to_ranges(&self.value)
}
}
impl Display for Specifier {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{} {}", self.op, self.value)
}
}
#[derive(
Debug, Clone, PartialEq, Eq, SerializeDisplay, DeserializeFromStr, Default,
)]
pub struct Specifiers(pub Vec<Specifier>);
impl Specifiers {
pub fn satisfied_by(&self, version: &Version) -> Result<bool> {
for specifier in &self.0 {
if !specifier.satisfied_by(&version)? {
return Ok(false);
}
}
Ok(true)
}
}
impl Display for Specifiers {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut first = true;
for spec in &self.0 {
if !first {
write!(f, ", ")?
}
first = false;
write!(f, "{}", spec)?
}
Ok(())
}
}
impl TryFrom<&str> for Specifiers {
type Error = eyre::Report;
fn try_from(input: &str) -> Result<Self, Self::Error> {
let specifiers_or_err = super::reqparse::versionspec(input);
specifiers_or_err.wrap_err_with(|| {
format!("failed to parse versions specifiers from {:?}", input)
})
}
}
try_from_str_boilerplate!(Specifiers);
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub enum CompareOp {
LessThanEqual,
StrictlyLessThan,
NotEqual,
Equal,
GreaterThanEqual,
StrictlyGreaterThan,
Compatible,
}
impl Display for CompareOp {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use CompareOp::*;
write!(
f,
"{}",
match self {
LessThanEqual => "<=",
StrictlyLessThan => "<",
NotEqual => "!=",
Equal => "==",
GreaterThanEqual => ">=",
StrictlyGreaterThan => ">",
Compatible => "~=",
}
)
}
}
impl TryFrom<&str> for CompareOp {
type Error = eyre::Report;
fn try_from(value: &str) -> Result<Self, Self::Error> {
use CompareOp::*;
Ok(match &value[..] {
"==" => Equal,
"!=" => NotEqual,
"<=" => LessThanEqual,
"<" => StrictlyLessThan,
">=" => GreaterThanEqual,
">" => StrictlyGreaterThan,
"~=" => Compatible,
"===" => bail!("'===' is not implemented"),
_ => bail!("unrecognized operator: {:?}", value),
})
}
}
try_from_str_boilerplate!(CompareOp);
fn parse_version_wildcard(input: &str) -> Result<(Version, bool)> {
let (vstr, wildcard) = if let Some(vstr) = input.strip_suffix(".*") {
(vstr, true)
} else {
(input, false)
};
let version: Version = vstr.try_into()?;
Ok((version, wildcard))
}
impl CompareOp {
pub fn to_ranges(&self, rhs: &str) -> Result<Vec<Range<Version>>> {
use CompareOp::*;
let (version, wildcard) = parse_version_wildcard(rhs)?;
Ok(if wildcard {
if version.0.dev.is_some() || !version.0.local.is_empty() {
bail!("version wildcards can't have dev or local suffixes");
}
let mut low = version.clone();
low.0.dev = Some(0);
let mut high = version.clone();
if let Some(post) = high.0.post {
high.0.post = Some(post + 1)
} else if let Some(pre) = high.0.pre {
use pep440::PreRelease::*;
high.0.pre = Some(match pre {
RC(n) => RC(n + 1),
A(n) => A(n + 1),
B(n) => B(n + 1),
})
} else {
*high.0.release.last_mut().unwrap() += 1;
}
high.0.dev = Some(0);
match self {
Equal => vec![low..high],
NotEqual => {
vec![VERSION_ZERO.clone()..low, high..VERSION_INFINITY.clone()]
}
_ => bail!("Can't use wildcard with {:?}", self),
}
} else {
if self != &Equal && self != &NotEqual {
if !version.0.local.is_empty() {
bail!("Operator {:?} cannot be used on a version with a +local suffix", self);
}
}
match self {
LessThanEqual => vec![VERSION_ZERO.clone()..version.next()],
GreaterThanEqual => vec![version.clone()..VERSION_INFINITY.clone()],
Equal => vec![version.clone()..version.next()],
NotEqual => vec![
VERSION_ZERO.clone()..version.clone(),
version.next()..VERSION_INFINITY.clone(),
],
StrictlyGreaterThan => {
let mut low = version.clone();
if let Some(dev) = &version.0.dev {
low.0.dev = Some(dev + 1);
} else if let Some(post) = &version.0.post {
low.0.post = Some(post + 1);
} else {
low.0.post = Some(u32::MAX);
}
vec![low..VERSION_INFINITY.clone()]
}
StrictlyLessThan => {
if (&version.0.pre, &version.0.dev) == (&None, &None) {
let mut new_max = version.clone();
new_max.0.dev = Some(0);
new_max.0.post = None;
new_max.0.local = vec![];
vec![VERSION_ZERO.clone()..new_max]
} else {
vec![VERSION_ZERO.clone()..version]
}
}
Compatible => {
if version.0.release.len() < 2 {
bail!("~= operator requires a version with two segments (X.Y)");
}
let mut new_max = Version(pep440::Version {
epoch: version.0.epoch,
release: version.0.release.clone(),
pre: None,
post: None,
dev: Some(0),
local: vec![],
});
new_max.0.release.pop().unwrap();
*new_max.0.release.last_mut().unwrap() += 1;
vec![version..new_max]
}
}
})
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::test_util::from_commented_json;
#[test]
fn test_invalid_specifiers_table() {
let examples: Vec<String> =
from_commented_json(include_str!("test-data/invalid-specifiers.txt"));
fn chew_on(example: &str) -> Result<Specifiers> {
let specs: Specifiers = example.try_into()?;
for spec in &specs.0 {
spec.to_ranges()?;
}
Ok(specs)
}
for example in examples {
println!("Parsing {:?}", example);
let got = chew_on(&example);
println!("Got {:?}", got);
assert!(got.is_err());
}
}
#[test]
fn test_successful_specifiers_table() {
let examples: Vec<(String, String)> =
from_commented_json(include_str!("test-data/successful-specifiers.txt"));
for (version_str, spec_str) in examples {
println!("Matching {:?} against {:?}", version_str, spec_str);
let version: Version = version_str.try_into().unwrap();
let specs: Specifiers = spec_str.try_into().unwrap();
println!("{:?}", specs.0[0].to_ranges());
assert!(specs.satisfied_by(&version).unwrap());
}
}
#[test]
fn test_failing_specifiers_table() {
let examples: Vec<(String, String)> =
from_commented_json(include_str!("test-data/failing-specifiers.txt"));
for (version_str, spec_str) in examples {
println!("Matching {:?} against {:?}", version_str, spec_str);
let version: Version = version_str.try_into().unwrap();
let specs: Specifiers = spec_str.try_into().unwrap();
println!("{:?}", specs.0[0].to_ranges());
assert!(!specs.satisfied_by(&version).unwrap());
}
}
}