use prov_graph::meta::Value;
use crate::filter::{CONDITION_KEYS, Condition};
use crate::spec::{GRAINS, Grain, VIEW_KEYS, ViewSpec};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ViewIssue {
pub view: String,
pub key: String,
pub kind: ViewIssueKind,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ViewIssueKind {
NotAMapping,
NoGrouping,
UnknownKey,
BadGrain,
NoCondition,
}
impl ViewIssueKind {
pub fn is_fatal(&self) -> bool {
matches!(self, ViewIssueKind::NotAMapping | ViewIssueKind::NoGrouping)
}
pub fn expected(&self) -> &'static [&'static str] {
match self {
ViewIssueKind::UnknownKey => VIEW_KEYS,
ViewIssueKind::BadGrain => GRAINS,
ViewIssueKind::NoCondition => CONDITION_KEYS,
_ => &[],
}
}
}
pub fn diagnose_view(name: &str, value: &Value) -> Vec<ViewIssue> {
let issue = |key: &str, kind| ViewIssue {
view: name.to_string(),
key: key.to_string(),
kind,
};
let Some(map) = value.as_mapping() else {
return vec![issue("", ViewIssueKind::NotAMapping)];
};
let mut issues = Vec::new();
if ViewSpec::parse(name, value).is_none() {
issues.push(issue("group", ViewIssueKind::NoGrouping));
}
for (key, value) in map {
match key.as_str() {
"label" | "icon" | "under" | "group" => {}
"where" => {
if Condition::parse(value).is_none() {
issues.push(issue(key, ViewIssueKind::NoCondition));
}
}
"by" | "nest" => {
if Grain::parse(value).is_none() {
issues.push(issue(key, ViewIssueKind::BadGrain));
}
}
_ => issues.push(issue(key, ViewIssueKind::UnknownKey)),
}
}
issues
}
pub fn diagnose_views(views: &Value) -> Vec<ViewIssue> {
let Some(map) = views.as_mapping() else {
return Vec::new();
};
map.iter()
.flat_map(|(name, value)| diagnose_view(name, value))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use prov_graph::meta::Mapping;
fn view(pairs: &[(&str, &str)]) -> Value {
let mut map = Mapping::new();
for (k, v) in pairs {
map.insert((*k).into(), Value::String((*v).to_string()));
}
Value::Mapping(map)
}
#[test]
fn a_clean_view_reports_nothing() {
assert!(
diagnose_view(
"daily",
&view(&[
("label", "Daily"),
("icon", "calendar"),
("group", "created"),
("by", "month"),
("under", "[Daily](id:abc1234)"),
("nest", "year"),
])
)
.is_empty()
);
}
#[test]
fn an_entry_that_is_not_a_mapping_is_reported_whole() {
let issues = diagnose_view("daily", &Value::String("created".into()));
assert_eq!(issues.len(), 1);
assert_eq!(issues[0].kind, ViewIssueKind::NotAMapping);
}
#[test]
fn a_missing_group_is_reported() {
let issues = diagnose_view("daily", &view(&[("label", "Daily")]));
assert!(issues.iter().any(|i| i.kind == ViewIssueKind::NoGrouping));
}
#[test]
fn a_misspelled_grain_is_reported_for_both_axes() {
for key in ["by", "nest"] {
let issues = diagnose_view("daily", &view(&[("group", "created"), (key, "yearr")]));
assert_eq!(
issues,
vec![ViewIssue {
view: "daily".into(),
key: key.into(),
kind: ViewIssueKind::BadGrain,
}]
);
}
}
#[test]
fn a_where_that_yields_no_condition_is_reported() {
for broken in [
Value::String("audience == public".into()),
Value::Mapping(Mapping::new()),
view(&[("hasnt", "draft")]),
] {
let mut entry = Mapping::new();
entry.insert("group".into(), Value::String("created".into()));
entry.insert("where".into(), broken.clone());
let issues = diagnose_view("daily", &Value::Mapping(entry));
assert_eq!(
issues,
vec![ViewIssue {
view: "daily".into(),
key: "where".into(),
kind: ViewIssueKind::NoCondition,
}],
"for {broken:?}"
);
assert_eq!(issues[0].kind.expected(), CONDITION_KEYS);
}
}
#[test]
fn a_readable_where_reports_nothing() {
let mut entry = Mapping::new();
entry.insert("group".into(), Value::String("created".into()));
entry.insert(
"where".into(),
Value::Mapping({
let mut w = Mapping::new();
w.insert("not".into(), view(&[("has", "draft")]));
w
}),
);
assert!(diagnose_view("daily", &Value::Mapping(entry)).is_empty());
}
#[test]
fn an_unknown_key_is_reported() {
let issues = diagnose_view("daily", &view(&[("group", "created"), ("labl", "Daily")]));
assert_eq!(issues.len(), 1);
assert_eq!(issues[0].key, "labl");
assert_eq!(issues[0].kind, ViewIssueKind::UnknownKey);
assert_eq!(issues[0].kind.expected(), VIEW_KEYS);
}
#[test]
fn fatal_issues_are_exactly_the_entries_parse_drops() {
let cases = [
Value::String("created".into()),
Value::Sequence(vec![]),
view(&[("label", "Nameless")]),
view(&[("group", " ")]),
view(&[("group", "created")]),
view(&[("group", "created"), ("by", "yearr")]),
view(&[("group", "created"), ("labl", "x")]),
];
for case in cases {
let parsed = ViewSpec::parse("daily", &case).is_some();
let fatal = diagnose_view("daily", &case)
.iter()
.any(|i| i.kind.is_fatal());
assert_eq!(parsed, !fatal, "disagreed about {case:?}");
}
}
}