use std::{fmt::Write, fs, path::PathBuf, thread, time::Duration, vec};
use crate::{
commands::{
gh::lib::get_github_username,
utils::types::{CrateData, CrateInnerData, CrateResponse, UserResponse},
},
config::Config,
errors::GeneralError,
utils::{pretty_print, table_to_markdown_table},
};
use clap::Parser;
use reqwest::blocking::Client;
fn get_user_agent() -> String {
"n4n5 (https://github.com/Its-Just-Nans/n4n5)".to_string()
}
#[derive(Parser, Debug, Clone)]
#[command(name = "list_crates")]
pub struct UtilsListCrates {
#[arg(long, default_value_t = get_github_username())]
username: String,
#[arg(long, default_value_t = get_user_agent())]
user_agent: String,
#[arg(long)]
output_markdown: Option<PathBuf>,
#[arg(long)]
output_list: Option<PathBuf>,
#[arg(long)]
output_list_full: Option<PathBuf>,
#[arg(long, default_value_t = 500)]
delay: u64,
#[arg(long)]
filtered: Option<String>,
#[arg(long, default_value_t = false)]
verbose: bool,
#[arg(long)]
specials: Option<String>,
}
impl UtilsListCrates {
pub fn get_all_crates(&self, verbose: bool, delay: u64) -> Result<Vec<String>, GeneralError> {
let client = Client::builder().user_agent(&self.user_agent).build()?;
let per_page: usize = 50;
let user_url = format!("https://crates.io/api/v1/users/{}", self.username);
let user_res: UserResponse = client.get(&user_url).send()?.json()?;
let Some(user_id) = user_res.user else {
let msg = format!("User '{}' not found on crates.io.", self.username);
eprintln!("{msg}");
return Err(GeneralError::new(msg));
};
if verbose {
println!(
"Fetching crates for user '{}' (ID: {})...",
self.username, user_id.id
);
}
let mut page = 1;
let mut all_crates: Vec<String> = Vec::new();
loop {
thread::sleep(Duration::from_millis(delay));
let url = format!(
"https://crates.io/api/v1/crates?user_id={}&page={}&per_page={}",
user_id.id, page, per_page
);
let resp: CrateResponse = client.get(&url).send()?.json()?;
if resp.crates.is_empty() {
break;
}
let crates_len = resp.crates.len();
for c in resp.crates {
all_crates.push(c.id);
}
if crates_len < per_page {
break;
}
page += 1;
}
if verbose {
println!("Found {} crates", all_crates.len());
}
Ok(all_crates)
}
pub fn generate_markdown_table<I>(&self, rows: I) -> Result<String, GeneralError>
where
I: Iterator<Item = [String; 4]>,
{
let specials_crates = if let Some(spe) = &self.specials {
spe.split(',').map(|s| s.trim().to_string()).collect()
} else {
vec![]
};
let header = [[
"Crate".to_string(),
"Description".to_string(),
"Homepage && Repo".to_string(),
]
.to_vec()];
let (mut table1, mut table2, mut table3) = (Vec::new(), Vec::new(), Vec::new());
if let Some(pattern) = &self.filtered {
for row in rows {
let to_push = row[1..].to_vec();
if specials_crates.contains(&row[0]) {
table3.push(to_push);
} else if row[2].to_lowercase().starts_with(&pattern.to_lowercase()) {
table2.push(to_push);
} else {
table1.push(to_push);
}
}
} else {
table1 = rows.map(|row| row.to_vec()).collect();
}
let mut buf = String::new();
let table1 = header.clone().into_iter().chain(table1);
let table1_markdown = table_to_markdown_table(table1, 3)?;
write!(&mut buf, "{table1_markdown}")?;
if !table2.is_empty() {
if let Some(pattern) = &self.filtered {
let first_letter = pattern.get(0..1).unwrap_or("");
let rest_letter = pattern.get(1..).unwrap_or("");
writeln!(
&mut buf,
"\n## {}{rest_letter}\n",
first_letter.to_ascii_uppercase()
)?;
} else {
writeln!(&mut buf, "\n## Filtered\n")?;
}
let table2 = header.clone().into_iter().chain(table2);
let table2_markdown = table_to_markdown_table(table2, 3)?;
write!(&mut buf, "{table2_markdown}")?;
}
if !table3.is_empty() {
writeln!(&mut buf, "\n## Others\n")?;
let table3 = header.into_iter().chain(table3);
let table3_markdown = table_to_markdown_table(table3, 3)?;
write!(&mut buf, "{table3_markdown}")?;
}
Ok(buf)
}
pub fn list_crates(&self, _config: &mut Config) -> Result<(), GeneralError> {
let all_crates = self.get_all_crates(self.verbose, self.delay)?;
if let Some(list_file) = &self.output_list {
pretty_print(&all_crates, list_file)?;
}
if self.output_list_full.is_none() && self.output_markdown.is_none() {
return Ok(());
}
let all_crates_infos: Vec<CrateData> = all_crates
.iter()
.map(|crate_name| Self::get_one_crate(crate_name, self.delay))
.filter_map(|res| match res {
Ok(val) => Some(val),
Err(err) => {
eprintln!("Error fetching crate: {err}");
None
}
})
.collect();
if let Some(file_list_full) = &self.output_list_full {
pretty_print(&all_crates_infos, file_list_full)?;
}
let Some(file_markdown) = &self.output_markdown else {
return Ok(());
};
let rows = all_crates_infos.into_iter().map(|one_crate| {
let CrateInnerData {
description,
name,
repository,
homepage,
documentation,
..
} = one_crate.krate;
let name_with_url = format!("[{}](https://crates.io/crates/{})", &name, &name);
let desc = description.unwrap_or("N/A".to_string());
let homepage = if let Some(h) = homepage {
&format!("<{h}>")
} else {
"N/A"
};
let url = if let Some(repo) = repository {
&format!("<{repo}>")
} else {
"N/A"
};
let docs = if let Some(doc) = documentation {
&format!("<{doc}>")
} else {
"N/A"
};
let infos = format!("{homepage} <br/> {url} <br/> {docs}");
[name, name_with_url, desc, infos]
});
let tables = self.generate_markdown_table(rows)?;
let mut buf = String::new();
writeln!(&mut buf, "# crates")?;
writeln!(&mut buf)?;
writeln!(&mut buf, "- <https://crates.io/users/{}>", self.username)?;
writeln!(&mut buf, "- <https://lib.rs/~{}/dash>", self.username)?;
writeln!(&mut buf)?;
writeln!(&mut buf, "## Crates")?;
writeln!(&mut buf)?;
write!(&mut buf, "{tables}")?;
if file_markdown == &PathBuf::from("-") {
print!("{buf}");
return Ok(());
}
fs::write(file_markdown, buf)?;
println!("Written to {}", file_markdown.display());
Ok(())
}
pub fn get_one_crate(crate_name: &String, delay: u64) -> Result<CrateData, GeneralError> {
thread::sleep(Duration::from_millis(delay));
let user_agent = get_user_agent();
let url = format!("https://crates.io/api/v1/crates/{crate_name}");
let client = Client::new();
let response = client
.get(&url)
.header("User-Agent", user_agent)
.send()?
.error_for_status()?
.text()?;
let crate_data: CrateData = serde_json::from_str(&response)?;
Ok(crate_data)
}
}