use std::collections::BTreeSet;
use serde::{Deserialize, Serialize};
use time::OffsetDateTime;
use crate::collectors::adoption::AdoptionRawData;
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
pub struct AdoptionFeatures {
pub weekly_downloads: Option<u64>,
pub package_systems: Vec<String>,
pub package_systems_count: u64,
pub has_readme: bool,
pub readme_word_count: Option<usize>,
pub has_docs_dir: bool,
pub has_examples_dir: bool,
pub documentation_maturity_score: f64,
pub awesome_list_mentions: u64,
pub archived: bool,
pub deps_dev_error: bool,
}
#[must_use]
pub fn compute(raw: &AdoptionRawData, _now: OffsetDateTime) -> AdoptionFeatures {
let weekly_downloads = if raw.packages.is_empty() {
None
} else {
let mut had_value = false;
let mut sum: u64 = 0;
for p in &raw.packages {
if let Some(v) = p.weekly_downloads {
had_value = true;
sum = sum.saturating_add(v);
}
}
if had_value {
Some(sum)
} else {
None
}
};
let systems_set: BTreeSet<String> = raw.packages.iter().map(|p| p.system.clone()).collect();
let package_systems: Vec<String> = systems_set.into_iter().collect();
let package_systems_count = u64::try_from(package_systems.len()).unwrap_or(0);
let documentation_maturity_score = doc_maturity(
raw.has_readme,
raw.readme_word_count,
raw.has_docs_dir,
raw.has_examples_dir,
);
AdoptionFeatures {
weekly_downloads,
package_systems,
package_systems_count,
has_readme: raw.has_readme,
readme_word_count: raw.readme_word_count,
has_docs_dir: raw.has_docs_dir,
has_examples_dir: raw.has_examples_dir,
documentation_maturity_score: crate::utils::time::round6(documentation_maturity_score),
awesome_list_mentions: u64::try_from(raw.awesome_list_mentions.len()).unwrap_or(0),
archived: raw.archived,
deps_dev_error: raw.deps_dev_error,
}
}
#[must_use]
pub fn doc_maturity(
has_readme: bool,
word_count: Option<usize>,
has_docs_dir: bool,
has_examples_dir: bool,
) -> f64 {
let readme: f64 = if has_readme {
match word_count.unwrap_or(0) {
n if n >= 500 => 0.50,
n if n >= 100 => 0.35,
_ => 0.20,
}
} else {
0.0
};
let docs: f64 = if has_docs_dir { 0.30 } else { 0.0 };
let examples: f64 = if has_examples_dir { 0.20 } else { 0.0 };
(readme + docs + examples).clamp(0.0, 1.0)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::api::deps_dev::PackageInfo;
fn pkg(system: &str, name: &str, downloads: Option<u64>) -> PackageInfo {
PackageInfo {
system: system.into(),
name: name.into(),
weekly_downloads: downloads,
latest_version: None,
}
}
fn now() -> OffsetDateTime {
OffsetDateTime::from_unix_timestamp(1_780_000_000).unwrap()
}
#[test]
fn doc_maturity_full_signal_caps_at_one() {
let s = doc_maturity(true, Some(800), true, true);
assert!((s - 1.0).abs() < 1e-9, "expected 1.0, got {s}");
}
#[test]
fn doc_maturity_no_readme_anchors_zero_signal() {
let s = doc_maturity(false, None, false, false);
assert_eq!(s, 0.0);
}
#[test]
fn doc_maturity_short_readme_only_yields_low() {
let s = doc_maturity(true, Some(20), false, false);
assert!((s - 0.20).abs() < 1e-9, "expected 0.20, got {s}");
}
#[test]
fn weekly_downloads_sums_across_packages() {
let raw = AdoptionRawData {
packages: vec![
pkg("NPM", "left-pad", Some(1_000)),
pkg("CARGO", "serde", Some(2_500)),
pkg("PYPI", "requests", None), ],
..AdoptionRawData::default()
};
let f = compute(&raw, now());
assert_eq!(f.weekly_downloads, Some(3_500));
}
#[test]
fn weekly_downloads_none_when_all_packages_have_none() {
let raw = AdoptionRawData {
packages: vec![pkg("NPM", "a", None), pkg("CARGO", "b", None)],
..AdoptionRawData::default()
};
let f = compute(&raw, now());
assert_eq!(f.weekly_downloads, None);
}
#[test]
fn package_systems_sorted_and_unique() {
let raw = AdoptionRawData {
packages: vec![
pkg("NPM", "x", Some(10)),
pkg("CARGO", "y", Some(20)),
pkg("NPM", "z", Some(30)), pkg("GO", "w", Some(40)),
],
..AdoptionRawData::default()
};
let f = compute(&raw, now());
assert_eq!(f.package_systems, vec!["CARGO", "GO", "NPM"]);
assert_eq!(f.package_systems_count, 3);
}
}