use crate::declaration::EndpointKinds;
use crate::finding::{at, Finding, Severity};
use crate::instance::Outcome;
use crate::scope::{DocumentCheck, DocumentView};
use crate::shape::Shape;
use headwater_graph::edges::Problem;
use headwater_graph::index::{Defect, Reported};
use headwater_graph::Declarations;
pub const RULE: &str = "identifier.unusable";
const NO_REPORT: &str = "the view carries no phase-A report for this document";
pub struct Identity<'a> {
kinds: EndpointKinds<'a>,
facet: String,
}
impl<'a> Identity<'a> {
pub fn over(declarations: &'a Declarations, shape: &'a Shape, facet: &str) -> Self {
Identity {
kinds: EndpointKinds::of(declarations, shape),
facet: facet.to_string(),
}
}
}
impl DocumentCheck for Identity<'_> {
const RULE: &'static str = self::RULE;
const VERSION: u32 = 1;
const NEEDS_PHASE_A: bool = true;
fn instantiates(&self, kind: &str) -> bool {
self.kinds.admit(kind)
}
fn evaluate(&self, view: &DocumentView<'_>) -> Outcome {
let Some(trouble) = view.phase_a() else {
return Outcome::Skipped(NO_REPORT.to_string());
};
let sourceless = trouble
.relations
.iter()
.any(|reported| matches!(reported.problem, Problem::SourceHasNoIdentifier));
let mut findings: Vec<Finding> = trouble
.identity
.iter()
.filter_map(|reported| finding(reported, sourceless, &self.facet))
.collect();
if sourceless && findings.is_empty() {
findings.push(Finding {
rule: self::RULE,
severity: Severity::Error,
obligation: None,
path: view.path().to_string(),
line: 0,
column: 0,
message: Problem::SourceHasNoIdentifier.to_string(),
remediation: self.mint(),
patch: None,
});
}
Outcome::failed(findings)
}
}
impl Identity<'_> {
fn mint(&self) -> String {
format!(
"write one identifier of this document under the `{}` key, because a relation names a \
document by its identifier and a document with none can stand at neither end of one",
self.facet
)
}
}
fn finding(reported: &Reported, sourceless: bool, facet: &str) -> Option<Finding> {
match &reported.defect {
Defect::NoIdentifier { .. } | Defect::NotAScalar => {}
Defect::Duplicate { .. } => return None,
}
let (line, column) = at(reported.span);
Some(Finding {
rule: self::RULE,
severity: Severity::Error,
obligation: None,
path: reported.path.clone(),
line,
column,
message: format!("{}{}", reported.defect, cost(sourceless)),
remediation: format!(
"write one identifier of this document under the `{facet}` key, because a relation \
names a document by its identifier and a document with none can stand at neither end \
of one"
),
patch: None,
})
}
fn cost(sourceless: bool) -> &'static str {
match sourceless {
true => {
", and every entry of its `relations:` block is lost with it, because an edge is \
identified by its source"
}
false => "",
}
}
#[cfg(test)]
mod tests {
use super::*;
use headwater_yaml::{Position, Span};
fn reported(defect: Defect) -> Reported {
Reported {
path: "docs/spec/03-anonymous.md".to_string(),
span: Some(Span {
start: Position {
line: 2,
col: 1,
offset: 0,
},
end: Position {
line: 2,
col: 3,
offset: 2,
},
}),
defect,
}
}
#[test]
fn a_document_that_cannot_be_named_is_one_finding_that_states_what_it_costs() {
for defect in [
Defect::NoIdentifier {
kind: "design_spec".to_string(),
},
Defect::NotAScalar,
] {
let bare = finding(&reported(defect.clone()), false, "id").expect("a finding");
assert_eq!(bare.severity, Severity::Error, "{bare:#?}");
assert!(!bare.fixable(), "{bare:#?}");
assert_eq!(bare.line, 2, "{bare:#?}");
assert!(bare.remediation.contains("`id` key"), "{bare:#?}");
assert!(!bare.message.contains("relations"), "{bare:#?}");
let laden = finding(&reported(defect), true, "id").expect("a finding");
assert!(
laden
.message
.contains("every entry of its `relations:` block is lost"),
"{laden:#?}"
);
assert_eq!(laden.remediation, bare.remediation);
}
assert!(finding(
&reported(Defect::Duplicate {
id: "SPEC-FIX-first".to_string(),
other: "docs/spec/00-first.md".to_string(),
other_span: None,
}),
false,
"id"
)
.is_none());
}
#[test]
fn the_repair_names_the_configured_identifier_key() {
let found = finding(&reported(Defect::NotAScalar), false, "identifier").expect("a finding");
assert!(found.remediation.contains("`identifier` key"), "{found:#?}");
}
}