use crate::entity;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum StatusClass {
Workable,
Terminal,
Unrecognised,
}
struct KindPartition {
prefix: &'static str,
workable: &'static [&'static str],
terminal: &'static [&'static str],
}
const PARTITION: &[KindPartition] = &[
KindPartition {
prefix: "SL",
workable: &[
"proposed",
"design",
"plan",
"ready",
"started",
"audit",
"reconcile",
],
terminal: &["done", "abandoned"],
},
KindPartition {
prefix: "ADR",
workable: &["proposed"],
terminal: &["accepted", "rejected", "superseded", "deprecated"],
},
KindPartition {
prefix: "POL",
workable: &["draft"],
terminal: &["required", "deprecated", "retired"],
},
KindPartition {
prefix: "STD",
workable: &["draft"],
terminal: &["default", "required", "deprecated", "retired"],
},
KindPartition {
prefix: "PRD",
workable: &["draft"],
terminal: &["active", "deprecated", "superseded"],
},
KindPartition {
prefix: "SPEC",
workable: &["draft"],
terminal: &["active", "deprecated", "superseded"],
},
KindPartition {
prefix: "REQ",
workable: &["pending", "in-progress"],
terminal: &["active", "deprecated", "retired", "superseded"],
},
KindPartition {
prefix: "ISS",
workable: BACKLOG_WORKABLE,
terminal: BACKLOG_TERMINAL,
},
KindPartition {
prefix: "IMP",
workable: BACKLOG_WORKABLE,
terminal: BACKLOG_TERMINAL,
},
KindPartition {
prefix: "CHR",
workable: BACKLOG_WORKABLE,
terminal: BACKLOG_TERMINAL,
},
KindPartition {
prefix: "RSK",
workable: BACKLOG_WORKABLE,
terminal: BACKLOG_TERMINAL,
},
KindPartition {
prefix: "IDE",
workable: BACKLOG_WORKABLE,
terminal: BACKLOG_TERMINAL,
},
KindPartition {
prefix: "RV",
workable: &["active"],
terminal: &["done"],
},
KindPartition {
prefix: "ASM",
workable: &[],
terminal: crate::knowledge::ASSUMPTION_STATUSES,
},
KindPartition {
prefix: "DEC",
workable: &[],
terminal: crate::knowledge::DECISION_STATUSES,
},
KindPartition {
prefix: "QUE",
workable: &[],
terminal: crate::knowledge::QUESTION_STATUSES,
},
KindPartition {
prefix: "CON",
workable: &[],
terminal: crate::knowledge::CONSTRAINT_STATUSES,
},
];
const BACKLOG_WORKABLE: &[&str] = &["open", "triaged", "started"];
const BACKLOG_TERMINAL: &[&str] = &["resolved", "closed"];
pub(crate) fn status_class(kind: &entity::Kind, status: Option<&str>) -> StatusClass {
let Some(status) = status else {
return StatusClass::Terminal;
};
let Some(part) = PARTITION.iter().find(|p| p.prefix == kind.prefix) else {
return StatusClass::Unrecognised;
};
if part.workable.contains(&status) {
StatusClass::Workable
} else if part.terminal.contains(&status) {
StatusClass::Terminal
} else {
StatusClass::Unrecognised
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{adr, backlog, knowledge, policy, requirement, review, slice, spec, standard};
use std::collections::BTreeSet;
fn part(prefix: &str) -> &'static KindPartition {
PARTITION
.iter()
.find(|p| p.prefix == prefix)
.expect("prefix in PARTITION")
}
fn vocab(prefix: &str) -> BTreeSet<&'static str> {
let p = part(prefix);
p.workable
.iter()
.chain(p.terminal.iter())
.copied()
.collect()
}
fn set(items: &[&'static str]) -> BTreeSet<&'static str> {
items.iter().copied().collect()
}
#[test]
fn slice_partition_binds_adr009_lifecycle_vocabulary() {
assert_eq!(vocab("SL"), set(slice::SLICE_STATUSES));
}
#[test]
fn adr_partition_covers_the_real_vocabulary() {
assert_eq!(vocab("ADR"), set(adr::ADR_STATUSES));
}
#[test]
fn policy_partition_covers_the_real_vocabulary() {
assert_eq!(vocab("POL"), set(policy::POLICY_STATUSES));
}
#[test]
fn standard_partition_covers_the_real_vocabulary() {
assert_eq!(vocab("STD"), set(standard::STANDARD_STATUSES));
}
#[test]
fn prd_and_tech_spec_partitions_cover_the_real_vocabulary() {
assert_eq!(vocab("PRD"), set(spec::SPEC_STATUSES));
assert_eq!(vocab("SPEC"), set(spec::SPEC_STATUSES));
}
#[test]
fn requirement_partition_covers_the_real_vocabulary() {
assert_eq!(vocab("REQ"), set(requirement::REQ_STATUSES));
}
#[test]
fn backlog_partition_covers_the_real_vocabulary() {
for prefix in ["ISS", "IMP", "CHR", "RSK", "IDE"] {
assert_eq!(
vocab(prefix),
set(backlog::BACKLOG_STATUSES),
"{prefix} partition matches BACKLOG_STATUSES"
);
}
}
#[test]
fn review_partition_covers_the_real_vocabulary() {
assert_eq!(vocab("RV"), set(review::REVIEW_STATUSES));
}
#[test]
fn knowledge_partitions_cover_the_real_vocabularies() {
for kind in knowledge::RecordKind::ALL {
let prefix = kind.prefix();
assert_eq!(
vocab(prefix),
set(knowledge::statuses(kind)),
"{prefix} partition matches statuses({kind:?})"
);
}
}
#[test]
fn every_knowledge_status_classifies_terminal_never_workable() {
for kind in knowledge::RecordKind::ALL {
for status in knowledge::statuses(kind) {
assert_eq!(
status_class(kind.kind(), Some(status)),
StatusClass::Terminal,
"{:?}/{status} must be Terminal, never Workable",
kind
);
}
}
}
#[test]
fn decision_accepted_diverges_hidden_from_status_class() {
assert!(!knowledge::is_hidden(
knowledge::RecordKind::Decision,
"accepted"
));
assert_eq!(
status_class(&knowledge::DECISION_KIND, Some("accepted")),
StatusClass::Terminal
);
}
#[test]
fn rec_status_less_is_terminal_no_diagnostic() {
assert_eq!(
status_class(&crate::rec::REC_KIND, None),
StatusClass::Terminal
);
}
#[test]
fn unrecognised_status_is_its_own_class() {
assert_eq!(
status_class(&slice::SLICE_KIND, Some("not-a-real-status")),
StatusClass::Unrecognised
);
}
#[test]
fn workable_and_terminal_lookups() {
assert_eq!(
status_class(&slice::SLICE_KIND, Some("design")),
StatusClass::Workable
);
assert_eq!(
status_class(&slice::SLICE_KIND, Some("audit")),
StatusClass::Workable
);
assert_eq!(
status_class(&slice::SLICE_KIND, Some("reconcile")),
StatusClass::Workable
);
assert_eq!(
status_class(&slice::SLICE_KIND, Some("done")),
StatusClass::Terminal
);
}
#[test]
fn rv_derived_status_resolves_through_the_table() {
assert_eq!(
status_class(&review::REVIEW_KIND, Some("active")),
StatusClass::Workable
);
assert_eq!(
status_class(&review::REVIEW_KIND, Some("done")),
StatusClass::Terminal
);
}
}