blob: a035f9971c5f0c8fa85968e2d84d85466b958139 [file] [log] [blame]
use crate::{cfg::Span, Spanned};
use semver::VersionReq;
use std::fmt;
use toml_span::{
de_helpers::{expected, TableHelper},
value::{Value, ValueInner},
DeserError, Deserialize,
};
/// A package identifier, consisting of a package name and a version requirement
///
/// This is specified similarly to [Cargo Package Ids](https://doc.rust-lang.org/cargo/reference/pkgid-spec.html),
/// however we change semantics a bit to instead use a [`semver::VersionReq`] instead
/// of a [`semver::Version`] as Cargo's are meant for disambiguating graph operations
/// whereas ours may be targeting single or multiple packages. In practice this
/// is mainly just a superset of Cargo's version
#[derive(Clone, PartialEq, Eq)]
pub struct PackageSpec {
pub name: Spanned<String>,
pub version_req: Option<VersionReq>,
}
impl fmt::Display for PackageSpec {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.name.value)?;
if let Some(vr) = &self.version_req {
write!(f, " = {vr}")?;
}
Ok(())
}
}
impl fmt::Debug for PackageSpec {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} = {:?}", self.name.value, self.version_req)
}
}
impl<'de> Deserialize<'de> for PackageSpec {
fn deserialize(value: &mut Value<'de>) -> Result<Self, DeserError> {
use std::borrow::Cow;
struct Ctx<'de> {
inner: Cow<'de, str>,
split: Option<(usize, bool)>,
span: Span,
}
impl<'de> Ctx<'de> {
fn from_str(bs: Cow<'de, str>, span: Span) -> Self {
let split = bs
.find('@')
.map(|i| (i, true))
.or_else(|| bs.find(':').map(|i| (i, false)));
Self {
inner: bs,
split,
span,
}
}
}
let ctx = match value.take() {
ValueInner::String(s) => Ctx::from_str(s, value.span),
ValueInner::Table(tab) => {
let mut th = TableHelper::from((tab, value.span));
if let Some(mut val) = th.table.remove(&"crate".into()) {
let s = val.take_string(Some("a crate spec"))?;
th.finalize(Some(value))?;
Ctx::from_str(s, val.span)
} else {
// Encourage user to use the 'crate' spec instead
let name = th.required("name").map_err(|e| {
if matches!(e.kind, toml_span::ErrorKind::MissingField(_)) {
(toml_span::ErrorKind::MissingField("crate"), e.span).into()
} else {
e
}
})?;
let version = th.optional::<Spanned<Cow<'_, str>>>("version");
th.finalize(Some(value))?;
let version_req = if let Some(vr) = version {
Some(vr.value.parse().map_err(|e: semver::Error| {
toml_span::Error::from((
toml_span::ErrorKind::Custom(e.to_string().into()),
vr.span,
))
})?)
} else {
None
};
return Ok(Self { name, version_req });
}
}
other => return Err(expected("a string or table", other, value.span).into()),
};
let (name, version_req) = if let Some((i, make_exact)) = ctx.split {
let mut v: VersionReq = ctx.inner[i + 1..].parse().map_err(|e: semver::Error| {
toml_span::Error::from((
toml_span::ErrorKind::Custom(e.to_string().into()),
Span::new(ctx.span.start + i + 1, ctx.span.end),
))
})?;
if make_exact {
if let Some(comp) = v.comparators.get_mut(0) {
comp.op = semver::Op::Exact;
}
}
(Spanned::with_span(ctx.inner[..i].into(), ctx.span), Some(v))
} else {
(Spanned::with_span(ctx.inner.into(), ctx.span), None)
};
Ok(Self { name, version_req })
}
}
#[cfg(test)]
impl serde::Serialize for PackageSpec {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
use serde::ser::SerializeMap;
let mut map = serializer.serialize_map(Some(3))?;
map.serialize_entry("name", &self.name.value)?;
map.serialize_entry("version-req", &self.version_req)?;
map.end()
}
}
use std::cmp::Ordering;
impl Ord for PackageSpec {
fn cmp(&self, other: &Self) -> Ordering {
match self.name.value.cmp(&other.name.value) {
Ordering::Equal => match (&self.version_req, &other.version_req) {
(None, None) => Ordering::Equal,
(Some(_), None) => Ordering::Less,
(None, Some(_)) => Ordering::Greater,
(Some(a), Some(b)) => {
if a == b {
Ordering::Equal
} else {
match a.comparators.len().cmp(&b.comparators.len()) {
Ordering::Equal => {
#[derive(PartialOrd, PartialEq, Ord, Eq)]
enum Op {
Exact,
Greater,
GreaterEq,
Less,
LessEq,
Tilde,
Caret,
Wildcard,
}
#[allow(clippy::fallible_impl_from)]
impl From<semver::Op> for Op {
fn from(op: semver::Op) -> Self {
match op {
semver::Op::Exact => Self::Exact,
semver::Op::Greater => Self::Greater,
semver::Op::GreaterEq => Self::GreaterEq,
semver::Op::Less => Self::Less,
semver::Op::LessEq => Self::LessEq,
semver::Op::Tilde => Self::Tilde,
semver::Op::Caret => Self::Caret,
semver::Op::Wildcard => Self::Wildcard,
// I fucking despise non_exhaustive
_ => panic!("semver has added a new Op, but non_exhaustive means we can't detect that at compile time, so please open an issue so that the additional match arm can be added"),
}
}
}
for (acmp, bcmp) in a.comparators.iter().zip(b.comparators.iter()) {
match Op::from(acmp.op).cmp(&Op::from(bcmp.op)) {
Ordering::Equal => {}
o => return o,
}
match acmp.major.cmp(&bcmp.major) {
Ordering::Equal => {}
o => return o,
}
match acmp.minor.cmp(&bcmp.minor) {
Ordering::Equal => {}
o => return o,
}
match acmp.patch.cmp(&bcmp.patch) {
Ordering::Equal => {}
o => return o,
}
match acmp.pre.cmp(&bcmp.pre) {
Ordering::Equal => {}
o => return o,
}
}
Ordering::Equal
}
o => o,
}
}
}
},
o => o,
}
}
}
impl PartialOrd for PackageSpec {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
#[cfg_attr(test, derive(serde::Serialize))]
pub struct PackageSpecOrExtended<T> {
pub spec: PackageSpec,
pub inner: Option<T>,
}
impl<T> PackageSpecOrExtended<T> {
pub fn try_convert<V, E>(self) -> Result<PackageSpecOrExtended<V>, E>
where
V: TryFrom<T, Error = E>,
{
let inner = if let Some(i) = self.inner {
Some(V::try_from(i)?)
} else {
None
};
Ok(PackageSpecOrExtended {
spec: self.spec,
inner,
})
}
pub fn convert<V>(self) -> PackageSpecOrExtended<V>
where
V: From<T>,
{
PackageSpecOrExtended {
spec: self.spec,
inner: self.inner.map(V::from),
}
}
}
impl<'de, T> toml_span::Deserialize<'de> for PackageSpecOrExtended<T>
where
T: toml_span::Deserialize<'de>,
{
fn deserialize(value: &mut Value<'de>) -> Result<Self, DeserError> {
let spec = PackageSpec::deserialize(value)?;
// If more keys exist in the table (or string) then try to deserialize
// the rest as the "extended" portion
let inner = if value.has_keys() {
Some(T::deserialize(value)?)
} else {
None
};
Ok(Self { spec, inner })
}
}
impl<T> fmt::Debug for PackageSpecOrExtended<T>
where
T: fmt::Debug,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("PackageSpecOrExtended")
.field("spec", &self.spec)
.field("inner", &self.inner)
.finish()
}
}
impl<T> Clone for PackageSpecOrExtended<T>
where
T: Clone,
{
fn clone(&self) -> Self {
Self {
spec: self.spec.clone(),
inner: self.inner.clone(),
}
}
}
impl<T> PartialEq for PackageSpecOrExtended<T> {
fn eq(&self, other: &Self) -> bool {
self.spec.eq(&other.spec)
}
}
impl<T> Eq for PackageSpecOrExtended<T> {}
impl<T> Ord for PackageSpecOrExtended<T> {
fn cmp(&self, other: &Self) -> Ordering {
self.spec.cmp(&other.spec)
}
}
impl<T> PartialOrd for PackageSpecOrExtended<T> {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
#[cfg(test)]
#[allow(dead_code)]
mod test {
use super::*;
use crate::{cfg::ValidationContext, test_utils::ConfigData};
#[test]
fn deserializes_package_id() {
struct Boop {
data: Option<Spanned<u32>>,
}
impl<'de> Deserialize<'de> for Boop {
fn deserialize(value: &mut toml_span::value::Value<'de>) -> Result<Self, DeserError> {
let mut th = TableHelper::new(value)?;
let data = th.optional_s("data");
th.finalize(None)?;
Ok(Self { data })
}
}
#[derive(serde::Serialize)]
struct ValidBoop {
data: Option<Spanned<u32>>,
}
impl From<Boop> for ValidBoop {
fn from(value: Boop) -> Self {
Self { data: value.data }
}
}
struct TestCfg {
bare: PackageSpecOrExtended<Boop>,
specific: PackageSpecOrExtended<Boop>,
range: PackageSpecOrExtended<Boop>,
mixed: Vec<PackageSpecOrExtended<Boop>>,
}
impl<'de> Deserialize<'de> for TestCfg {
fn deserialize(value: &mut toml_span::value::Value<'de>) -> Result<Self, DeserError> {
let mut th = TableHelper::new(value)?;
let bare = th.required("bare")?;
let specific = th.required("specific")?;
let range = th.required("range")?;
let mixed = th.required("mixed")?;
th.finalize(None)?;
Ok(Self {
bare,
specific,
range,
mixed,
})
}
}
#[derive(serde::Serialize)]
struct ValidTestCfg {
bare: PackageSpecOrExtended<ValidBoop>,
specific: PackageSpecOrExtended<ValidBoop>,
range: PackageSpecOrExtended<ValidBoop>,
mixed: Vec<PackageSpecOrExtended<ValidBoop>>,
}
impl crate::cfg::UnvalidatedConfig for TestCfg {
type ValidCfg = ValidTestCfg;
fn validate(self, mut _ctx: ValidationContext<'_>) -> Self::ValidCfg {
ValidTestCfg {
bare: self.bare.convert(),
specific: self.specific.convert(),
range: self.range.convert(),
mixed: self.mixed.into_iter().map(|m| m.convert()).collect(),
}
}
}
let cd = ConfigData::<TestCfg>::load("tests/cfg/package-specs.toml");
let validated = cd.validate(|s| s);
insta::assert_json_snapshot!(validated);
}
}