use std::{borrow::Cow, fmt, str::FromStr}; use binstalk_types::{crate_info::cratesio_url, maybe_owned::MaybeOwned}; use compact_str::CompactString; use miette::Diagnostic; use semver::Version; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use thiserror::Error; use url::Url; use crate::crate_info::{CrateInfo, CrateSource, SourceType}; #[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq)] pub struct CrateVersionSource { pub name: CompactString, pub version: Version, pub source: Source<'static>, } impl From<&CrateInfo> for CrateVersionSource { fn from(metadata: &CrateInfo) -> Self { use SourceType::*; let url = metadata.source.url.clone(); super::CrateVersionSource { name: metadata.name.clone(), version: metadata.current_version.clone(), source: match metadata.source.source_type { Git => Source::Git(url), Path => Source::Path(url), Registry => Source::Registry(url), }, } } } #[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq)] pub enum Source<'a> { Git(MaybeOwned<'a, Url>), Path(MaybeOwned<'a, Url>), Registry(MaybeOwned<'a, Url>), } impl Source<'static> { pub fn cratesio_registry() -> Self { Self::Registry(MaybeOwned::Borrowed(cratesio_url())) } } impl<'a> From<&'a CrateSource> for Source<'a> { fn from(source: &'a CrateSource) -> Self { use SourceType::*; let url = MaybeOwned::Borrowed(source.url.as_ref()); match source.source_type { Git => Self::Git(url), Path => Self::Path(url), Registry => Self::Registry(url), } } } impl FromStr for CrateVersionSource { type Err = CvsParseError; fn from_str(s: &str) -> Result { match s.splitn(3, ' ').collect::>()[..] { [name, version, source] => { let version = version.parse()?; let source = match source .trim_matches(&['(', ')'][..]) .splitn(2, '+') .collect::>()[..] { ["git", url] => Source::Git(Url::parse(url)?.into()), ["path", url] => Source::Path(Url::parse(url)?.into()), ["registry", url] => Source::Registry(Url::parse(url)?.into()), [kind, arg] => { return Err(CvsParseError::UnknownSourceType { kind: kind.to_string().into_boxed_str(), arg: arg.to_string().into_boxed_str(), }) } _ => return Err(CvsParseError::BadSource), }; Ok(Self { name: name.into(), version, source, }) } _ => Err(CvsParseError::BadFormat), } } } #[derive(Debug, Diagnostic, Error)] #[non_exhaustive] pub enum CvsParseError { #[error("Failed to parse url in cvs: {0}")] UrlParse(#[from] url::ParseError), #[error("Failed to parse version in cvs: {0}")] VersionParse(#[from] semver::Error), #[error("unknown source type {kind}+{arg}")] UnknownSourceType { kind: Box, arg: Box }, #[error("bad source format")] BadSource, #[error("bad CVS format")] BadFormat, } impl fmt::Display for CrateVersionSource { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let Self { name, version, source, } = &self; write!(f, "{name} {version} ({source})") } } impl fmt::Display for Source<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Source::Git(url) => write!(f, "git+{url}"), Source::Path(url) => write!(f, "path+{url}"), Source::Registry(url) => write!(f, "registry+{url}"), } } } impl Serialize for CrateVersionSource { fn serialize(&self, serializer: S) -> Result where S: Serializer, { serializer.serialize_str(&self.to_string()) } } impl<'de> Deserialize<'de> for CrateVersionSource { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { let s = Cow::<'_, str>::deserialize(deserializer)?; Self::from_str(&s).map_err(serde::de::Error::custom) } }