use prov_graph::meta::Value;
use crate::spec::{EXPORT_KEYS, ExportSpec, GATE_KEYS};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExportIssue {
pub export: String,
pub key: String,
pub kind: ExportIssueKind,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ExportIssueKind {
NotAMapping,
NoGate,
UnknownKey,
GateUnknownKey,
}
impl ExportIssueKind {
pub fn is_fatal(&self) -> bool {
matches!(self, ExportIssueKind::NotAMapping | ExportIssueKind::NoGate)
}
pub fn expected(&self) -> &'static [&'static str] {
match self {
ExportIssueKind::UnknownKey => EXPORT_KEYS,
ExportIssueKind::GateUnknownKey | ExportIssueKind::NoGate => GATE_KEYS,
ExportIssueKind::NotAMapping => &[],
}
}
}
pub fn diagnose_export(name: &str, value: &Value) -> Vec<ExportIssue> {
let issue = |key: &str, kind| ExportIssue {
export: name.to_string(),
key: key.to_string(),
kind,
};
let Some(map) = value.as_mapping() else {
return vec![issue("", ExportIssueKind::NotAMapping)];
};
let mut issues = Vec::new();
if ExportSpec::parse(name, value).is_none() {
issues.push(issue("gate", ExportIssueKind::NoGate));
}
for (key, value) in map {
match key.as_str() {
"label" | "view" => {}
"gate" => {
let Some(gate) = value.as_mapping() else {
continue;
};
for (gate_key, _) in gate {
if !GATE_KEYS.contains(&gate_key.as_str()) {
issues.push(issue(gate_key, ExportIssueKind::GateUnknownKey));
}
}
}
_ => issues.push(issue(key, ExportIssueKind::UnknownKey)),
}
}
issues
}
pub fn diagnose_exports(exports: &Value) -> Vec<ExportIssue> {
let Some(map) = exports.as_mapping() else {
return Vec::new();
};
map.iter()
.flat_map(|(name, value)| diagnose_export(name, value))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use prov_graph::meta::Mapping;
fn entry(pairs: &[(&str, Value)]) -> Value {
let mut map = Mapping::new();
for (k, v) in pairs {
map.insert((*k).into(), v.clone());
}
Value::Mapping(map)
}
fn gate(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)
}
fn good_gate() -> Value {
gate(&[("field", "audience"), ("value", "family")])
}
#[test]
fn a_clean_export_reports_nothing() {
assert!(
diagnose_export(
"letters",
&entry(&[
("label", Value::String("Letters home".into())),
("gate", good_gate()),
("view", Value::String("daily".into())),
])
)
.is_empty()
);
}
#[test]
fn an_entry_that_is_not_a_mapping_is_reported_whole() {
let issues = diagnose_export("letters", &Value::String("family".into()));
assert_eq!(issues.len(), 1);
assert_eq!(issues[0].kind, ExportIssueKind::NotAMapping);
assert_eq!(issues[0].key, "");
}
#[test]
fn a_missing_or_unreadable_gate_is_reported() {
for broken in [
entry(&[("view", Value::String("daily".into()))]),
entry(&[("gate", Value::String("family".into()))]),
entry(&[("gate", gate(&[("field", "audience")]))]),
entry(&[("gate", gate(&[("value", "family")]))]),
entry(&[("gate", gate(&[("field", "audience"), ("value", " ")]))]),
] {
let issues = diagnose_export("letters", &broken);
assert!(
issues.iter().any(|i| i.kind == ExportIssueKind::NoGate),
"for {broken:?}"
);
assert_eq!(issues[0].kind.expected(), GATE_KEYS);
}
}
#[test]
fn an_unknown_key_is_reported_at_both_levels() {
let issues = diagnose_export(
"letters",
&entry(&[
("gate", good_gate()),
("veiw", Value::String("daily".into())),
]),
);
assert_eq!(issues.len(), 1);
assert_eq!(issues[0].key, "veiw");
assert_eq!(issues[0].kind, ExportIssueKind::UnknownKey);
assert_eq!(issues[0].kind.expected(), EXPORT_KEYS);
let issues = diagnose_export(
"letters",
&entry(&[(
"gate",
gate(&[
("field", "audience"),
("value", "family"),
("audience", "x"),
]),
)]),
);
assert_eq!(issues.len(), 1);
assert_eq!(issues[0].key, "audience");
assert_eq!(issues[0].kind, ExportIssueKind::GateUnknownKey);
assert_eq!(issues[0].kind.expected(), GATE_KEYS);
}
#[test]
fn fatal_issues_are_exactly_the_entries_parse_drops() {
let cases = [
Value::String("family".into()),
Value::Sequence(vec![]),
entry(&[("label", Value::String("Nameless".into()))]),
entry(&[("gate", Value::String("family".into()))]),
entry(&[("gate", gate(&[("field", "audience")]))]),
entry(&[("gate", good_gate())]),
entry(&[
("gate", good_gate()),
("veiw", Value::String("daily".into())),
]),
entry(&[(
"gate",
gate(&[("field", "audience"), ("value", "family"), ("extra", "x")]),
)]),
];
for case in cases {
let parsed = ExportSpec::parse("letters", &case).is_some();
let fatal = diagnose_export("letters", &case)
.iter()
.any(|i| i.kind.is_fatal());
assert_eq!(parsed, !fatal, "disagreed about {case:?}");
}
}
}