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