crates_readme_table/
api.rs1use reqwest::Client;
4use serde::Deserialize;
5use urlogger::{LogLevel, log};
6
7pub const CRATES_IO_USER_AGENT: &str =
9 "crates-readme-table (https://github.com/un-rust/template; help@crates.io for issues)";
10
11const CRATES_IO_API: &str = "https://crates.io/api/v1/crates";
12
13#[derive(Debug, Deserialize)]
14pub struct Crate {
15 pub id: String,
16 pub name: String,
17 pub description: Option<String>,
18 pub homepage: Option<String>,
19 pub repository: Option<String>,
20 #[serde(default)]
21 pub links: Option<CrateLinks>,
22}
23
24#[derive(Debug, Deserialize)]
25pub struct CrateLinks {
26 pub version_downloads: Option<String>,
27 pub versions: Option<String>,
28 pub owners: Option<String>,
29 pub owner_team: Option<String>,
30 pub owner_user: Option<String>,
31 pub reverse_dependencies: Option<String>,
32}
33
34#[derive(Debug, Deserialize)]
35pub struct Meta {
36 pub total: u64,
37 pub next_page: Option<u32>,
38 pub prev_page: Option<u32>,
39}
40
41#[derive(Debug, Deserialize)]
42pub struct CratesResponse {
43 pub crates: Vec<Crate>,
44 pub meta: Meta,
45}
46
47pub async fn get_crates(
49 client: &Client,
50 page: u32,
51 per_page: u32,
52 sort: &str,
53 user_id: u32,
54) -> Result<CratesResponse, Box<dyn std::error::Error + Send + Sync>> {
55 let url = format!(
56 "{}?page={}&per_page={}&sort={}&user_id={}",
57 CRATES_IO_API, page, per_page, sort, user_id
58 );
59 log!(LogLevel::Debug, "Fetching page {} from crates.io", page);
60 let resp = client.get(&url).send().await?;
61 let text = resp.text().await?;
62
63 let value: serde_json::Value =
64 serde_json::from_str(&text).map_err(|e| format!("response not JSON: {}", e))?;
65
66 if let Some(errors) = value.get("errors").and_then(|e| e.as_array()) {
68 if !errors.is_empty() {
69 let msg: String = errors
70 .iter()
71 .filter_map(|e| e.get("detail").and_then(|d| d.as_str()))
72 .collect::<Vec<_>>()
73 .join("; ");
74 return Err(if msg.is_empty() {
75 "API returned errors".into()
76 } else {
77 msg.into()
78 });
79 }
80 }
81
82 let crates_value = value
84 .get("crates")
85 .or_else(|| value.get("crate"))
86 .ok_or_else(|| {
87 let keys: Vec<&str> = value
88 .as_object()
89 .map(|o| o.keys().map(String::as_str).collect())
90 .unwrap_or_default();
91 format!(
92 "API response has no 'crates' or 'crate' field. Top-level keys: {:?}",
93 keys
94 )
95 })?;
96
97 let crates: Vec<Crate> = serde_json::from_value(crates_value.clone())
98 .map_err(|e| format!("failed to parse crates array: {}", e))?;
99
100 let meta = value
101 .get("meta")
102 .ok_or_else(|| "API response has no 'meta' field".to_string())?;
103 let meta: Meta = serde_json::from_value(meta.clone()).map_err(|e| format!("meta: {}", e))?;
104
105 log!(
106 LogLevel::Debug,
107 "Fetched {} crates on page {}",
108 crates.len(),
109 page
110 );
111 Ok(CratesResponse { crates, meta })
112}
113
114pub async fn get_all_crates(
116 client: &Client,
117 user_id: u32,
118) -> Result<Vec<Crate>, Box<dyn std::error::Error + Send + Sync>> {
119 log!(LogLevel::Info, "Fetching crates for user_id={}", user_id);
120
121 let mut crate_list = Vec::new();
122 let mut page = 1u32;
123
124 loop {
125 let data = get_crates(client, page, 10, "alpha", user_id).await?;
126 let crates = data.crates;
127 let meta = data.meta;
128 crate_list.extend(crates);
129
130 log!(
131 LogLevel::Info,
132 "Page {} done, total so far: {} (meta.total={})",
133 page,
134 crate_list.len(),
135 meta.total
136 );
137
138 match meta.next_page {
139 Some(next) => page = next,
140 None => break,
141 }
142 }
143
144 log!(
145 LogLevel::Info,
146 "Fetched {} crates for user {}",
147 crate_list.len(),
148 user_id
149 );
150
151 Ok(crate_list)
152}