use std::collections::{HashMap, HashSet};
use scraps_libs::model::{key::ScrapKey, scrap::Scrap, tag::Tag};
#[derive(PartialEq, Debug)]
pub struct BacklinksMap<'a> {
scrap_backlinks: HashMap<ScrapKey, Vec<&'a Scrap>>,
tag_backlinks: HashMap<Tag, Vec<&'a Scrap>>,
}
impl<'a> BacklinksMap<'a> {
pub fn new(scraps: &'a [Scrap]) -> BacklinksMap<'a> {
BacklinksMap {
scrap_backlinks: Self::gen_scrap_backlinks(scraps),
tag_backlinks: Self::gen_tag_backlinks(scraps),
}
}
pub fn get(&self, key: &ScrapKey) -> &[&'a Scrap] {
self.scrap_backlinks.get(key).map_or(&[], Vec::as_slice)
}
pub fn get_tag(&self, tag: &Tag) -> &[&'a Scrap] {
self.tag_backlinks.get(tag).map_or(&[], Vec::as_slice)
}
fn gen_scrap_backlinks(scraps: &'a [Scrap]) -> HashMap<ScrapKey, Vec<&'a Scrap>> {
scraps.iter().fold(HashMap::new(), |mut acc, scrap| {
for key in scrap.links() {
acc.entry(key.clone()).or_default().push(scrap);
}
acc
})
}
fn gen_tag_backlinks(scraps: &'a [Scrap]) -> HashMap<Tag, Vec<&'a Scrap>> {
scraps.iter().fold(HashMap::new(), |mut acc, scrap| {
let mut keys: HashSet<Tag> = HashSet::new();
for tag in scrap.tags() {
keys.insert(tag.clone());
for ancestor in tag.ancestors() {
keys.insert(ancestor);
}
}
for tag in keys {
acc.entry(tag).or_default().push(scrap);
}
acc
})
}
}
#[cfg(test)]
mod tests {
use scraps_libs::model::stem::Stem;
use scraps_libs::model::tag::Tag;
use super::*;
#[test]
fn it_get_returns_scrap_link_backlinks_only() {
let scrap1 = Scrap::new("scrap1", &None, "");
let scrap2 = Scrap::new("scrap2", &None, "[[scrap1]]");
let scraps = vec![scrap1.clone(), scrap2.clone()];
let backlinks_map = BacklinksMap::new(&scraps);
assert_eq!(
backlinks_map.get(&Stem::from("scrap1").into()),
vec![&scrap2]
);
}
#[test]
fn it_get_with_context() {
let scrap1 = Scrap::new("scrap1", &Some("Context".into()), "");
let scrap2 = Scrap::new("scrap2", &Some("Context".into()), "[[Context/scrap1]]");
let scrap3 = Scrap::new("scrap3", &None, "[[Context/scrap1]][[Context/scrap2]]");
let scraps = vec![scrap1.clone(), scrap2.clone(), scrap3.clone()];
let backlinks_map = BacklinksMap::new(&scraps);
assert_eq!(
backlinks_map.get(&ScrapKey::with_ctx(&"scrap1".into(), &"Context".into())),
vec![&scrap2, &scrap3]
);
assert_eq!(
backlinks_map.get(&ScrapKey::with_ctx(&"scrap2".into(), &"Context".into())),
vec![&scrap3]
);
assert!(backlinks_map.get(&Stem::from("scrap3").into()).is_empty());
}
#[test]
fn it_get_tag_collects_explicit_tagged_scraps() {
let scrap1 = Scrap::new("a", &None, "#[[ai]]");
let scrap2 = Scrap::new("b", &None, "#[[ai]]");
let scrap3 = Scrap::new("c", &None, "no tags");
let scraps = vec![scrap1.clone(), scrap2.clone(), scrap3];
let backlinks_map = BacklinksMap::new(&scraps);
let mut got = backlinks_map.get_tag(&Tag::from("ai")).to_vec();
got.sort_by_key(|s| s.stem().to_string());
assert_eq!(got, vec![&scrap1, &scrap2]);
}
#[test]
fn it_get_tag_includes_descendant_tagged_scraps_via_auto_aggregation() {
let scrap = Scrap::new("paper", &None, "#[[ai/ml/transformer]]");
let scraps = vec![scrap.clone()];
let backlinks_map = BacklinksMap::new(&scraps);
assert_eq!(
backlinks_map.get_tag(&Tag::from("ai/ml/transformer")),
vec![&scrap]
);
assert_eq!(backlinks_map.get_tag(&Tag::from("ai/ml")), vec![&scrap]);
assert_eq!(backlinks_map.get_tag(&Tag::from("ai")), vec![&scrap]);
}
#[test]
fn it_get_tag_unrelated_returns_empty() {
let scraps = [Scrap::new("a", &None, "#[[ai]]")];
let backlinks_map = BacklinksMap::new(&scraps);
assert!(backlinks_map.get_tag(&Tag::from("unrelated")).is_empty());
}
#[test]
fn it_get_tag_dedupes_when_scrap_tagged_with_overlapping_paths() {
let scrap = Scrap::new("multi", &None, "#[[ai]] and #[[ai/ml]]");
let scraps = vec![scrap.clone()];
let backlinks_map = BacklinksMap::new(&scraps);
assert_eq!(backlinks_map.get_tag(&Tag::from("ai")), vec![&scrap]);
}
}