use crate::diagnostics::Diagnostic;
use crate::wip::{WipDiagnostic, WipDiagnosticKind};
use super::calculate::CapacityScope;
use super::scope::{ScopeDiagnostic, ScopeDiagnosticKind};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CapacityWarningKind {
Overflow,
Conversion(WipDiagnosticKind),
Scope(ScopeDiagnosticKind),
}
impl CapacityWarningKind {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Overflow => "capacity_overflow",
Self::Conversion(kind) => kind.as_str(),
Self::Scope(kind) => kind.as_str(),
}
}
#[must_use]
pub const fn default_remedy(self) -> Option<&'static str> {
match self {
Self::Overflow => Some(
"reduce the scope's committed work, raise the ceiling in `tix.toml`, or move \
work to a scope with room",
),
Self::Conversion(WipDiagnosticKind::MalformedEstimate) => Some(
"rewrite `estimate:` as `<number>` or `<number><unit>`, or clear it to leave \
the issue unestimated",
),
Self::Conversion(WipDiagnosticKind::MissingConversion) => Some(
"declare the unit under `[kanban.<board>.wip.units]`, or state the estimate in \
a unit the board declares",
),
Self::Conversion(WipDiagnosticKind::AmbiguousConversion) => {
Some("declare a unit the arithmetic divides evenly into, or rewrite the estimate")
}
Self::Conversion(WipDiagnosticKind::UnknownUnit) => Some(
"declare the unit under `[kanban.<board>.wip.units]`, or name one the board \
declares",
),
Self::Conversion(_) => None,
Self::Scope(ScopeDiagnosticKind::UnknownScope) => {
Some("declare the cycle, or remove the reference that names it")
}
Self::Scope(ScopeDiagnosticKind::AmbiguousScope) => {
Some("keep one spelling of the cycle on the issue; the two count once")
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CapacityWarning {
pub kind: CapacityWarningKind,
pub scope: CapacityScope,
pub issues: Vec<u32>,
pub current: Option<u64>,
pub limit: Option<u64>,
pub overflow: Option<u64>,
pub unit: Option<String>,
pub reason: String,
pub remedy: Option<String>,
}
impl CapacityWarning {
#[must_use]
pub fn new(kind: CapacityWarningKind, scope: CapacityScope, reason: impl Into<String>) -> Self {
Self {
kind,
scope,
issues: Vec::new(),
current: None,
limit: None,
overflow: None,
unit: None,
reason: reason.into(),
remedy: kind.default_remedy().map(str::to_string),
}
}
#[must_use]
pub fn overflow(
scope: CapacityScope,
committed: u64,
capacity: u64,
unit: &str,
issues: Vec<u32>,
) -> Self {
let over = committed.saturating_sub(capacity);
let mut warning = Self::new(
CapacityWarningKind::Overflow,
scope,
format!(
"committed {committed}{unit} exceeds the configured capacity of {capacity}{unit} \
by {over}{unit}"
),
);
warning.issues = issues;
warning.current = Some(committed);
warning.limit = Some(capacity);
warning.overflow = Some(over);
warning.unit = Some(unit.to_string());
warning
}
#[must_use]
pub fn from_estimate_diagnostic(
scope: CapacityScope,
issue: u32,
unit: &str,
finding: &WipDiagnostic,
) -> Self {
let kind = CapacityWarningKind::Conversion(finding.kind);
let mut warning = Self::new(kind, scope, finding.message.clone())
.for_issue(issue)
.in_unit(unit);
if warning.remedy.is_none() {
warning.remedy.clone_from(&finding.because);
}
warning
}
#[must_use]
pub fn from_scope_diagnostic(scope: CapacityScope, finding: &ScopeDiagnostic) -> Self {
let kind = CapacityWarningKind::Scope(finding.kind);
let mut warning = Self::new(kind, scope, finding.message.clone());
if let Some(issue) = finding.issue {
warning = warning.for_issue(issue);
}
if warning.remedy.is_none() {
warning.remedy.clone_from(&finding.because);
}
warning
}
#[must_use]
pub fn unresolved_scope(scope: CapacityScope) -> Self {
let reason = format!(
"the {kind} `{reference}` does not resolve to a cycle record, so this scope selects \
no issues",
kind = scope.kind,
reference = scope.reference
);
Self::new(
CapacityWarningKind::Scope(ScopeDiagnosticKind::UnknownScope),
scope,
reason,
)
.with_remedy(
"declare the cycle, complete a record whose generated identity is half written, or \
remove the reference that names it",
)
}
#[must_use]
pub fn for_issue(mut self, issue: u32) -> Self {
self.issues.push(issue);
self
}
#[must_use]
pub fn in_unit(mut self, unit: &str) -> Self {
self.unit = Some(unit.to_string());
self
}
#[must_use]
pub fn with_remedy(mut self, remedy: impl Into<String>) -> Self {
self.remedy = Some(remedy.into());
self
}
#[must_use]
pub fn render(&self) -> String {
let mut diagnostic = Diagnostic::warning("E-tix-capacity", self.reason.clone())
.with_note(format!("{}: {}", self.kind.as_str(), self.scope.describe()));
if let Some(unit) = self.unit.as_deref() {
diagnostic = diagnostic.with_note(format!("unit: {unit}"));
}
if let Some(current) = self.current {
diagnostic = diagnostic.with_note(format!("current: {current}"));
}
if let Some(limit) = self.limit {
diagnostic = diagnostic.with_note(format!("limit: {limit}"));
}
if let Some(overflow) = self.overflow {
diagnostic = diagnostic.with_note(format!("overflow: {overflow}"));
}
if !self.issues.is_empty() {
let issues = self
.issues
.iter()
.map(|issue| format!("#{issue}"))
.collect::<Vec<_>>()
.join(", ");
diagnostic = diagnostic.with_note(format!("issues: {issues}"));
}
if let Some(remedy) = self.remedy.as_deref() {
diagnostic = diagnostic.with_help(remedy);
}
diagnostic.render()
}
}