use crate::finding::{at, Finding, Severity};
use crate::instance::Outcome;
use crate::lifecycle_state::{Standing, StateFacet, Stood};
use crate::scope::{EdgeCheck, EdgeUnit, EdgeView};
use crate::shape::Shape;
use headwater_graph::declarations::Relation;
use headwater_graph::Declarations;
pub const RULE: &str = "lifecycle.dependency.on_terminal";
const NO_HALF: &str = "the entry carries no declared half";
const NO_PAIR: &str = "this edge has no second document to read";
pub struct Dependency<'a> {
sensitive: Vec<&'a Relation>,
shape: &'a Shape,
facet: StateFacet,
}
impl<'a> Dependency<'a> {
pub fn over(declarations: &'a Declarations, shape: &'a Shape) -> Self {
Dependency {
sensitive: declarations
.relations
.iter()
.filter(|relation| relation.lifecycle_sensitive)
.collect(),
shape,
facet: StateFacet::of(shape),
}
}
fn regime_names(&self, kind: &str, state: &str) -> bool {
match self.shape.lifecycle_of(kind) {
Some(regime) => regime.states().contains(&state),
None => true,
}
}
}
impl EdgeCheck for Dependency<'_> {
const RULE: &'static str = self::RULE;
const VERSION: u32 = 1;
const UNIT: EdgeUnit = EdgeUnit::Pair;
fn instantiates(&self, relation: &str) -> bool {
self.facet.name.is_some() && self.sensitive.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
.sensitive
.iter()
.find(|known| known.name == half.declared)
else {
return Outcome::Skipped(NO_PAIR.to_string());
};
let Some((source, target)) = view.ends() else {
return Outcome::Skipped(NO_PAIR.to_string());
};
let (Some(source_facets), Some(target_facets)) = (source.facets(), target.facets()) else {
return Outcome::Skipped(
"the census parsed no document at one end of this edge, so there is no state to \
read there"
.to_string(),
);
};
let source_state = match self.facet.stood(source_facets) {
Stood::At(state) => state,
Stood::Undeclared => return Outcome::Skipped(undeclared("source", source.id)),
Stood::NotAState(value) => {
return Outcome::Skipped(not_a_state("source", source.id, value))
}
};
let target_state = match self.facet.stood(target_facets) {
Stood::At(state) => state,
Stood::Undeclared => return Outcome::Skipped(undeclared("target", target.id)),
Stood::NotAState(value) => {
return Outcome::Skipped(not_a_state("target", target.id, value))
}
};
if self.facet.standing(source_state) != Standing::Live {
return Outcome::Passed;
}
if self.facet.standing(target_state) != Standing::Terminal {
return Outcome::Passed;
}
if relation.sets_target_state.as_deref() == Some(target_state) {
return Outcome::Passed;
}
if !self.regime_names(target.kind, target_state) {
return Outcome::Skipped(format!(
"`{}` stands at `{target_state}`, which the lifecycle regime of a `{}` does not \
name, and `{}` reports that",
target.id,
target.kind,
crate::lifecycle_state::RULE
));
}
let (line, column) = at(Some(half.span));
Outcome::failed_with(Finding {
rule: self::RULE,
severity: Severity::Warn,
obligation: None,
path: half.source.path.clone(),
line,
column,
message: format!(
"`{}` stands at `{source_state}` and declares `{}` to `{}`, which stands at the \
terminal state `{target_state}`: the taxonomy declares this relation \
lifecycle-sensitive, so a live document does not rest on a retired one through it",
source.id, relation.name, target.id
),
remediation: format!(
"point `{}` in {} at the document that replaced {}, or retire {} on its own terms \
if it still means what the retired one said",
half.name, half.source.path, target.path, source.path
),
patch: None,
})
}
}
pub(crate) fn undeclared(end: &str, id: &str) -> String {
format!(
"the document at the {end} end, `{id}`, declares no value for the state facet, so there is \
no state to read there"
)
}
pub(crate) fn not_a_state(end: &str, id: &str, value: &str) -> String {
format!(
"the document at the {end} end, `{id}`, writes `{value}`, which the state facet does not \
admit, and `facet.value.not_permitted` reports that"
)
}