1use std::collections::HashMap;
2use std::path::Path;
3
4use anyhow::Context;
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Serialize, Deserialize, Default)]
22pub struct PopularityMap {
23 pub version: u32,
24 pub packages: HashMap<String, u64>,
26}
27
28impl PopularityMap {
29 pub fn new() -> Self {
30 Self {
31 version: 1,
32 packages: HashMap::new(),
33 }
34 }
35
36 pub fn load(path: &Path) -> anyhow::Result<Self> {
37 let s = std::fs::read_to_string(path)
38 .with_context(|| format!("read popularity map '{}'", path.display()))?;
39 serde_json::from_str(&s)
40 .with_context(|| format!("parse popularity map '{}'", path.display()))
41 }
42
43 pub fn save(&self, path: &Path) -> anyhow::Result<()> {
44 let json = serde_json::to_string_pretty(self)?;
45 std::fs::write(path, &json)?;
46 Ok(())
47 }
48
49 pub fn score(&self, package_name: &str) -> u64 {
51 self.packages.get(package_name).copied().unwrap_or(0)
52 }
53
54 pub fn record_tool(&mut self, package_names: &[String]) {
56 for name in package_names {
57 *self.packages.entry(name.clone()).or_insert(0) += 1;
58 }
59 }
60}
61
62pub fn compute_from_spec_dir(specs_root: &Path) -> anyhow::Result<PopularityMap> {
68 let mut map = PopularityMap::new();
69
70 for entry in walkdir(specs_root)? {
71 let path = entry?;
72 if path.extension().and_then(|e| e.to_str()) != Some("yaml") {
73 continue;
74 }
75
76 let s = std::fs::read_to_string(&path)
77 .with_context(|| format!("read spec '{}'", path.display()))?;
78
79 let raw: serde_yaml::Value = serde_yaml::from_str(&s)
81 .with_context(|| format!("parse spec '{}'", path.display()))?;
82
83 let names = extract_package_names(&raw);
84 map.record_tool(&names);
85 }
86
87 Ok(map)
88}
89
90fn extract_package_names(yaml: &serde_yaml::Value) -> Vec<String> {
92 let Some(pkgs) = yaml.get("packages").and_then(|v| v.as_sequence()) else {
93 return vec![];
94 };
95
96 pkgs.iter()
97 .filter_map(|v| v.as_str())
98 .map(|s| {
99 s.split_whitespace().next().unwrap_or(s).to_string()
101 })
102 .collect()
103}
104
105fn walkdir(root: &Path) -> anyhow::Result<impl Iterator<Item = anyhow::Result<std::path::PathBuf>>> {
106 let entries = walkdir_inner(root);
107 Ok(entries.into_iter())
108}
109
110fn walkdir_inner(root: &Path) -> Vec<anyhow::Result<std::path::PathBuf>> {
111 let Ok(read) = std::fs::read_dir(root) else {
112 return vec![];
113 };
114 let mut results = vec![];
115 for entry in read {
116 let Ok(e) = entry else { continue };
117 let path = e.path();
118 if path.is_dir() {
119 results.extend(walkdir_inner(&path));
120 } else {
121 results.push(Ok(path));
122 }
123 }
124 results
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130
131 #[test]
132 fn record_tool_increments_counts() {
133 let mut map = PopularityMap::new();
134 map.record_tool(&["openssl".into(), "zlib".into()]);
135 map.record_tool(&["openssl".into(), "samtools".into()]);
136 assert_eq!(map.score("openssl"), 2);
137 assert_eq!(map.score("zlib"), 1);
138 assert_eq!(map.score("samtools"), 1);
139 assert_eq!(map.score("unknown"), 0);
140 }
141
142 #[test]
143 fn extract_names_strips_version_constraints() {
144 let yaml: serde_yaml::Value = serde_yaml::from_str(
145 "packages:\n - samtools ==1.19.2\n - openssl\n - bwa >=0.7",
146 )
147 .unwrap();
148 let names = extract_package_names(&yaml);
149 assert_eq!(names, vec!["samtools", "openssl", "bwa"]);
150 }
151
152 #[test]
153 fn save_and_load_round_trips() {
154 let mut map = PopularityMap::new();
155 map.record_tool(&["openssl".into(), "zlib".into()]);
156
157 let dir = tempfile::tempdir().unwrap();
158 let path = dir.path().join("popularity.json");
159 map.save(&path).unwrap();
160
161 let loaded = PopularityMap::load(&path).unwrap();
162 assert_eq!(loaded.score("openssl"), 1);
163 assert_eq!(loaded.score("zlib"), 1);
164 }
165}