use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum RoadmapDiagnosticKind {
MissingReference,
EmptyScope,
UnresolvableRollup,
SourceDiagnostic,
UnknownValue,
Undated,
}
impl RoadmapDiagnosticKind {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::MissingReference => "missing_reference",
Self::EmptyScope => "empty_scope",
Self::UnresolvableRollup => "unresolvable_rollup",
Self::SourceDiagnostic => "source_diagnostic",
Self::UnknownValue => "unknown_value",
Self::Undated => "undated",
}
}
pub const ALL: [Self; 6] = [
Self::MissingReference,
Self::EmptyScope,
Self::UnresolvableRollup,
Self::SourceDiagnostic,
Self::UnknownValue,
Self::Undated,
];
#[must_use]
pub const fn default_remedy(self) -> Option<&'static str> {
match self {
Self::MissingReference => {
Some("declare the referenced project/epic/milestone, or remove the reference")
}
Self::EmptyScope => Some(
"associate a project, epic or milestone, or record a target for this initiative",
),
Self::UnresolvableRollup => {
Some("inspect the source service for the reference; the value could not be derived")
}
Self::SourceDiagnostic => {
Some("resolve the source entity's own finding; it is reported verbatim here")
}
Self::UnknownValue => {
Some("record progress or a health value on the referenced sources")
}
Self::Undated => Some("set a `target_date`, or leave the row in the undated state"),
}
}
}
impl fmt::Display for RoadmapDiagnosticKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RoadmapDiagnostic {
pub kind: RoadmapDiagnosticKind,
pub initiative: Option<String>,
pub field: Option<String>,
pub reference: Option<String>,
pub source: Option<String>,
pub message: String,
pub remedy: Option<String>,
}
impl RoadmapDiagnostic {
#[must_use]
pub fn new(kind: RoadmapDiagnosticKind, message: impl Into<String>) -> Self {
Self {
kind,
initiative: None,
field: None,
reference: None,
source: None,
message: message.into(),
remedy: kind.default_remedy().map(str::to_string),
}
}
#[must_use]
pub fn for_initiative(mut self, initiative: impl Into<String>) -> Self {
self.initiative = Some(initiative.into());
self
}
#[must_use]
pub fn at_field(mut self, field: impl Into<String>) -> Self {
self.field = Some(field.into());
self
}
#[must_use]
pub fn referencing(mut self, reference: impl Into<String>) -> Self {
self.reference = Some(reference.into());
self
}
#[must_use]
pub fn about_source(mut self, source: impl Into<String>) -> Self {
self.source = Some(source.into());
self
}
#[must_use]
pub fn with_remedy(mut self, remedy: impl Into<String>) -> Self {
self.remedy = Some(remedy.into());
self
}
fn order_key(&self) -> OrderKey<'_> {
(
self.kind.as_str(),
self.initiative.as_deref(),
self.field.as_deref(),
self.reference.as_deref(),
self.source.as_deref(),
self.message.as_str(),
)
}
}
type OrderKey<'a> = (
&'static str,
Option<&'a str>,
Option<&'a str>,
Option<&'a str>,
Option<&'a str>,
&'a str,
);
pub fn order(diagnostics: &mut [RoadmapDiagnostic]) {
diagnostics.sort_by(|a, b| a.order_key().cmp(&b.order_key()));
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_kind_has_a_stable_spelling_and_a_remedy() {
for kind in RoadmapDiagnosticKind::ALL {
assert_ne!(kind.as_str(), "");
assert!(kind.default_remedy().is_some(), "{kind} has no remedy");
}
let mut spellings: Vec<&str> = RoadmapDiagnosticKind::ALL
.iter()
.map(|kind| kind.as_str())
.collect();
spellings.sort_unstable();
spellings.dedup();
assert_eq!(spellings.len(), RoadmapDiagnosticKind::ALL.len());
}
#[test]
fn order_is_total_and_stable() {
let mut diagnostics = vec![
RoadmapDiagnostic::new(RoadmapDiagnosticKind::Undated, "b").for_initiative("i-a"),
RoadmapDiagnostic::new(RoadmapDiagnosticKind::MissingReference, "z")
.for_initiative("i-b")
.at_field("projects")
.referencing("p-x"),
RoadmapDiagnostic::new(RoadmapDiagnosticKind::MissingReference, "z")
.for_initiative("i-a")
.at_field("epics")
.referencing("e-x"),
];
order(&mut diagnostics);
assert_eq!(diagnostics[0].initiative.as_deref(), Some("i-a"));
assert_eq!(diagnostics[0].field.as_deref(), Some("epics"));
assert_eq!(diagnostics[1].initiative.as_deref(), Some("i-b"));
assert_eq!(diagnostics[2].kind, RoadmapDiagnosticKind::Undated);
}
}