1use reqwest_middleware::ClientWithMiddleware;
2use serde::{Deserialize, Serialize};
3use serde_json::Value;
4use std::collections::HashMap;
5
6use crate::cache::DiskCache;
7use crate::error::{DocsError, Result};
8
9const CRATESIO_BASE: &str = "https://crates.io/api/v1";
10
11#[derive(Debug, Deserialize, Serialize, Clone)]
14pub struct CrateInfo {
15 pub id: String,
16 pub name: String,
17 pub description: Option<String>,
18 pub homepage: Option<String>,
19 pub documentation: Option<String>,
20 pub repository: Option<String>,
21 pub downloads: u64,
22 pub recent_downloads: Option<u64>,
23 pub created_at: String,
24 pub updated_at: String,
25 pub max_stable_version: Option<String>,
26 pub max_version: Option<String>,
27 pub newest_version: Option<String>,
28 pub links: Option<Value>,
29 pub categories: Option<Vec<String>>,
30 pub keywords: Option<Vec<String>>,
31}
32
33#[derive(Debug, Deserialize, Serialize, Clone)]
34pub struct CrateResponse {
35 #[serde(rename = "crate")]
36 pub krate: CrateInfo,
37 pub versions: Option<Vec<VersionInfo>>,
38 pub keywords: Option<Vec<Keyword>>,
39 pub categories: Option<Vec<Category>>,
40}
41
42#[derive(Debug, Deserialize, Serialize, Clone)]
43pub struct VersionInfo {
44 pub id: u64,
45 pub num: String,
46 pub crate_id: Option<String>, pub dl_path: Option<String>,
48 pub readme_path: Option<String>,
49 pub license: Option<String>,
50 pub edition: Option<String>,
51 pub rust_version: Option<String>,
52 pub has_lib: Option<bool>,
53 pub bins: Option<Vec<String>>,
54 pub crate_size: Option<u64>,
55 pub downloads: u64,
56 pub yanked: bool,
57 pub yank_message: Option<String>,
58 pub published_by: Option<Publisher>,
59 pub created_at: String,
60 pub updated_at: Option<String>,
61 pub checksum: Option<String>,
62 pub features: Option<HashMap<String, Vec<String>>>,
63 pub links: Option<Value>,
64 pub lib_links: Option<String>,
65}
66
67#[derive(Debug, Deserialize, Serialize, Clone)]
68pub struct Publisher {
69 pub id: u64,
70 pub login: String,
71 pub name: Option<String>,
72 pub avatar: Option<String>,
73}
74
75#[derive(Debug, Deserialize, Serialize, Clone)]
76pub struct Keyword {
77 pub id: String,
78 pub keyword: String,
79 pub crates_cnt: u64,
80}
81
82#[derive(Debug, Deserialize, Serialize, Clone)]
83pub struct Category {
84 pub id: String,
85 pub category: String,
86 pub crates_cnt: u64,
87 pub description: Option<String>,
88}
89
90#[derive(Debug, Deserialize, Serialize, Clone)]
91pub struct SearchResult {
92 pub crates: Vec<CrateInfo>,
93 pub meta: SearchMeta,
94}
95
96#[derive(Debug, Deserialize, Serialize, Clone)]
97pub struct SearchMeta {
98 pub total: u64,
99}
100
101#[derive(Debug, Deserialize, Serialize, Clone)]
102pub struct VersionsResponse {
103 pub versions: Vec<VersionInfo>,
104}
105
106#[derive(Debug, Deserialize, Serialize, Clone)]
107pub struct DependenciesResponse {
108 pub dependencies: Vec<Dependency>,
109}
110
111#[derive(Debug, Deserialize, Serialize, Clone)]
112pub struct Dependency {
113 pub id: Option<u64>,
114 pub version_id: Option<u64>,
115 pub crate_id: String,
116 pub req: String,
117 pub optional: bool,
118 pub default_features: bool,
119 pub features: Vec<String>,
120 pub target: Option<String>,
121 pub kind: Option<String>,
122 pub downloads: Option<u64>,
123}
124
125#[derive(Debug, Deserialize, Serialize, Clone)]
126pub struct ReverseDepsResponse {
127 pub dependencies: Vec<ReverseDep>,
128 pub versions: Vec<ReverseDepVersion>,
129 pub meta: ReverseDepsMetaSerde,
130}
131
132#[derive(Debug, Deserialize, Serialize, Clone)]
133pub struct ReverseDep {
134 pub id: u64,
135 pub version_id: u64,
136 pub crate_id: String,
137 pub req: String,
138 pub optional: bool,
139 pub default_features: bool,
140 pub features: Vec<String>,
141 pub kind: Option<String>,
142 pub downloads: Option<u64>,
143}
144
145#[derive(Debug, Deserialize, Serialize, Clone)]
146pub struct ReverseDepVersion {
147 pub id: u64,
148 pub num: String,
149 #[serde(rename = "crate")]
150 pub crate_name: String,
151 pub downloads: u64,
152}
153
154#[derive(Debug, Deserialize, Serialize, Clone)]
155pub struct ReverseDepsMetaSerde {
156 pub total: u64,
157}
158
159#[derive(Debug, Deserialize, Serialize, Clone)]
160pub struct DownloadsResponse {
161 pub version_downloads: Vec<VersionDownload>,
162}
163
164#[derive(Debug, Deserialize, Serialize, Clone)]
165pub struct VersionDownload {
166 pub version: u64, pub downloads: u64,
168 pub date: String,
169}
170
171pub struct CratesIoClient<'a> {
174 client: &'a ClientWithMiddleware,
175 cache: &'a DiskCache,
176}
177
178impl<'a> CratesIoClient<'a> {
179 pub fn new(client: &'a ClientWithMiddleware, cache: &'a DiskCache) -> Self {
180 Self { client, cache }
181 }
182
183 pub async fn search(
184 &self,
185 query: &str,
186 category: Option<&str>,
187 keyword: Option<&str>,
188 sort: Option<&str>,
189 page: u32,
190 per_page: u32,
191 ) -> Result<SearchResult> {
192 let mut url = format!("{CRATESIO_BASE}/crates?q={query}&page={page}&per_page={per_page}");
193 if let Some(cat) = category {
194 url.push_str(&format!("&category={cat}"));
195 }
196 if let Some(kw) = keyword {
197 url.push_str(&format!("&keyword={kw}"));
198 }
199 if let Some(s) = sort {
200 url.push_str(&format!("&sort={s}"));
201 }
202 self.cache.get_json(self.client, &url).await
203 }
204
205 pub async fn get_crate(&self, name: &str) -> Result<CrateResponse> {
206 let url = format!("{CRATESIO_BASE}/crates/{name}");
207 self.cache.get_json(self.client, &url).await
208 }
209
210 pub async fn get_readme(&self, name: &str, version: &str) -> Result<String> {
211 let url = format!("{CRATESIO_BASE}/crates/{name}/{version}/readme");
212 self.cache.get_text(self.client, &url).await.or_else(|e| {
214 Err(DocsError::Other(format!("Failed to fetch README: {e}")))
215 })
216 }
217
218 pub async fn get_version(&self, name: &str, version: &str) -> Result<VersionInfo> {
219 let url = format!("{CRATESIO_BASE}/crates/{name}/{version}");
220 #[derive(Deserialize)]
221 struct Wrapper {
222 version: VersionInfo,
223 }
224 let w: Wrapper = self.cache.get_json(self.client, &url).await?;
225 Ok(w.version)
226 }
227
228 pub async fn get_versions(&self, name: &str) -> Result<VersionsResponse> {
229 let url = format!("{CRATESIO_BASE}/crates/{name}/versions");
230 self.cache.get_json(self.client, &url).await
231 }
232
233 pub async fn get_dependencies(&self, name: &str, version: &str) -> Result<DependenciesResponse> {
234 let url = format!("{CRATESIO_BASE}/crates/{name}/{version}/dependencies");
235 self.cache.get_json(self.client, &url).await
236 }
237
238 pub async fn get_reverse_deps(
239 &self,
240 name: &str,
241 page: u32,
242 per_page: u32,
243 ) -> Result<ReverseDepsResponse> {
244 let url = format!("{CRATESIO_BASE}/crates/{name}/reverse_dependencies?page={page}&per_page={per_page}");
245 self.cache.get_json(self.client, &url).await
246 }
247
248 pub async fn get_downloads(&self, name: &str, before_date: Option<&str>) -> Result<DownloadsResponse> {
249 let mut url = format!("{CRATESIO_BASE}/crates/{name}/downloads");
250 if let Some(d) = before_date {
251 url.push_str(&format!("?before_date={d}"));
252 }
253 self.cache.get_json(self.client, &url).await
254 }
255}