Skip to main content

workflow_utils/
version.rs

1use crate::imports::*;
2
3/// A semantic version composed of major, minor and patch components.
4#[derive(Debug, Clone, PartialEq, Eq)]
5pub struct Version {
6    /// The major version component.
7    pub major: u64,
8    /// The minor version component.
9    pub minor: u64,
10    /// The patch version component.
11    pub patch: u64,
12}
13
14impl AsRef<Version> for Version {
15    fn as_ref(&self) -> &Version {
16        self
17    }
18}
19
20impl FromStr for Version {
21    type Err = Error;
22
23    fn from_str(s: &str) -> Result<Self> {
24        let mut parts = s.split('.');
25        let major = parts
26            .next()
27            .ok_or_else(|| Error::custom("Invalid version"))?
28            .chars()
29            .filter(|c| c.is_ascii_digit())
30            .collect::<String>()
31            .parse()?;
32        let minor = parts
33            .next()
34            .ok_or_else(|| Error::custom("Invalid version"))?
35            .chars()
36            .filter(|c| c.is_ascii_digit())
37            .collect::<String>()
38            .parse()?;
39        let patch = parts
40            .next()
41            .ok_or_else(|| Error::custom("Invalid version"))?
42            .chars()
43            .filter(|c| c.is_ascii_digit())
44            .collect::<String>()
45            .parse()?;
46        Ok(Version {
47            major,
48            minor,
49            patch,
50        })
51    }
52}
53
54impl Display for Version {
55    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
56        write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
57    }
58}
59
60impl Version {
61    /// Returns `true` if this version is strictly greater than `other`,
62    /// comparing major, then minor, then patch components.
63    pub fn is_greater_than<V>(&self, other: V) -> bool
64    where
65        V: AsRef<Version>,
66    {
67        use std::cmp::Ordering;
68
69        let other = other.as_ref();
70
71        matches!(
72            (
73                self.major.cmp(&other.major),
74                self.minor.cmp(&other.minor),
75                self.patch.cmp(&other.patch),
76            ),
77            (Ordering::Greater, _, _)
78                | (Ordering::Equal, Ordering::Greater, _)
79                | (Ordering::Equal, Ordering::Equal, Ordering::Greater)
80        )
81    }
82}
83
84#[derive(Debug, Deserialize)]
85struct CrateResponse {
86    #[serde(rename = "crate")]
87    crate_: Crate,
88}
89
90#[derive(Debug, Deserialize)]
91struct Crate {
92    max_version: String,
93}
94
95/// Asynchronously fetches the latest published version of `crate_name` from
96/// crates.io, identifying the request with the given `user_agent`.
97pub async fn latest_crate_version<S: Display, U: Display>(
98    crate_name: S,
99    user_agent: U,
100) -> Result<Version> {
101    let url = format!("https://crates.io/api/v1/crates/{crate_name}");
102    let response = http::Request::new(url)
103        .with_user_agent(user_agent.to_string())
104        .get_json::<CrateResponse>()
105        .await?;
106    response.crate_.max_version.parse()
107}
108
109/// Blocking (non-`wasm32`) variants of the crates.io version helpers.
110#[cfg(not(target_arch = "wasm32"))]
111pub mod blocking {
112    use super::*;
113    use reqwest::blocking::Client;
114    use reqwest::header::*;
115
116    /// Synchronously fetches the latest published version of `crate_name` from
117    /// crates.io, identifying the request with the given `user_agent`.
118    pub fn latest_crate_version<S: Display, U: Display>(
119        crate_name: S,
120        user_agent: U,
121    ) -> Result<Version> {
122        let url = format!("https://crates.io/api/v1/crates/{crate_name}");
123        let mut headers = HeaderMap::new();
124        headers.insert(
125            USER_AGENT,
126            HeaderValue::from_str(user_agent.to_string().as_str())?,
127        );
128        let response = Client::builder()
129            .default_headers(headers)
130            .build()?
131            .get(url)
132            .send()?
133            .json::<CrateResponse>()?;
134        response.crate_.max_version.parse()
135    }
136}