use fig::Value;
use crate::field::FieldRule;
use crate::vocab::Term;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Severity {
Notice,
Confirm,
ConfirmExplicitly,
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct Consequence {
pub when: Option<Value>,
pub severity: Severity,
pub message: String,
}
impl Consequence {
pub fn always(message: impl Into<String>) -> Self {
Self {
when: None,
severity: Severity::Notice,
message: message.into(),
}
}
pub fn when(value: impl Into<Value>, message: impl Into<String>) -> Self {
Self {
when: Some(value.into()),
severity: Severity::Notice,
message: message.into(),
}
}
pub fn severity(mut self, severity: Severity) -> Self {
self.severity = severity;
self
}
pub fn applies_to(&self, value: &Value) -> bool {
match &self.when {
None => true,
Some(guard) => guard.eq_canonical(value),
}
}
}
impl<C> FieldRule<C> {
pub fn consequences_of(&self, value: &Value) -> Vec<&Consequence> {
self.on_change
.iter()
.filter(|c| c.applies_to(value))
.collect()
}
pub fn severity_of(&self, value: &Value) -> Option<Severity> {
self.on_change
.iter()
.filter(|c| c.applies_to(value))
.map(|c| c.severity)
.max()
}
}
pub fn guards_without_terms<'a>(consequences: &'a [Consequence], terms: &[Term]) -> Vec<&'a str> {
consequences
.iter()
.filter_map(|c| match &c.when {
Some(Value::Str(s)) => Some(s.as_str()),
_ => None,
})
.filter(|s| !terms.iter().any(|t| t.value == *s))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::path::PathPat;
use crate::vocab::Validate;
#[derive(Debug, Clone)]
struct NoConstraint;
impl Validate for NoConstraint {
fn validate(&self, _value: &Value) -> crate::Validation {
crate::Validation::Ok
}
}
fn rule(consequences: Vec<Consequence>) -> FieldRule<NoConstraint> {
FieldRule::new(PathPat::key("setting")).on_change_all(consequences)
}
#[test]
fn a_float_guard_matches_rather_than_being_skipped() {
let r = rule(vec![Consequence::when(1.5, "The scale changes.")]);
assert_eq!(r.consequences_of(&Value::Float(1.5)).len(), 1);
assert_eq!(
r.consequences_of(&crate::FieldType::Float.coerce("1.50"))
.len(),
1,
);
assert!(r.consequences_of(&Value::Float(1.6)).is_empty());
}
#[test]
fn guard_matching_goes_through_eq_canonical_not_derived_equality() {
let r = rule(vec![Consequence::when(1i64, "The count changes.")]);
assert_eq!(r.consequences_of(&Value::Uint(1)).len(), 1);
assert_eq!(
rule(vec![Consequence::when(Value::Uint(u64::MAX), "…")])
.consequences_of(&Value::Uint(u64::MAX))
.len(),
1,
);
assert!(
rule(vec![Consequence::when(Value::Uint(1), "…")])
.consequences_of(&Value::Int(-1))
.is_empty()
);
}
#[test]
fn a_typoed_guard_is_caught_by_the_lint_because_nothing_else_would() {
let terms = [Term::value("off"), Term::value("registry")];
let declared = [
Consequence::always("The archive is rewritten."),
Consequence::when("none", "History will be discarded."),
Consequence::when(false, "…"),
Consequence::when("registry", "…"),
];
assert_eq!(guards_without_terms(&declared, &terms), vec!["none"]);
let retired = [Term::value("off").retired(true)];
assert!(guards_without_terms(&declared[3..], &terms).is_empty());
assert!(guards_without_terms(&[Consequence::when("off", "…")], &retired).is_empty());
}
#[test]
fn an_unguarded_and_a_matching_guarded_consequence_both_survive() {
let r = rule(vec![
Consequence::always("Every document is rewritten."),
Consequence::when("none", "Existing ids cannot be recovered.")
.severity(Severity::ConfirmExplicitly),
Consequence::when("registry", "Ids move into the registry."),
]);
let hit = r.consequences_of(&Value::Str("none".into()));
assert_eq!(hit.len(), 2);
assert_eq!(hit[0].message, "Every document is rewritten.");
assert_eq!(hit[1].message, "Existing ids cannot be recovered.");
assert_eq!(
r.severity_of(&Value::Str("none".into())),
Some(Severity::ConfirmExplicitly)
);
assert_eq!(r.consequences_of(&Value::Str("registry".into())).len(), 2);
assert_eq!(
r.severity_of(&Value::Str("registry".into())),
Some(Severity::Notice)
);
}
#[test]
fn asking_twice_about_one_value_answers_the_same_way_because_no_op_detection_is_the_hosts() {
let r = rule(vec![Consequence::when("off", "History will be discarded.")]);
let value = Value::Str("off".into());
let first = r.consequences_of(&value);
let second = r.consequences_of(&value);
assert_eq!(first, second);
assert_eq!(first.len(), 1);
assert_eq!(r.severity_of(&value), r.severity_of(&value));
}
#[test]
fn a_rule_declaring_nothing_has_no_consequences() {
let r: FieldRule<NoConstraint> = FieldRule::new(PathPat::key("title"));
assert!(r.consequences_of(&Value::Str("anything".into())).is_empty());
assert_eq!(r.severity_of(&Value::Str("anything".into())), None);
}
#[test]
fn severity_orders_ascending_so_max_picks_the_loudest() {
assert!(Severity::Notice < Severity::Confirm);
assert!(Severity::Confirm < Severity::ConfirmExplicitly);
}
}