#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum GoalStatus {
Active,
Paused,
Completed,
Cleared,
}
impl GoalStatus {
#[must_use]
pub fn can_transition_to(self, to: Self) -> bool {
matches!(
(self, to),
(Self::Active, Self::Paused | Self::Completed | Self::Cleared)
| (Self::Paused, Self::Active | Self::Cleared)
)
}
#[must_use]
pub fn is_terminal(self) -> bool {
matches!(self, Self::Completed | Self::Cleared)
}
#[must_use]
pub fn badge_symbol(self) -> &'static str {
match self {
Self::Active => "▶",
Self::Paused => "⏸",
Self::Completed => "✓",
Self::Cleared => "✗",
}
}
}
impl std::fmt::Display for GoalStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = match self {
Self::Active => "active",
Self::Paused => "paused",
Self::Completed => "completed",
Self::Cleared => "cleared",
};
f.write_str(s)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn valid_transitions() {
assert!(GoalStatus::Active.can_transition_to(GoalStatus::Paused));
assert!(GoalStatus::Active.can_transition_to(GoalStatus::Completed));
assert!(GoalStatus::Active.can_transition_to(GoalStatus::Cleared));
assert!(GoalStatus::Paused.can_transition_to(GoalStatus::Active));
assert!(GoalStatus::Paused.can_transition_to(GoalStatus::Cleared));
}
#[test]
fn terminal_states_reject_transitions() {
for from in [GoalStatus::Completed, GoalStatus::Cleared] {
for to in [
GoalStatus::Active,
GoalStatus::Paused,
GoalStatus::Completed,
GoalStatus::Cleared,
] {
assert!(
!from.can_transition_to(to),
"{from:?} -> {to:?} should be invalid"
);
}
}
}
#[test]
fn is_terminal() {
assert!(GoalStatus::Completed.is_terminal());
assert!(GoalStatus::Cleared.is_terminal());
assert!(!GoalStatus::Active.is_terminal());
assert!(!GoalStatus::Paused.is_terminal());
}
}