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 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}