use std::{
collections::{BTreeMap, HashMap},
num::NonZeroUsize,
};
use petgraph::graphmap::DiGraphMap;
use tracing::instrument;
use uuid::Uuid;
use crate::{
domain::{
hrid::KindString,
requirement::{LoadError, Parent},
requirement_data::RequirementData,
requirement_view::RequirementView,
Hrid,
},
Requirement,
};
#[derive(Debug, Clone, PartialEq, Eq)]
struct EdgeData {
parent_hrid: Hrid,
fingerprint: String,
}
#[derive(Debug)]
pub struct Tree {
requirements: HashMap<Uuid, RequirementData>,
hrids: HashMap<Uuid, Hrid>,
hrid_to_uuid: BTreeMap<Hrid, Uuid>,
graph: DiGraphMap<Uuid, EdgeData>,
}
#[derive(Debug)]
pub struct LinkOutcome {
pub child_uuid: Uuid,
pub child_hrid: Hrid,
pub parent_uuid: Uuid,
pub parent_hrid: Hrid,
pub already_linked: bool,
}
impl Default for Tree {
fn default() -> Self {
Self {
requirements: HashMap::new(),
hrids: HashMap::new(),
hrid_to_uuid: BTreeMap::new(),
graph: DiGraphMap::new(),
}
}
}
impl Tree {
#[must_use]
pub fn with_capacity(capacity: usize) -> Self {
Self {
requirements: HashMap::with_capacity(capacity),
hrids: HashMap::with_capacity(capacity),
hrid_to_uuid: BTreeMap::new(),
graph: DiGraphMap::with_capacity(capacity, capacity * 2),
}
}
pub fn insert(&mut self, requirement: Requirement) {
let uuid = requirement.metadata.uuid;
assert!(
!self.requirements.contains_key(&uuid),
"Duplicate requirement UUID: {uuid}"
);
let hrid = requirement.metadata.hrid.clone();
self.graph.add_node(uuid);
for (parent_uuid, parent_info) in &requirement.metadata.parents {
let edge_data = EdgeData {
parent_hrid: parent_info.hrid.clone(),
fingerprint: parent_info.fingerprint.clone(),
};
self.graph.add_edge(uuid, *parent_uuid, edge_data);
}
self.hrids.insert(uuid, hrid.clone());
self.hrid_to_uuid.insert(hrid, uuid);
let data = RequirementData::from(requirement);
self.requirements.insert(uuid, data);
}
#[must_use]
pub fn hrid(&self, uuid: Uuid) -> Option<&Hrid> {
self.hrids.get(&uuid)
}
#[must_use]
pub fn get_requirement(&self, uuid: Uuid) -> Option<Requirement> {
use std::collections::HashMap;
let data = self.requirements.get(&uuid)?;
let hrid = self.hrids.get(&uuid)?;
let parents: HashMap<Uuid, Parent> = self
.graph
.edges(uuid)
.map(|(_, parent_uuid, edge_data)| {
(
parent_uuid,
Parent {
hrid: edge_data.parent_hrid.clone(),
fingerprint: edge_data.fingerprint.clone(),
},
)
})
.collect();
Some(Requirement {
content: crate::domain::requirement::Content {
title: data.title.clone(),
body: data.body.clone(),
tags: data.tags.clone(),
},
metadata: crate::domain::requirement::Metadata {
uuid,
hrid: hrid.clone(),
created: data.created,
parents,
},
})
}
#[must_use]
pub fn requirement(&self, uuid: Uuid) -> Option<RequirementView<'_>> {
let data = self.requirements.get(&uuid)?;
let hrid = self.hrids.get(&uuid)?;
let parents: Vec<(Uuid, Parent)> = self
.graph
.edges(uuid)
.map(|(_, parent_uuid, edge_data)| {
(
parent_uuid,
Parent {
hrid: edge_data.parent_hrid.clone(),
fingerprint: edge_data.fingerprint.clone(),
},
)
})
.collect();
let uuid_ref = self.requirements.get_key_value(&uuid)?.0;
Some(RequirementView {
uuid: uuid_ref,
hrid,
created: &data.created,
title: &data.title,
body: &data.body,
tags: &data.tags,
parents,
})
}
#[must_use]
pub fn next_index(&self, kind: &KindString) -> NonZeroUsize {
let start =
crate::domain::Hrid::new_with_namespace(Vec::new(), kind.clone(), NonZeroUsize::MIN);
let end =
crate::domain::Hrid::new_with_namespace(Vec::new(), kind.clone(), NonZeroUsize::MAX);
self.hrid_to_uuid
.range(start..=end)
.next_back()
.map_or(NonZeroUsize::MIN, |(hrid, _)| {
hrid.id().checked_add(1).expect("requirement ID overflow!")
})
}
pub fn iter(&self) -> impl Iterator<Item = RequirementView<'_>> + '_ {
self.requirements.iter().filter_map(move |(uuid, data)| {
let hrid = self.hrids.get(uuid)?;
let parents: Vec<(Uuid, Parent)> = self
.graph
.edges(*uuid)
.map(|(_, parent_uuid, edge_data)| {
(
parent_uuid,
Parent {
hrid: edge_data.parent_hrid.clone(),
fingerprint: edge_data.fingerprint.clone(),
},
)
})
.collect();
Some(RequirementView {
uuid,
hrid,
created: &data.created,
title: &data.title,
body: &data.body,
tags: &data.tags,
parents,
})
})
}
#[must_use]
pub fn find_by_hrid(&self, hrid: &Hrid) -> Option<RequirementView<'_>> {
let uuid = self.hrid_to_uuid.get(hrid)?;
self.requirement(*uuid)
}
pub fn link_requirement(
&mut self,
child: &Hrid,
parent: &Hrid,
) -> Result<LinkOutcome, LoadError> {
let (child_uuid, child_hrid) = {
let view = self.find_by_hrid(child).ok_or(LoadError::NotFound)?;
(*view.uuid, view.hrid.clone())
};
let (parent_uuid, parent_hrid, parent_fingerprint) = {
let view = self.find_by_hrid(parent).ok_or(LoadError::NotFound)?;
(*view.uuid, view.hrid.clone(), view.fingerprint())
};
let already_linked = self
.parents(child_uuid)
.into_iter()
.any(|(uuid, _)| uuid == parent_uuid);
self.upsert_parent_link(child_uuid, parent_uuid, parent_fingerprint);
Ok(LinkOutcome {
child_uuid,
child_hrid,
parent_uuid,
parent_hrid,
already_linked,
})
}
#[must_use]
pub fn children(&self, uuid: Uuid) -> Vec<Uuid> {
if !self.graph.contains_node(uuid) {
return Vec::new();
}
self.graph
.neighbors_directed(uuid, petgraph::Direction::Incoming)
.collect()
}
#[must_use]
pub fn parents(&self, uuid: Uuid) -> Vec<(Uuid, String)> {
if !self.graph.contains_node(uuid) {
return Vec::new();
}
self.graph
.edges(uuid)
.map(|(_, parent_uuid, edge_data)| (parent_uuid, edge_data.fingerprint.clone()))
.collect()
}
pub fn upsert_parent_link(
&mut self,
child_uuid: Uuid,
parent_uuid: Uuid,
fingerprint: String,
) -> bool {
assert!(
self.requirements.contains_key(&child_uuid),
"Child requirement {child_uuid} not found in tree"
);
assert!(
self.requirements.contains_key(&parent_uuid),
"Parent requirement {parent_uuid} not found in tree"
);
self.graph.add_node(child_uuid);
self.graph.add_node(parent_uuid);
let parent_hrid = self
.hrids
.get(&parent_uuid)
.unwrap_or_else(|| panic!("Parent HRID for {parent_uuid} not found"));
let edge = EdgeData {
parent_hrid: parent_hrid.clone(),
fingerprint,
};
self.graph.add_edge(child_uuid, parent_uuid, edge).is_some()
}
#[instrument(skip(self))]
pub fn update_hrids(&mut self) -> impl Iterator<Item = Uuid> + '_ {
use std::collections::HashSet;
let mut updated_uuids = HashSet::new();
let mut edges_to_update = Vec::new();
for child_uuid in self.graph.nodes() {
for (_, parent_uuid, edge_data) in self.graph.edges(child_uuid) {
let current_parent_hrid = self
.hrids
.get(&parent_uuid)
.unwrap_or_else(|| panic!("Parent requirement {parent_uuid} not found!"));
if &edge_data.parent_hrid != current_parent_hrid {
edges_to_update.push((child_uuid, parent_uuid));
updated_uuids.insert(child_uuid);
}
}
}
for (child_uuid, parent_uuid) in edges_to_update {
let current_parent_hrid = self.hrids.get(&parent_uuid).unwrap();
if let Some(edge_data) = self.graph.edge_weight_mut(child_uuid, parent_uuid) {
edge_data.parent_hrid = current_parent_hrid.clone();
}
}
updated_uuids.into_iter()
}
#[must_use]
pub fn suspect_links(&self) -> Vec<SuspectLink> {
use crate::domain::requirement::ContentRef;
let mut suspect = Vec::new();
for child_uuid in self.graph.nodes() {
let child_hrid = self.hrids.get(&child_uuid).unwrap();
for (_, parent_uuid, edge_data) in self.graph.edges(child_uuid) {
let Some(parent_data) = self.requirements.get(&parent_uuid) else {
continue;
};
let current_fingerprint = ContentRef {
title: &parent_data.title,
body: &parent_data.body,
tags: &parent_data.tags,
}
.fingerprint();
if edge_data.fingerprint != current_fingerprint {
suspect.push(SuspectLink {
child_uuid,
child_hrid: child_hrid.clone(),
parent_uuid,
parent_hrid: edge_data.parent_hrid.clone(),
stored_fingerprint: edge_data.fingerprint.clone(),
current_fingerprint,
});
}
}
}
suspect
}
pub fn accept_suspect_link(&mut self, child_uuid: Uuid, parent_uuid: Uuid) -> bool {
let parent = self
.requirement(parent_uuid)
.unwrap_or_else(|| panic!("Parent requirement {parent_uuid} not found!"));
let current_fingerprint = parent.fingerprint();
assert!(
self.graph.contains_node(child_uuid),
"Child requirement {child_uuid} not found!"
);
assert!(
self.graph.contains_node(parent_uuid),
"Parent requirement {parent_uuid} not found!"
);
if let Some(edge_data) = self.graph.edge_weight_mut(child_uuid, parent_uuid) {
if edge_data.fingerprint == current_fingerprint {
return false; }
edge_data.fingerprint.clone_from(¤t_fingerprint);
true
} else {
panic!("Parent link {parent_uuid} not found in child {child_uuid}");
}
}
pub fn accept_all_suspect_links(&mut self) -> Vec<(Uuid, Uuid)> {
let suspect = self.suspect_links();
let mut updated = Vec::new();
for link in suspect {
if self.accept_suspect_link(link.child_uuid, link.parent_uuid) {
updated.push((link.child_uuid, link.parent_uuid));
}
}
updated
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SuspectLink {
pub child_uuid: Uuid,
pub child_hrid: Hrid,
pub parent_uuid: Uuid,
pub parent_hrid: Hrid,
pub stored_fingerprint: String,
pub current_fingerprint: String,
}