use super::MetricCategory;
use super::MetricValue;
use super::metric_def::{METRIC_DEFINITIONS, MetricDef};
use crate::facts::CrateFacts;
#[cfg(test)]
use crate::facts::{CratesData, ProviderResult};
#[cfg(test)]
use crate::facts::advisories::AdvisoryData;
#[cfg(test)]
use crate::facts::codebase::CodebaseData;
#[cfg(test)]
use crate::facts::coverage::CoverageData;
#[cfg(test)]
use crate::facts::crates::{CrateOverallData, CrateVersionData};
#[cfg(test)]
use crate::facts::docs::DocsData;
#[cfg(test)]
use crate::facts::hosting::HostingData;
#[derive(Debug, Clone)]
pub struct Metric {
pub def: &'static MetricDef,
pub value: Option<MetricValue>,
}
impl Metric {
#[must_use]
pub const fn new(def: &'static MetricDef) -> Self {
Self { def, value: None }
}
#[must_use]
pub const fn with_value(def: &'static MetricDef, value: MetricValue) -> Self {
Self { def, value: Some(value) }
}
#[must_use]
pub const fn name(&self) -> &'static str {
self.def.name
}
#[must_use]
pub const fn description(&self) -> &'static str {
self.def.description
}
#[must_use]
pub const fn category(&self) -> MetricCategory {
self.def.category
}
}
pub fn flatten(facts: &CrateFacts) -> impl Iterator<Item = Metric> + '_ {
METRIC_DEFINITIONS
.iter()
.map(|def| (def.extractor)(facts).map_or_else(|| Metric::new(def), |value| Metric::with_value(def, value)))
}
pub fn default_metrics() -> impl Iterator<Item = Metric> {
METRIC_DEFINITIONS
.iter()
.map(|def| (def.default_value)().map_or_else(|| Metric::new(def), |value| Metric::with_value(def, value)))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::facts::CrateSpec;
use crate::facts::advisories::AdvisoryCounts;
use crate::facts::docs::DocsMetrics;
use crate::facts::hosting::{AgeStats, TimeWindowStats};
use chrono::Utc;
use semver::Version;
use std::collections::BTreeMap;
use std::sync::Arc;
#[expect(clippy::too_many_lines, reason = "Test helper function with comprehensive test data")]
fn create_test_crate_facts() -> CrateFacts {
use chrono::TimeZone;
let now = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
CrateFacts {
crate_spec: CrateSpec::from_arcs(Arc::from("test-crate"), Arc::new(Version::parse("1.0.0").unwrap())),
crates_data: ProviderResult::Found(CratesData::new(
CrateVersionData {
description: "Test crate".into(),
homepage: None,
documentation: None,
license: "MIT".into(),
rust_version: "1.70.0".into(),
edition: None,
features: BTreeMap::new(),
created_at: now,
updated_at: now,
yanked: false,
downloads: 1000,
monthly_downloads: vec![],
},
CrateOverallData {
created_at: now,
updated_at: now,
repository: None,
categories: vec![],
keywords: vec!["test".into()],
owners: vec![],
monthly_downloads: vec![],
downloads: 5000,
dependents: 10,
versions_last_90_days: 0,
versions_last_180_days: 0,
versions_last_365_days: 0,
},
)),
hosting_data: ProviderResult::Found(HostingData {
stars: 100,
forks: 20,
subscribers: 5,
open_issues: 5,
open_prs: 2,
issues_opened: TimeWindowStats::default(),
issues_closed: TimeWindowStats::default(),
prs_opened: TimeWindowStats::default(),
prs_merged: TimeWindowStats::default(),
prs_closed: TimeWindowStats::default(),
open_issue_age: AgeStats {
avg: 10,
p50: 8,
p75: 15,
p90: 20,
p95: 25,
},
open_pr_age: AgeStats {
avg: 5,
p50: 4,
p75: 7,
p90: 10,
p95: 12,
},
closed_issue_age: AgeStats::default(),
closed_issue_age_last_90_days: AgeStats::default(),
closed_issue_age_last_180_days: AgeStats::default(),
closed_issue_age_last_365_days: AgeStats::default(),
merged_pr_age: AgeStats::default(),
merged_pr_age_last_90_days: AgeStats::default(),
merged_pr_age_last_180_days: AgeStats::default(),
merged_pr_age_last_365_days: AgeStats::default(),
}),
advisory_data: ProviderResult::Found(AdvisoryData {
per_version: AdvisoryCounts::default(),
total: AdvisoryCounts {
low_vulnerability_count: 1,
medium_vulnerability_count: 0,
high_vulnerability_count: 0,
critical_vulnerability_count: 0,
notice_warning_count: 0,
unmaintained_warning_count: 0,
unsound_warning_count: 0,
yanked_warning_count: 0,
},
}),
codebase_data: ProviderResult::Found(CodebaseData {
source_files_analyzed: 10,
source_files_with_errors: 0,
production_lines: 1000,
test_lines: 500,
comment_lines: 200,
unsafe_count: 2,
example_count: 3,
transitive_dependencies: 25,
workflows_detected: true,
miri_detected: false,
clippy_detected: true,
contributors: 5,
commits_last_90_days: 50,
commits_last_180_days: 100,
commits_last_365_days: 200,
commit_count: 1000,
first_commit_at: now,
last_commit_at: now,
}),
coverage_data: ProviderResult::Found(CoverageData {
code_coverage_percentage: 85.5,
}),
docs_data: ProviderResult::Found(DocsData {
metrics: DocsMetrics {
doc_coverage_percentage: 90.0,
public_api_elements: 100,
undocumented_elements: 10,
examples_in_docs: 25,
has_crate_level_docs: true,
broken_doc_links: 1,
},
}),
}
}
#[test]
fn test_flatten_returns_metrics() {
let facts = create_test_crate_facts();
let metrics: Vec<_> = flatten(&facts).collect();
assert!(metrics.len() > 50, "Expected many metrics, got {}", metrics.len());
for metric in &metrics {
assert!(metric.value.is_some(), "Metric '{}' should have a value", metric.name());
}
}
#[test]
fn test_flatten_includes_version_data() {
let facts = create_test_crate_facts();
let metrics: Vec<_> = flatten(&facts).collect();
assert!(metrics.iter().any(|m| m.name() == "crate.version"), "Should have version metric");
assert!(
metrics.iter().any(|m| m.name() == "usage.version_downloads"),
"Should have version downloads metric"
);
assert!(metrics.iter().any(|m| m.name() == "crate.license"), "Should have license metric");
}
#[test]
fn test_flatten_includes_overall_data() {
let facts = create_test_crate_facts();
let metrics: Vec<_> = flatten(&facts).collect();
assert!(metrics.iter().any(|m| m.name() == "crate.name"), "Should have crate name metric");
assert!(
metrics.iter().any(|m| m.name() == "usage.total_downloads"),
"Should have total downloads metric"
);
assert!(
metrics.iter().any(|m| m.name() == "usage.dependent_crates"),
"Should have dependent crate count metric"
);
}
#[test]
fn test_flatten_includes_hosting_data() {
let facts = create_test_crate_facts();
let metrics: Vec<_> = flatten(&facts).collect();
assert!(
metrics.iter().any(|m| m.name() == "community.repo_stars"),
"Should have stars metric"
);
assert!(
metrics.iter().any(|m| m.name() == "activity.open_issues"),
"Should have open issues metric"
);
assert!(
metrics.iter().any(|m| m.name() == "activity.commits_last_90_days"),
"Should have commits metric"
);
}
#[test]
fn test_flatten_includes_advisory_data() {
let facts = create_test_crate_facts();
let metrics: Vec<_> = flatten(&facts).collect();
assert!(
metrics
.iter()
.any(|m| m.name() == "advisories.version_low_severity_vulnerabilities"),
"Should have version low severity vulnerabilities metric"
);
assert!(
metrics
.iter()
.any(|m| m.name() == "advisories.total_critical_severity_vulnerabilities"),
"Should have total critical severity vulnerabilities metric"
);
assert!(
metrics.iter().any(|m| m.name() == "advisories.version_notice_warnings"),
"Should have version notice warnings metric"
);
}
#[test]
fn test_flatten_includes_codebase_data() {
let facts = create_test_crate_facts();
let metrics: Vec<_> = flatten(&facts).collect();
assert!(
metrics.iter().any(|m| m.name() == "code.code_lines"),
"Should have production lines metric"
);
assert!(
metrics.iter().any(|m| m.name() == "trust.unsafe_blocks"),
"Should have unsafe count metric"
);
assert!(
metrics.iter().any(|m| m.name() == "trust.ci_workflows"),
"Should have CI workflows metric"
);
}
#[test]
fn test_flatten_includes_coverage_data() {
let facts = create_test_crate_facts();
let metrics: Vec<_> = flatten(&facts).collect();
assert!(
metrics.iter().any(|m| m.name() == "trust.code_coverage_percentage"),
"Should have code coverage metric"
);
}
#[test]
fn test_flatten_includes_docs_data() {
let facts = create_test_crate_facts();
let metrics: Vec<_> = flatten(&facts).collect();
assert!(
metrics.iter().any(|m| m.name() == "docs.public_api_coverage_percentage"),
"Should have doc coverage metric"
);
assert!(
metrics.iter().any(|m| m.name() == "docs.public_api_elements"),
"Should have public API elements metric"
);
assert!(
metrics.iter().any(|m| m.name() == "docs.broken_links"),
"Should have broken links metric"
);
}
#[test]
fn test_metric_categories_are_assigned() {
let facts = create_test_crate_facts();
let metrics: Vec<_> = flatten(&facts).collect();
let categories: crate::HashSet<_> = metrics.iter().map(Metric::category).collect();
assert!(categories.len() > 5, "Should use multiple metric categories, found {categories:?}");
}
#[test]
#[cfg_attr(coverage_nightly, coverage(off))]
fn test_all_metrics_have_descriptions() {
let facts = create_test_crate_facts();
let metrics: Vec<_> = flatten(&facts).collect();
for metric in &metrics {
assert!(
!metric.description().is_empty(),
"Metric '{}' should have a description",
metric.name()
);
assert!(
metric.description().len() > 10,
"Metric '{}' description should be meaningful",
metric.name()
);
}
}
}