use std::{
collections::{BTreeMap, BTreeSet},
error::Error,
fmt,
};
use super::{ConclusionId, Explanation, FactId};
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct OwnerProjectionGraph {
owner: String,
dependencies: BTreeMap<ConclusionId, BTreeSet<FactId>>,
}
impl OwnerProjectionGraph {
pub fn new(
owner: impl Into<String>,
dependencies: impl IntoIterator<Item = (ConclusionId, BTreeSet<FactId>)>,
) -> Result<Self, ClosureError> {
let owner = owner.into();
if owner.trim().is_empty() {
return Err(ClosureError::EmptyOwner);
}
let mut canonical = BTreeMap::new();
for (conclusion, facts) in dependencies {
if facts.is_empty() {
return Err(ClosureError::ConclusionWithoutFacts(conclusion));
}
if canonical.insert(conclusion.clone(), facts).is_some() {
return Err(ClosureError::DuplicateConclusion(conclusion));
}
}
if canonical.is_empty() {
return Err(ClosureError::EmptyOwnerGraph(owner));
}
Ok(Self {
owner,
dependencies: canonical,
})
}
#[must_use]
pub fn owner(&self) -> &str {
&self.owner
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct FederatedClosure {
owners: BTreeMap<ConclusionId, String>,
dependencies: BTreeMap<ConclusionId, BTreeSet<FactId>>,
consumers: BTreeMap<FactId, BTreeSet<ConclusionId>>,
}
impl FederatedClosure {
pub fn seal(
facts: impl IntoIterator<Item = FactId>,
graphs: impl IntoIterator<Item = OwnerProjectionGraph>,
) -> Result<Self, ClosureError> {
let facts = facts.into_iter().collect::<BTreeSet<_>>();
let mut owners = BTreeMap::new();
let mut dependencies = BTreeMap::new();
let mut consumers = facts
.iter()
.cloned()
.map(|fact| (fact, BTreeSet::new()))
.collect::<BTreeMap<_, _>>();
for graph in graphs {
for (conclusion, required) in graph.dependencies {
if let Some(first) = owners.insert(conclusion.clone(), graph.owner.clone()) {
return Err(ClosureError::DuplicateOwner {
conclusion,
first,
second: graph.owner,
});
}
for fact in &required {
let Some(fact_consumers) = consumers.get_mut(fact) else {
return Err(ClosureError::UnknownFact {
conclusion,
fact: fact.clone(),
});
};
fact_consumers.insert(conclusion.clone());
}
dependencies.insert(conclusion, required);
}
}
if dependencies.is_empty() {
return Err(ClosureError::EmptyClosure);
}
Ok(Self {
owners,
dependencies,
consumers,
})
}
#[must_use]
pub fn affected(&self, changed: impl IntoIterator<Item = FactId>) -> Vec<ConclusionId> {
changed
.into_iter()
.filter_map(|fact| self.consumers.get(&fact))
.flatten()
.cloned()
.collect::<BTreeSet<_>>()
.into_iter()
.collect()
}
pub fn explain(
&self,
conclusion: &ConclusionId,
fact: &FactId,
) -> Result<Explanation, ClosureError> {
let Some(required) = self.dependencies.get(conclusion) else {
return Err(ClosureError::UnknownConclusion(conclusion.clone()));
};
if !required.contains(fact) {
return Err(ClosureError::Unrelated {
conclusion: conclusion.clone(),
fact: fact.clone(),
});
}
Ok(Explanation {
conclusion: conclusion.clone(),
fact: fact.clone(),
path: vec![
format!("owner/{}", self.owners[conclusion]),
format!("conclusion/{conclusion}"),
format!("fact/{fact}"),
],
})
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ClosureError {
EmptyOwner,
EmptyOwnerGraph(String),
ConclusionWithoutFacts(ConclusionId),
DuplicateConclusion(ConclusionId),
DuplicateOwner {
conclusion: ConclusionId,
first: String,
second: String,
},
UnknownFact {
conclusion: ConclusionId,
fact: FactId,
},
EmptyClosure,
UnknownConclusion(ConclusionId),
Unrelated {
conclusion: ConclusionId,
fact: FactId,
},
}
impl fmt::Display for ClosureError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "{self:?}")
}
}
impl Error for ClosureError {}