use crate::finding::{at, Finding, Severity};
use crate::instance::Outcome;
use crate::scope::{DocumentCheck, DocumentView, ExportTargets};
use crate::shape::Shape;
pub const RULE: &str = "facet.value.not_permitted";
struct Enumerated {
facet: String,
values: Vec<String>,
}
struct Admitted {
kind: String,
facets: Vec<Enumerated>,
}
pub struct Values {
admitted: Vec<Admitted>,
}
impl Values {
pub fn over(shape: &Shape) -> Self {
let enumerated: Vec<&crate::shape::Facet> = shape
.facets
.iter()
.filter(|facet| !facet.values.is_empty())
.collect();
Values {
admitted: shape
.kinds
.iter()
.map(|kind| {
let forbidden: Vec<&str> = shape
.ancestry(&kind.name)
.iter()
.flat_map(|step| step.forbid.iter().map(String::as_str))
.collect();
Admitted {
kind: kind.name.clone(),
facets: enumerated
.iter()
.filter(|facet| !forbidden.contains(&facet.name.as_str()))
.map(|facet| Enumerated {
facet: facet.name.clone(),
values: shape
.admitted_values(&kind.name, &facet.name)
.unwrap_or_default()
.into_iter()
.map(str::to_string)
.collect(),
})
.collect(),
}
})
.filter(|admitted| !admitted.facets.is_empty())
.collect(),
}
}
fn admitted_by(&self, kind: &str) -> &[Enumerated] {
self.admitted
.iter()
.find(|admitted| admitted.kind == kind)
.map(|admitted| admitted.facets.as_slice())
.unwrap_or_default()
}
}
impl DocumentCheck for Values {
const RULE: &'static str = self::RULE;
const VERSION: u32 = 2;
const EXPORTABLE_AS: ExportTargets = &["jsonschema"];
fn instantiates(&self, kind: &str) -> bool {
!self.admitted_by(kind).is_empty()
}
fn evaluate(&self, view: &DocumentView<'_>) -> Outcome {
let findings = self
.admitted_by(view.kind())
.iter()
.filter_map(|Enumerated { facet, values }| {
let entry = view.facets().entry(facet)?;
let declared = entry.value.value.as_scalar()?;
if values.iter().any(|value| value == &declared.text) {
return None;
}
let (line, column) = at(Some(entry.value.span));
Some(Finding {
rule: self::RULE,
severity: Severity::Error,
obligation: None,
path: view.path().to_string(),
line,
column,
message: format!(
"`{facet}` admits {}, and this document declares `{}`",
values.join(", "),
declared.text
),
remediation: format!(
"change `{facet}` in {} to one of: {}",
view.path(),
values.join(", ")
),
patch: None,
})
})
.collect();
Outcome::failed(findings)
}
}