binstalk_manifests/cargo_crates_v1/
crate_version_source.rs

1use std::{
2    borrow::Cow,
3    fmt::{self, Write as _},
4    str::FromStr,
5};
6
7use binstalk_types::maybe_owned::MaybeOwned;
8use compact_str::CompactString;
9use miette::Diagnostic;
10use semver::Version;
11use serde::{Deserialize, Deserializer, Serialize, Serializer};
12use thiserror::Error;
13use url::Url;
14
15use crate::crate_info::{CrateInfo, CrateSource, SourceType};
16
17#[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq)]
18pub struct CrateVersionSource {
19    pub name: CompactString,
20    pub version: Version,
21    pub source: Source<'static>,
22}
23
24impl From<&CrateInfo> for CrateVersionSource {
25    fn from(metadata: &CrateInfo) -> Self {
26        use SourceType::*;
27
28        let url = metadata.source.url.clone();
29
30        super::CrateVersionSource {
31            name: metadata.name.clone(),
32            version: metadata.current_version.clone(),
33            source: match metadata.source.source_type {
34                Git => Source::Git(url),
35                Path => Source::Path(url),
36                Registry => Source::Registry(url),
37                Sparse => Source::Sparse(url),
38            },
39        }
40    }
41}
42
43#[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq)]
44pub enum Source<'a> {
45    Git(MaybeOwned<'a, Url>),
46    Path(MaybeOwned<'a, Url>),
47    Registry(MaybeOwned<'a, Url>),
48    Sparse(MaybeOwned<'a, Url>),
49}
50
51impl<'a> From<&'a CrateSource> for Source<'a> {
52    fn from(source: &'a CrateSource) -> Self {
53        use SourceType::*;
54
55        let url = MaybeOwned::Borrowed(source.url.as_ref());
56
57        match source.source_type {
58            Git => Self::Git(url),
59            Path => Self::Path(url),
60            Registry => Self::Registry(url),
61            Sparse => Self::Sparse(url),
62        }
63    }
64}
65
66impl FromStr for CrateVersionSource {
67    type Err = CvsParseError;
68    fn from_str(s: &str) -> Result<Self, Self::Err> {
69        match s.splitn(3, ' ').collect::<Vec<_>>()[..] {
70            [name, version, source] => {
71                let version = version.parse()?;
72                let source = match source
73                    .trim_matches(&['(', ')'][..])
74                    .splitn(2, '+')
75                    .collect::<Vec<_>>()[..]
76                {
77                    ["git", url] => Source::Git(Url::parse(url)?.into()),
78                    ["path", url] => Source::Path(Url::parse(url)?.into()),
79                    ["registry", url] => Source::Registry(Url::parse(url)?.into()),
80                    [kind, arg] => {
81                        return Err(CvsParseError::UnknownSourceType {
82                            kind: kind.to_string().into_boxed_str(),
83                            arg: arg.to_string().into_boxed_str(),
84                        })
85                    }
86                    _ => return Err(CvsParseError::BadSource),
87                };
88                Ok(Self {
89                    name: name.into(),
90                    version,
91                    source,
92                })
93            }
94            _ => Err(CvsParseError::BadFormat),
95        }
96    }
97}
98
99#[derive(Debug, Diagnostic, Error)]
100#[non_exhaustive]
101pub enum CvsParseError {
102    #[error("Failed to parse url in cvs: {0}")]
103    UrlParse(#[from] url::ParseError),
104
105    #[error("Failed to parse version in cvs: {0}")]
106    VersionParse(#[from] semver::Error),
107
108    #[error("unknown source type {kind}+{arg}")]
109    UnknownSourceType { kind: Box<str>, arg: Box<str> },
110
111    #[error("bad source format")]
112    BadSource,
113
114    #[error("bad CVS format")]
115    BadFormat,
116}
117
118impl fmt::Display for CrateVersionSource {
119    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
120        let Self {
121            name,
122            version,
123            source,
124        } = &self;
125        write!(f, "{name} {version} ({source})")
126    }
127}
128
129impl fmt::Display for Source<'_> {
130    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
131        match self {
132            Source::Git(url) => write!(f, "git+{url}"),
133            Source::Path(url) => write!(f, "path+{url}"),
134            Source::Registry(url) => write!(f, "registry+{url}"),
135            Source::Sparse(url) => {
136                let url = url.as_str();
137                write!(f, "sparse+{url}")?;
138                if url.ends_with("/") {
139                    Ok(())
140                } else {
141                    f.write_char('/')
142                }
143            }
144        }
145    }
146}
147
148impl Serialize for CrateVersionSource {
149    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
150    where
151        S: Serializer,
152    {
153        serializer.serialize_str(&self.to_string())
154    }
155}
156
157impl<'de> Deserialize<'de> for CrateVersionSource {
158    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
159    where
160        D: Deserializer<'de>,
161    {
162        let s = Cow::<'_, str>::deserialize(deserializer)?;
163        Self::from_str(&s).map_err(serde::de::Error::custom)
164    }
165}