| 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); |
| } |
| } |