1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
use crate::imports::*;

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Version {
    pub major: u64,
    pub minor: u64,
    pub patch: u64,
}

impl AsRef<Version> for Version {
    fn as_ref(&self) -> &Version {
        self
    }
}

impl FromStr for Version {
    type Err = Error;

    fn from_str(s: &str) -> Result<Self> {
        let mut parts = s.split('.');
        let major = parts
            .next()
            .ok_or_else(|| Error::custom("Invalid version"))?
            .chars()
            .filter(|c| c.is_ascii_digit())
            .collect::<String>()
            .parse()?;
        let minor = parts
            .next()
            .ok_or_else(|| Error::custom("Invalid version"))?
            .chars()
            .filter(|c| c.is_ascii_digit())
            .collect::<String>()
            .parse()?;
        let patch = parts
            .next()
            .ok_or_else(|| Error::custom("Invalid version"))?
            .chars()
            .filter(|c| c.is_ascii_digit())
            .collect::<String>()
            .parse()?;
        Ok(Version {
            major,
            minor,
            patch,
        })
    }
}

impl Display for Version {
    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
        write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
    }
}

impl Version {
    pub fn is_greater_than<V>(&self, other: V) -> bool
    where
        V: AsRef<Version>,
    {
        use std::cmp::Ordering;

        let other = other.as_ref();

        matches!(
            (
                self.major.cmp(&other.major),
                self.minor.cmp(&other.minor),
                self.patch.cmp(&other.patch),
            ),
            (Ordering::Greater, _, _)
                | (Ordering::Equal, Ordering::Greater, _)
                | (Ordering::Equal, Ordering::Equal, Ordering::Greater)
        )
    }
}

#[derive(Debug, Deserialize)]
struct CrateResponse {
    #[serde(rename = "crate")]
    crate_: Crate,
}

#[derive(Debug, Deserialize)]
struct Crate {
    max_version: String,
}

pub async fn latest_crate_version<S: Display, U: Display>(
    crate_name: S,
    user_agent: U,
) -> Result<Version> {
    let url = format!("https://crates.io/api/v1/crates/{crate_name}");
    let response = http::Request::new(url)
        .with_user_agent(user_agent.to_string())
        .get_json::<CrateResponse>()
        .await?;
    response.crate_.max_version.parse()
}

#[cfg(not(target_arch = "wasm32"))]
pub mod blocking {
    use super::*;
    use reqwest::blocking::Client;
    use reqwest::header::*;

    pub fn latest_crate_version<S: Display, U: Display>(
        crate_name: S,
        user_agent: U,
    ) -> Result<Version> {
        let url = format!("https://crates.io/api/v1/crates/{crate_name}");
        let mut headers = HeaderMap::new();
        headers.insert(
            USER_AGENT,
            HeaderValue::from_str(user_agent.to_string().as_str())?,
        );
        let response = Client::builder()
            .default_headers(headers)
            .build()?
            .get(url)
            .send()?
            .json::<CrateResponse>()?;
        response.crate_.max_version.parse()
    }
}