krate/
lib.rs

1use reqwest::{ClientBuilder, Response};
2use serde::Deserialize;
3use std::collections::HashMap;
4use thiserror::Error;
5
6type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
7
8const CRATES_IO_URL: &str = "https://crates.io/api/v1/crates";
9const UNIQUE_USER_AGENT: &str = "krates/0.3.0";
10
11#[derive(Error, Debug)]
12enum KrateError {
13    #[error("Crate name is not found. Did you mispell the crate name?")]
14    KrateNotFound,
15    #[error("User Agent must be a string with at least one character")]
16    UserAgentNotProvided,
17    #[error("Server Status Error: {0}")]
18    OtherKrateError(reqwest::Error),
19}
20
21impl Krate {
22    pub fn get_latest(&self) -> String {
23        String::from(&self.versions[0].num)
24    }
25
26    pub fn get_features_for_version(&self, version: &str) -> Option<&HashMap<String, Vec<String>>> {
27        for v in &self.versions {
28            if v.num == version {
29                if let Some(features) = &v.features {
30                    return Some(features);
31                }
32            }
33        }
34        None
35    }
36}
37
38#[derive(Debug, Clone, Deserialize)]
39pub struct Krate {
40    pub categories: Option<Vec<KrateCategory>>,
41    pub versions: Vec<KrateVersion>,
42    #[serde(rename = "crate")]
43    pub krate: KrateMetadata,
44    pub keywords: Option<Vec<Option<KrateKeyword>>>,
45}
46
47#[derive(Debug, Clone, Deserialize)]
48pub struct KrateVersion {
49    pub crate_size: Option<i64>,
50    pub license: Option<String>,
51    pub num: String,
52    pub readme_path: String,
53    pub yanked: bool,
54    pub features: Option<HashMap<String, Vec<String>>>,
55    pub id: i64,
56}
57
58#[derive(Debug, Clone, Deserialize)]
59pub struct KrateCategory {
60    pub category: String,
61    pub crates_cnt: i32,
62    pub created_at: String,
63    pub description: String,
64    pub id: String,
65    pub slug: String,
66}
67
68#[derive(Debug, Clone, Deserialize)]
69pub struct KrateMetadata {
70    pub categories: Vec<String>,
71    pub created_at: String,
72    pub description: String,
73    pub documentation: Option<String>,
74    pub downloads: i32,
75    pub exact_match: bool,
76    pub homepage: Option<String>,
77    pub id: String,
78    pub keywords: Vec<String>,
79    //links:
80    pub max_version: String,
81    pub max_stable_version: String,
82    pub name: String,
83    pub newest_version: String,
84    pub recent_downloads: i64,
85    pub repository: Option<String>,
86    pub updated_at: String,
87    pub versions: Vec<i32>,
88}
89
90#[derive(Debug, Clone, Deserialize)]
91pub struct KrateKeyword {
92    pub crates_cnt: i64,
93    pub created_at: String,
94    pub id: String,
95    pub keyword: String,
96}
97
98#[derive(Debug)]
99pub struct SyncKrateClient {
100    client: reqwest::blocking::Client,
101}
102
103#[derive(Debug)]
104pub struct AsyncKrateClient {
105    client: reqwest::Client,
106}
107
108impl SyncKrateClient {
109    pub fn get(&self, crate_name: &str) -> Result<Krate> {
110        let url = format!("{CRATES_IO_URL}/{crate_name}");
111
112        let res = self.client.get(url).send()?;
113        match res.error_for_status() {
114            Ok(res) => {
115                let krate: Krate = res.json()?;
116                Ok(krate)
117            }
118            Err(e) => Err(handle_error(e).into()),
119        }
120    }
121}
122
123impl AsyncKrateClient {
124    pub async fn get_async(&self, crate_name: &str) -> Result<Krate> {
125        let url = format!("{CRATES_IO_URL}/{crate_name}");
126        let res: Response = self.client.get(url).send().await?;
127
128        match res.error_for_status() {
129            Ok(res) => {
130                let krate: Krate = res.json().await?;
131                Ok(krate)
132            }
133            Err(e) => Err(handle_error(e).into()),
134        }
135    }
136}
137
138pub struct KrateClientBuilder {
139    user_agent: String,
140}
141
142impl KrateClientBuilder {
143    pub fn new(user_agent: &str) -> KrateClientBuilder {
144        KrateClientBuilder {
145            user_agent: user_agent.to_string(),
146        }
147    }
148
149    pub fn build_sync(&self) -> Result<SyncKrateClient> {
150        if has_empty_user_agent(&self.user_agent) {
151            return Err(Box::new(KrateError::UserAgentNotProvided));
152        }
153
154        let operator_user_agent = format!(
155            "{} - Brought to you by: {UNIQUE_USER_AGENT}",
156            self.user_agent
157        );
158
159        let client = reqwest::blocking::ClientBuilder::new()
160            .user_agent(&operator_user_agent)
161            .build()?;
162
163        return Ok(SyncKrateClient { client: client });
164    }
165
166    pub fn build_asnyc(&self) -> Result<AsyncKrateClient> {
167        if has_empty_user_agent(&self.user_agent) {
168            if has_empty_user_agent(&self.user_agent) {
169                return Err(Box::new(KrateError::UserAgentNotProvided));
170            }
171        }
172
173        let operator_user_agent = format!(
174            "{} - Brought to you by: {UNIQUE_USER_AGENT}",
175            self.user_agent
176        );
177
178        let client = reqwest::ClientBuilder::new()
179            .user_agent(&operator_user_agent)
180            .build()?;
181
182        return Ok(AsyncKrateClient { client: client });
183    }
184}
185
186fn handle_error(e: reqwest::Error) -> KrateError {
187    if e.status() == Some(reqwest::StatusCode::NOT_FOUND) {
188        KrateError::KrateNotFound
189    } else {
190        KrateError::OtherKrateError(e)
191    }
192}
193
194fn has_empty_user_agent(user_agent: &str) -> bool {
195    user_agent.trim().len() == 0
196}
197
198pub fn get(crate_name: &str, user_agent: &str) -> Result<Krate> {
199    let url = format!("{CRATES_IO_URL}/{crate_name}");
200    let client = reqwest::blocking::ClientBuilder::new()
201        .user_agent(format!(
202            "{user_agent} - Brought to you by: {UNIQUE_USER_AGENT}",
203        ))
204        .build()?;
205
206    let res = client.get(url).send()?;
207    match res.error_for_status() {
208        Ok(res) => {
209            let krate: Krate = res.json()?;
210            Ok(krate)
211        }
212        Err(e) => Err(handle_error(e).into()),
213    }
214}
215
216pub async fn get_async(crate_name: &str, user_agent: &str) -> Result<Krate> {
217    let url = format!("{CRATES_IO_URL}/{crate_name}");
218
219    let client = ClientBuilder::new()
220        .user_agent(format!(
221            "{user_agent} - Brought to you by: {UNIQUE_USER_AGENT}",
222        ))
223        .build()?;
224
225    let res: Response = client.get(url).send().await?;
226
227    match res.error_for_status() {
228        Ok(res) => {
229            let krate: Krate = res.json().await?;
230            Ok(krate)
231        }
232        Err(e) => Err(handle_error(e).into()),
233    }
234}
235
236#[cfg(test)]
237mod tests {
238    use super::*;
239
240    fn client_builder() -> KrateClientBuilder {
241        KrateClientBuilder::new("Test Mocks for TheLarkInn/krate")
242    }
243
244    fn get_sync_krate_client() -> SyncKrateClient {
245        client_builder().build_sync().unwrap()
246    }
247
248    fn get_async_krate_client() -> AsyncKrateClient {
249        client_builder().build_asnyc().unwrap()
250    }
251
252    #[tokio::test]
253    async fn test_get_async_crate_basic() {
254        let krate = get_async_krate_client().get_async("is-wsl").await.unwrap();
255        assert_eq!(krate.krate.name, "is-wsl");
256    }
257
258    #[tokio::test]
259    async fn test_get_async_latest_version_from_crate() {
260        let krate: Krate = get_async_krate_client().get_async("tokio").await.unwrap();
261        assert_eq!(krate.get_latest(), krate.versions[0].num);
262    }
263
264    #[tokio::test]
265    async fn test_get_async_informs_operator_of_not_found_error() {
266        let krate = get_async_krate_client().get_async("tokioz").await;
267        assert!(krate.is_err());
268        assert_eq!(
269            krate.err().unwrap().to_string(),
270            "Crate name is not found. Did you mispell the crate name?"
271        );
272    }
273
274    #[tokio::test]
275    async fn test_get_async_errors_on_empty_user_agent() {
276        let builder = KrateClientBuilder::new("     ").build_asnyc();
277
278        assert_eq!(
279            builder.err().unwrap().to_string(),
280            "User Agent must be a string with at least one character"
281        );
282    }
283
284    #[test]
285    fn test_get_crate_basic() {
286        let krate = get_sync_krate_client().get("is-interactive").unwrap();
287        assert_eq!(krate.krate.name, "is-interactive");
288        assert_eq!(krate.versions[0].num, "0.1.0");
289        assert_eq!(
290            krate.krate.description,
291            "Checks if stdout or stderr is interactive"
292        );
293    }
294
295    #[test]
296    fn test_get_get_latest() {
297        let krate: Krate = get_sync_krate_client().get("syn").unwrap();
298        assert_eq!(krate.get_latest(), krate.versions[0].num);
299    }
300
301    #[test]
302    fn test_get_features_for_version() {
303        let krate: Krate = get_sync_krate_client().get("tokio").unwrap();
304        let features = krate.get_features_for_version("1.24.2");
305        assert_eq!(features.unwrap().len(), 15);
306    }
307
308    #[test]
309    fn test_get_features_for_wrong_version() {
310        let krate: Krate = get_sync_krate_client().get("cargo-outdated").unwrap();
311        let features = krate.get_features_for_version("9999.0.00");
312        assert!(features.is_none());
313    }
314
315    #[test]
316    fn test_edge_case_packages_without_data() {
317        let krate = get_sync_krate_client().get("rustc-workspace-hack");
318
319        assert!(krate.is_ok())
320    }
321}