Skip to main content

docs_mcp/cratesio/
client.rs

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// ─── Response types ────────────────────────────────────────────────────────────
12
13#[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>,  // sometimes missing
47    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, // version ID
167    pub downloads: u64,
168    pub date: String,
169}
170
171// ─── Client ───────────────────────────────────────────────────────────────────
172
173pub 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        // README endpoint returns HTML; we fetch as text
213        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}