use crate::finding::{at, Finding, Severity};
use crate::instance::Outcome;
use crate::scope::{EdgeCheck, EdgeView};
use crate::shape::Shape;
use headwater_graph::declarations::Relation;
use headwater_graph::Declarations;
pub const RULE: &str = "relation.endpoint.not_permitted";
const NO_HALF: &str = "the pair carries no declared half";
const NO_RELATION: &str = "no declaration holds the relation this pair declares";
struct Endpoint<'a> {
key: &'static str,
id: &'a str,
path: &'a str,
kind: &'a str,
}
pub struct Endpoints<'a> {
declared: Vec<&'a Relation>,
shape: &'a Shape,
}
impl<'a> Endpoints<'a> {
pub fn over(declarations: &'a Declarations, shape: &'a Shape) -> Self {
Endpoints {
declared: declarations
.relations
.iter()
.filter(|relation| !relation.from.is_empty() && !relation.to.is_empty())
.collect(),
shape,
}
}
fn admits(&self, permitted: &[String], kind: &str) -> bool {
permitted
.iter()
.any(|allowed| self.shape.descends_from(kind, allowed))
}
}
impl EdgeCheck for Endpoints<'_> {
const RULE: &'static str = self::RULE;
const VERSION: u32 = 1;
fn instantiates(&self, relation: &str) -> bool {
self.declared.iter().any(|known| known.name == relation)
}
fn evaluate(&self, view: &EdgeView<'_>) -> Outcome {
let Some(half) = view.declared_half().or_else(|| view.inverse_half()) else {
return Outcome::Skipped(NO_HALF.to_string());
};
let Some(relation) = self
.declared
.iter()
.find(|known| known.name == half.declared)
else {
return Outcome::Skipped(NO_RELATION.to_string());
};
let Some((at_from, at_to)) = view.ends() else {
return Outcome::Skipped(NO_RELATION.to_string());
};
let source = Endpoint {
key: "from",
id: at_from.id,
path: at_from.path,
kind: at_from.kind,
};
let target = Endpoint {
key: "to",
id: at_to.id,
path: at_to.path,
kind: at_to.kind,
};
let (line, column) = at(Some(half.span));
let findings = [(&source, &relation.from), (&target, &relation.to)]
.into_iter()
.filter(|(end, permitted)| !self.admits(permitted, end.kind))
.map(|(end, permitted)| Finding {
rule: self::RULE,
severity: Severity::Error,
obligation: None,
path: half.source.path.clone(),
line,
column,
message: format!(
"`{}` declares `{}: [{}]`, and `{}` at that end has the kind `{}`",
relation.name,
end.key,
permitted.join(", "),
end.id,
end.kind
),
remediation: format!(
"declare this relation between kinds `{}` permits, or widen its `{}` in the \
taxonomy; the document at that end is {}",
relation.name, end.key, end.path
),
patch: None,
})
.collect();
Outcome::failed(findings)
}
}