use crate::Page; use kitchen_sink::CResult; use kitchen_sink::KitchenSink; use kitchen_sink::Origin; use kitchen_sink::RevDependencies; use kitchen_sink::SemVer; use locale::Numeric; use render_readme::Renderer; use rich_crate::RichCrateVersion; use semver::VersionReq; use std::fmt::Display; pub struct CratePageRevDeps<'a> { pub ver: &'a RichCrateVersion, pub deps: Vec>, pub stats: Option<&'a RevDependencies>, pub has_download_columns: bool, downloads_by_ver: Vec<(SemVer, u32)>, } pub struct RevDepInf<'a> { pub origin: Origin, pub downloads: usize, pub depender: &'a kitchen_sink::Version, pub is_optional: bool, pub matches_latest: bool, pub kind: &'a str, pub req: VersionReq, pub rev_dep_count: u32, } pub struct DlRow { pub ver: SemVer, pub num: u16, pub num_str: String, pub perc: f32, pub num_width: f32, pub dl: u32, pub dl_str: (String, &'static str), pub dl_perc: f32, pub dl_num_width: f32, } impl<'a> CratePageRevDeps<'a> { pub async fn new(ver: &'a RichCrateVersion, kitchen_sink: &'a KitchenSink, _markup: &'a Renderer) -> CResult> { let all_deps_stats = kitchen_sink.index.deps_stats().await?; let own_name = &ver.short_name().to_ascii_lowercase(); let latest_stable_semver = &kitchen_sink.index.crate_highest_version(&own_name, true)?.version().parse()?; let latest_unstable_semver = &kitchen_sink.index.crate_highest_version(&own_name, false)?.version().parse()?; let stats = all_deps_stats.counts.get(own_name.as_str()); let mut downloads_by_ver: Vec<_> = kitchen_sink.recent_downloads_by_version(ver)?.into_iter().map(|(v, d)| (v.to_semver(), d)).collect(); downloads_by_ver.sort_by(|a, b| b.0.cmp(&a.0)); let mut deps: Vec<_> = match stats { Some(s) => futures::future::join_all(s.rev_dep_names.iter().map(|rev_dep| async move { let origin = Origin::from_crates_io_name(rev_dep); let downloads = kitchen_sink.downloads_per_month(&origin).await.ok().and_then(|x| x).unwrap_or(0); let depender = kitchen_sink.index.crate_highest_version(&rev_dep.to_lowercase(), true).expect("rev dep integrity"); let (is_optional, req, kind) = depender.dependencies().iter().find(|d| { own_name.eq_ignore_ascii_case(d.crate_name()) }) .map(|d| { (d.is_optional(), d.requirement(), d.kind().unwrap_or_default()) }) .unwrap_or_default(); let req = req.parse().unwrap_or_else(|_| VersionReq::any()); let matches_latest = req.matches(&latest_stable_semver) || req.matches(&latest_unstable_semver); RevDepInf { origin, depender, downloads, is_optional, req, kind, matches_latest, rev_dep_count: 0, } })).await, None => Vec::new(), }; // sort by downloads if > 100, then by name deps.sort_by(|a, b| { b.downloads.max(100).cmp(&a.downloads.max(100)) .then_with(|| { a.depender.name().cmp(b.depender.name()) }) }); deps.truncate(1000); for d in deps.iter_mut() { d.rev_dep_count = all_deps_stats.counts.get(d.depender.name()).map(|s| s.direct.all()).unwrap_or(0); } let has_download_columns = deps.iter().any(|d| d.rev_dep_count > 0 || d.downloads > 100); Ok(Self { ver, deps, stats, downloads_by_ver, has_download_columns, }) } /// Nicely rounded number of downloads /// /// To show that these numbers are just approximate. pub fn downloads(&self, num: usize) -> (String, &'static str) { match num { a @ 0..=99 => (format!("{}", a), ""), a @ 0..=500 => (format!("{}", a / 10 * 10), ""), a @ 0..=999 => (format!("{}", a / 50 * 50), ""), a @ 0..=9999 => (format!("{}.{}", a / 1000, a % 1000 / 100), "K"), a @ 0..=999_999 => (format!("{}", a / 1000), "K"), a => (format!("{}.{}", a / 1_000_000, a % 1_000_000 / 100_000), "M"), } } pub fn format_number(&self, num: impl Display) -> String { Numeric::english().format_int(num) } // version, deps, normalized popularity 0..100 pub fn version_breakdown(&self) -> Vec { let stats = match self.stats { None => return Vec::new(), Some(s) => s, }; let mut ver: Vec<_> = stats.versions.iter().map(|(k, v)| { DlRow { ver: k.to_semver(), num: *v, perc: 0., num_width: 0., dl: 0, dl_perc: 0., dl_num_width: 0., num_str: String::new(), dl_str: (String::new(),""), } }).collect(); // Ensure the (latest) version is always included let own_ver_semver: SemVer = self.ver.version().parse().expect("semver2"); if !ver.iter().any(|v| v.ver == own_ver_semver) { ver.push(DlRow { ver: own_ver_semver, num: 0, perc: 0., num_width: 0., dl: 0, dl_perc: 0., dl_num_width: 0., num_str: String::new(), dl_str: (String::new(), ""), }); } // Download data may be older and not match exactly, so at least avoid // accidentally omitting the most popular version if let Some(biggest) = self.downloads_by_ver.iter().max_by_key(|v| v.1) { if !ver.iter().any(|v| v.ver == biggest.0) { ver.push(DlRow { ver: biggest.0.clone(), num: 0, perc: 0., num_width: 0., dl: 0, dl_perc: 0., dl_num_width: 0., num_str: String::new(), dl_str: (String::new(), ""), }); } } // align selected versions and their (or older) downloads let mut dl_vers = self.downloads_by_ver.iter().rev().peekable(); ver.sort_by(|a, b| b.ver.cmp(&a.ver)); for curr in ver.iter_mut().rev() { let mut sum = 0; while let Some((next_ver, dl)) = dl_vers.peek() { if next_ver > &curr.ver { break; } if next_ver.major == curr.ver.major && (next_ver.major != 0 || next_ver.minor == curr.ver.minor) { sum += dl; } dl_vers.next(); } curr.dl = sum; } let max = ver.iter().map(|v| v.num).max().unwrap_or(1) as f32; let dl_max = ver.iter().map(|v| v.dl).max().unwrap_or(1) as f32; for i in ver.iter_mut() { i.perc = i.num as f32 / max * 100.0; i.num_str = self.format_number(i.num); i.num_width = 4. + 7. * i.num_str.len() as f32; // approx visual width of the number i.dl_perc = i.dl as f32 / dl_max * 100.0; i.dl_str = self.downloads(i.dl as usize); i.dl_num_width = 4. + 7. * (i.dl_str.0.len() + i.dl_str.1.len()) as f32; // approx visual width of the number } ver } pub fn page(&self) -> Page { Page { title: format!("Reverse dependencies of {}", self.ver.short_name()), item_name: Some(self.ver.short_name().to_string()), item_description: self.ver.description().map(|d| d.to_string()), noindex: true, search_meta: false, critical_css_data: Some(include_str!("../../style/public/revdeps.css")), critical_css_dev_url: Some("/revdeps.css"), ..Default::default() } } }