use super::{count_nodes, EdgeGroups};
use crate::datatypes::values::{DataFrame, Value};
use crate::graph::mutation::maintain;
use crate::graph::DirGraph;
use crate::okf::model::{BuildReport, ConceptDoc, Profile, SOURCE_LABEL};
use std::collections::{BTreeMap, BTreeSet};
pub(super) fn build_aux_nodes(
graph: &mut DirGraph,
docs: &[ConceptDoc],
report: &mut BuildReport,
) -> Result<(), String> {
let mut sources: BTreeSet<&str> = BTreeSet::new();
for d in docs {
for l in &d.links {
if l.is_external {
sources.insert(l.target.as_str());
}
}
}
count_nodes(report, SOURCE_LABEL, sources.len());
add_id_nodes(graph, SOURCE_LABEL, &sources)?;
Ok(())
}
pub(super) fn build_hubs(
graph: &mut DirGraph,
docs: &[ConceptDoc],
profile: &Profile,
report: &mut BuildReport,
) -> Result<EdgeGroups, String> {
let mut groups: EdgeGroups = BTreeMap::new();
for (key, spec) in &profile.hubs {
let mut spellings: BTreeMap<String, BTreeMap<&str, usize>> = BTreeMap::new();
let mut members: Vec<(&ConceptDoc, String)> = Vec::new();
for d in docs {
for raw in hub_values(d, key) {
let id = if spec.case_insensitive {
raw.to_lowercase()
} else {
raw.to_string()
};
*spellings
.entry(id.clone())
.or_default()
.entry(raw)
.or_default() += 1;
members.push((d, id));
}
}
if spellings.is_empty() {
continue;
}
count_nodes(report, &spec.label, spellings.len());
let rows: Vec<Vec<Value>> = spellings
.iter()
.map(|(id, counts)| {
vec![
Value::String(id.clone()),
Value::String(hub_title(id, counts)),
]
})
.collect();
let df = DataFrame::from_cypher_rows(vec!["id".to_string(), "title".to_string()], rows)?;
maintain::add_nodes(
graph,
df,
spec.label.clone(),
"id".to_string(),
Some("title".to_string()),
Some("update".to_string()),
)?;
for (d, id) in members {
groups
.entry((spec.edge.clone(), d.label.clone(), spec.label.clone()))
.or_default()
.push((d.concept_id.clone(), id, Vec::new()));
}
}
Ok(groups)
}
fn hub_title(id: &str, counts: &BTreeMap<&str, usize>) -> String {
counts
.iter()
.min_by(|a, b| b.1.cmp(a.1).then(a.0.cmp(b.0)))
.map(|(spelling, _)| (*spelling).to_string())
.unwrap_or_else(|| id.to_string())
}
fn add_id_nodes(graph: &mut DirGraph, label: &str, ids: &BTreeSet<&str>) -> Result<(), String> {
if ids.is_empty() {
return Ok(());
}
let rows: Vec<Vec<Value>> = ids
.iter()
.map(|s| vec![Value::String((*s).to_string())])
.collect();
let df = DataFrame::from_cypher_rows(vec!["id".to_string()], rows)?;
maintain::add_nodes(
graph,
df,
label.to_string(),
"id".to_string(),
None,
Some("update".to_string()),
)?;
Ok(())
}
fn hub_values<'a>(d: &'a ConceptDoc, key: &str) -> Vec<&'a str> {
let mut vals: Vec<&str> = d
.props
.iter()
.filter(|(k, _)| k == key)
.flat_map(|(_, v)| match v {
Value::List(items) => items
.iter()
.filter_map(|x| match x {
Value::String(s) => Some(s.as_str()),
_ => None,
})
.collect::<Vec<_>>(),
_ => Vec::new(),
})
.collect();
if key == "tags" {
for t in &d.inline_tags {
if !vals.contains(&t.as_str()) {
vals.push(t.as_str());
}
}
}
vals
}
#[cfg(test)]
#[path = "hubs_tests.rs"]
mod hubs_tests;