use crate::finding::{at, Finding, Severity};
use crate::instance::Outcome;
use crate::lifecycle_state::{Standing, StateFacet, Stood};
use crate::scope::{EdgeCheck, EdgeView};
use crate::shape::Shape;
use headwater_graph::declarations::Relation;
use headwater_graph::{Declarations, Reciprocal, Target};
pub const RULE: &str = "relation.reciprocity.missing";
const NO_HALF: &str = "the pair carries no declared half";
const NO_RELATION: &str = "no declaration holds the relation this pair declares";
pub struct Reciprocity<'a> {
required: Vec<&'a Relation>,
facet: StateFacet,
}
impl<'a> Reciprocity<'a> {
pub fn over(declarations: &'a Declarations, shape: &Shape) -> Self {
Reciprocity {
required: declarations
.relations
.iter()
.filter(|relation| relation.reciprocal == Reciprocal::Required)
.collect(),
facet: StateFacet::of(shape),
}
}
}
impl EdgeCheck for Reciprocity<'_> {
const RULE: &'static str = self::RULE;
const VERSION: u32 = 2;
fn instantiates(&self, relation: &str) -> bool {
self.required.iter().any(|known| known.name == relation)
}
fn evaluate(&self, view: &EdgeView<'_>) -> Outcome {
let (written, missing) = match (view.declared_half(), view.inverse_half()) {
(Some(_), Some(_)) => return Outcome::Passed,
(Some(edge), None) => (edge, Missing::TheInverseHalf),
(None, Some(edge)) => (edge, Missing::TheDeclaredHalf),
(None, None) => return Outcome::Skipped(NO_HALF.to_string()),
};
if let Some(facets) = view.declarer_facets() {
if let Stood::At(state) = self.facet.stood(facets) {
if self.facet.standing(state) == Standing::Initial {
return Outcome::Passed;
}
}
}
let Some(relation) = self
.required
.iter()
.find(|known| known.name == written.declared)
else {
return Outcome::Skipped(NO_RELATION.to_string());
};
let owed_by = match &written.target {
Target::Document { path, .. } => path.clone(),
_ => written.raw_target.clone(),
};
let (name, target) = match missing {
Missing::TheInverseHalf => (
relation
.inverse
.clone()
.unwrap_or_else(|| relation.name.clone()),
written.source.id.clone(),
),
Missing::TheDeclaredHalf => (relation.name.clone(), written.source.id.clone()),
};
let (line, column) = at(Some(written.span));
Outcome::failed_with(Finding {
rule: self::RULE,
severity: Severity::Error,
obligation: None,
path: written.source.path.clone(),
line,
column,
message: format!(
"`{}` declares `{}: {}`, and `{}` requires both ends, so {owed_by} owes `{name}`",
written.source.id, written.name, written.raw_target, relation.name,
),
remediation: format!("add `{name}: {target}` under `relations:` in {owed_by}"),
patch: Some(crate::Patch::Half {
path: owed_by.clone(),
relation: name.clone(),
id: target.clone(),
attributes: Vec::new(),
}),
})
}
}
enum Missing {
TheInverseHalf,
TheDeclaredHalf,
}