1#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
20#[serde(rename_all = "snake_case")]
21pub enum GoalStatus {
22 Active,
24 Paused,
26 Completed,
28 Cleared,
30}
31
32impl GoalStatus {
33 #[must_use]
44 pub fn can_transition_to(self, to: Self) -> bool {
45 matches!(
46 (self, to),
47 (Self::Active, Self::Paused | Self::Completed | Self::Cleared)
48 | (Self::Paused, Self::Active | Self::Cleared)
49 )
50 }
51
52 #[must_use]
63 pub fn is_terminal(self) -> bool {
64 matches!(self, Self::Completed | Self::Cleared)
65 }
66
67 #[must_use]
69 pub fn badge_symbol(self) -> &'static str {
70 match self {
71 Self::Active => "▶",
72 Self::Paused => "⏸",
73 Self::Completed => "✓",
74 Self::Cleared => "✗",
75 }
76 }
77}
78
79impl std::fmt::Display for GoalStatus {
80 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
81 let s = match self {
82 Self::Active => "active",
83 Self::Paused => "paused",
84 Self::Completed => "completed",
85 Self::Cleared => "cleared",
86 };
87 f.write_str(s)
88 }
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94
95 #[test]
96 fn valid_transitions() {
97 assert!(GoalStatus::Active.can_transition_to(GoalStatus::Paused));
98 assert!(GoalStatus::Active.can_transition_to(GoalStatus::Completed));
99 assert!(GoalStatus::Active.can_transition_to(GoalStatus::Cleared));
100 assert!(GoalStatus::Paused.can_transition_to(GoalStatus::Active));
101 assert!(GoalStatus::Paused.can_transition_to(GoalStatus::Cleared));
102 }
103
104 #[test]
105 fn terminal_states_reject_transitions() {
106 for from in [GoalStatus::Completed, GoalStatus::Cleared] {
107 for to in [
108 GoalStatus::Active,
109 GoalStatus::Paused,
110 GoalStatus::Completed,
111 GoalStatus::Cleared,
112 ] {
113 assert!(
114 !from.can_transition_to(to),
115 "{from:?} -> {to:?} should be invalid"
116 );
117 }
118 }
119 }
120
121 #[test]
122 fn is_terminal() {
123 assert!(GoalStatus::Completed.is_terminal());
124 assert!(GoalStatus::Cleared.is_terminal());
125 assert!(!GoalStatus::Active.is_terminal());
126 assert!(!GoalStatus::Paused.is_terminal());
127 }
128}