use std::collections::BTreeMap;
use crate::entry::Entry;
use crate::id::EntryId;
use crate::links::StructuralSettings;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CheckMode {
Edit,
Review,
}
impl CheckMode {
pub fn severity(self) -> CheckSeverity {
match self {
| Self::Edit => CheckSeverity::Warning,
| Self::Review => CheckSeverity::Error,
}
}
pub fn check_entries<'a>(
self, entries: impl IntoIterator<Item = &'a Entry>, structural: &StructuralSettings,
) -> CheckReport {
let entries = entries.into_iter().collect::<Vec<_>>();
let entries_by_id =
entries.iter().map(|entry| (entry.id.clone(), *entry)).collect::<BTreeMap<_, _>>();
let severity = self.severity();
let mut report = CheckReport::new();
for entry in entries {
for (field, targets) in entry.metadata.structural_fields() {
if !structural.contains_field(field) {
report.push(CheckDiagnostic {
severity: CheckSeverity::Warning,
kind: CheckDiagnosticKind::UnconfiguredStructuralField,
entry: entry.id.clone(),
field: field.to_owned(),
target: None,
});
continue;
}
for target in targets {
if !entries_by_id.contains_key(target) {
report.push(CheckDiagnostic {
severity,
kind: CheckDiagnosticKind::MissingTarget,
entry: entry.id.clone(),
field: field.to_owned(),
target: Some(target.clone()),
});
}
}
}
}
report
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CheckSeverity {
Warning,
Error,
}
impl CheckSeverity {
pub fn label(self) -> &'static str {
match self {
| Self::Warning => "warning",
| Self::Error => "error",
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CheckDiagnosticKind {
UnconfiguredStructuralField,
MissingTarget,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CheckDiagnostic {
pub severity: CheckSeverity,
pub kind: CheckDiagnosticKind,
pub entry: EntryId,
pub field: String,
pub target: Option<EntryId>,
}
impl CheckDiagnostic {
pub fn message(&self) -> String {
match self.kind {
| CheckDiagnosticKind::UnconfiguredStructuralField => format!(
"`{}` uses structural field `{}` that is not configured in `Sirno.toml`",
self.entry, self.field
),
| CheckDiagnosticKind::MissingTarget => format!(
"`{}` references missing entry `{}` through `{}`",
self.entry,
self.target.as_ref().expect("missing target diagnostic has target"),
self.field
),
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct CheckReport {
diagnostics: Vec<CheckDiagnostic>,
}
impl CheckReport {
pub fn new() -> Self {
Self::default()
}
pub fn push(&mut self, diagnostic: CheckDiagnostic) {
self.diagnostics.push(diagnostic);
}
pub fn diagnostics(&self) -> &[CheckDiagnostic] {
&self.diagnostics
}
pub fn is_clean(&self) -> bool {
self.diagnostics.is_empty()
}
pub fn has_errors(&self) -> bool {
self.diagnostics.iter().any(|diagnostic| diagnostic.severity == CheckSeverity::Error)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::entry::EntryMetadata;
use crate::links::StructuralFieldSettings;
const FIELD_TOPIC: &str = "topic";
fn entry(id: &str) -> Entry {
Entry::new(EntryId::new(id).unwrap(), EntryMetadata::new(id, "desc").unwrap(), "")
}
fn structural_settings() -> StructuralSettings {
StructuralSettings::from_fields([(FIELD_TOPIC, StructuralFieldSettings::default())])
}
#[test]
fn clean_entries_produce_clean_report() {
let mut concept = entry("concept");
concept.metadata.push_structural_target(FIELD_TOPIC, EntryId::new("meta").unwrap());
let mut meta = entry("meta");
meta.metadata.push_structural_target(FIELD_TOPIC, EntryId::new("meta").unwrap());
let report = CheckMode::Review.check_entries([&concept, &meta], &structural_settings());
assert!(report.is_clean());
}
#[test]
fn edit_mode_reports_dangling_reference_as_warning() {
let mut concept = entry("concept");
concept.metadata.push_structural_target(FIELD_TOPIC, EntryId::new("meta").unwrap());
let report = CheckMode::Edit.check_entries([&concept], &structural_settings());
assert_eq!(report.diagnostics()[0].kind, CheckDiagnosticKind::MissingTarget);
assert_eq!(report.diagnostics()[0].severity, CheckSeverity::Warning);
assert!(!report.has_errors());
}
#[test]
fn review_mode_reports_dangling_reference_as_error() {
let mut concept = entry("concept");
concept.metadata.push_structural_target(FIELD_TOPIC, EntryId::new("meta").unwrap());
let report = CheckMode::Review.check_entries([&concept], &structural_settings());
assert_eq!(report.diagnostics()[0].kind, CheckDiagnosticKind::MissingTarget);
assert_eq!(report.diagnostics()[0].severity, CheckSeverity::Error);
assert!(report.has_errors());
}
#[test]
fn unconfigured_structural_fields_warn() {
let mut concept = entry("concept");
concept.metadata.push_structural_target(FIELD_TOPIC, EntryId::new("meta").unwrap());
let report = CheckMode::Review.check_entries([&concept], &StructuralSettings::default());
assert_eq!(report.diagnostics()[0].kind, CheckDiagnosticKind::UnconfiguredStructuralField);
assert_eq!(report.diagnostics()[0].severity, CheckSeverity::Warning);
assert!(!report.has_errors());
}
}