nd / nd-core/src/resume.rs

raw file 2761 bytes
//! 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");
    }
}