//! Skills aggregation shared by `nd resume` (cli) and the /u/ owner
//! pages (hub): what does this maintainer build with, and with what.
use crate::models::Project;
use serde::Serialize;
use std::collections::BTreeMap;
#[derive(Debug, Default, Serialize)]
pub struct Skills {
pub languages: Vec<(String, usize)>,
pub tools: Vec<(String, usize)>,
pub topics: Vec<(String, usize)>,
pub models: Vec<String>,
}
fn bump(map: &mut BTreeMap<String, usize>, values: &[String]) {
for v in values {
let k = v.trim().to_lowercase();
if !k.is_empty() {
*map.entry(k).or_default() += 1;
}
}
}
fn ranked(m: BTreeMap<String, usize>) -> Vec<(String, usize)> {
let mut v: Vec<_> = m.into_iter().collect();
v.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
v
}
pub fn collect(projects: &[Project]) -> Skills {
let (mut langs, mut tools, mut topics) = (BTreeMap::new(), BTreeMap::new(), BTreeMap::new());
let mut models: Vec<String> = Vec::new();
for p in projects {
bump(&mut langs, &p.languages);
bump(&mut tools, &p.code_keywords);
bump(&mut topics, &p.keywords);
if !p.model.trim().is_empty() && !models.contains(&p.model) {
models.push(p.model.clone());
}
}
Skills {
languages: ranked(langs),
tools: ranked(tools),
topics: ranked(topics),
models,
}
}
/// "rust ×3, clap ×2, axum" — top n items
pub fn x2(items: &[(String, usize)], n: usize) -> String {
items
.iter()
.take(n)
.map(|(k, c)| {
if *c > 1 {
format!("{k} ×{c}")
} else {
k.clone()
}
})
.collect::<Vec<_>>()
.join(", ")
}
#[cfg(test)]
mod tests {
use super::*;
fn p(name: &str, langs: &[&str], tools: &[&str], topics: &[&str]) -> Project {
Project {
name: name.into(),
slug: name.into(),
version: "1.0.0".into(),
languages: langs.iter().map(|s| s.to_string()).collect(),
code_keywords: tools.iter().map(|s| s.to_string()).collect(),
keywords: topics.iter().map(|s| s.to_string()).collect(),
..Default::default()
}
}
#[test]
fn ranking() {
let projects = vec![
p("a", &["rust"], &["clap", "tokio"], &["cli"]),
p("b", &["rust", "js"], &["clap"], &["cli", "llm"]),
];
let s = collect(&projects);
assert_eq!(s.languages[0], ("rust".into(), 2));
assert_eq!(s.tools[0], ("clap".into(), 2));
assert_eq!(s.topics[0], ("cli".into(), 2));
assert_eq!(x2(&s.languages, 10), "rust ×2, js");
}
}