use crate::dependency::{not_a_state, undeclared};
use crate::finding::{at, Finding, Severity};
use crate::instance::Outcome;
use crate::lifecycle_state::{Standing, StateFacet, Stood};
use crate::scope::{EdgeCheck, EdgeEnd, EdgeUnit, EdgeView};
use crate::shape::Shape;
use headwater_graph::declarations::Relation;
use headwater_graph::edges::Edge;
use headwater_graph::Declarations;
pub const RULE: &str = "lifecycle.dependency.on_initial";
const NO_HALF: &str = "the entry carries no declared half";
const NO_PAIR: &str = "this edge has no second document to read";
pub struct InitialDependency<'a> {
relations: &'a [Relation],
shape: &'a Shape,
facet: StateFacet,
}
impl<'a> InitialDependency<'a> {
pub fn over(declarations: &'a Declarations, shape: &'a Shape) -> Self {
InitialDependency {
relations: &declarations.relations,
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 InitialDependency<'_> {
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.relations.iter().any(|known| known.name == relation)
}
fn evaluate(&self, view: &EdgeView<'_>) -> Outcome {
let Some(first) = view.declared_half().or_else(|| view.inverse_half()) else {
return Outcome::Skipped(NO_HALF.to_string());
};
let Some(relation) = self
.relations
.iter()
.find(|known| known.name == first.declared)
else {
return Outcome::Skipped(NO_PAIR.to_string());
};
let Some((source, target)) = view.ends() else {
return Outcome::Skipped(NO_PAIR.to_string());
};
if relation.sets_target_state.is_some() {
return Outcome::Passed;
}
let written = [
view.declared_half().map(|half| (half, source, target)),
view.inverse_half().map(|half| (half, target, source)),
];
let mut findings = Vec::new();
let mut skipped = None;
for (half, writer, cited) in written.into_iter().flatten() {
match self.judge(half, writer, cited) {
Judged::Passed => {}
Judged::Skipped(why) => {
skipped.get_or_insert(why);
}
Judged::Failed(finding) => findings.push(*finding),
}
}
if !findings.is_empty() {
return Outcome::failed(findings);
}
match skipped {
Some(why) => Outcome::Skipped(why),
None => Outcome::Passed,
}
}
}
enum Judged {
Passed,
Skipped(String),
Failed(Box<Finding>),
}
impl InitialDependency<'_> {
fn judge(&self, half: &Edge, writer: EdgeEnd<'_>, cited: EdgeEnd<'_>) -> Judged {
let (Some(writer_facets), Some(cited_facets)) = (writer.facets(), cited.facets()) else {
return Judged::Skipped(
"the census parsed no document at one end of this edge, so there is no state to \
read there"
.to_string(),
);
};
let writer_state = match self.facet.stood(writer_facets) {
Stood::At(state) => state,
Stood::Undeclared => return Judged::Skipped(undeclared("source", writer.id)),
Stood::NotAState(value) => {
return Judged::Skipped(not_a_state("source", writer.id, value))
}
};
let cited_state = match self.facet.stood(cited_facets) {
Stood::At(state) => state,
Stood::Undeclared => return Judged::Skipped(undeclared("target", cited.id)),
Stood::NotAState(value) => {
return Judged::Skipped(not_a_state("target", cited.id, value))
}
};
if self.facet.standing(writer_state) != Standing::Live {
return Judged::Passed;
}
if self.facet.standing(cited_state) != Standing::Initial {
return Judged::Passed;
}
if !self.regime_names(cited.kind, cited_state) {
return Judged::Skipped(format!(
"`{}` stands at `{cited_state}`, which the lifecycle regime of a `{}` does not \
name, and `{}` reports that",
cited.id,
cited.kind,
crate::lifecycle_state::RULE
));
}
let (line, column) = at(Some(half.span));
Judged::Failed(Box::new(Finding {
rule: self::RULE,
severity: Severity::Warn,
obligation: None,
path: half.source.path.clone(),
line,
column,
message: format!(
"`{}` stands at `{writer_state}` and writes `{}` to `{}`, which stands at the \
initial state `{cited_state}`: nothing has promoted that document, so a live \
document does not rest on it",
writer.id, half.name, cited.id
),
remediation: format!(
"promote {} once it is settled, point `{}` in {} at a document that stands, or \
move {} back to its initial state until the target is settled",
cited.path, half.name, half.source.path, writer.path
),
patch: None,
}))
}
}