1use serde::Serialize;
2use std::str::FromStr;
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
8#[serde(rename_all = "lowercase")]
9pub enum TaskStatus {
10 Active,
12 Archived,
14}
15
16impl TaskStatus {
17 pub const ACTIVE: &'static str = "active";
18 pub const ARCHIVED: &'static str = "archived";
19
20 pub fn as_str(&self) -> &str {
22 match self {
23 TaskStatus::Active => Self::ACTIVE,
24 TaskStatus::Archived => Self::ARCHIVED,
25 }
26 }
27
28 pub fn can_transition_to(self, target: Self) -> bool {
30 matches!(
31 (self, target),
32 (TaskStatus::Active, TaskStatus::Active)
33 | (TaskStatus::Active, TaskStatus::Archived)
34 | (TaskStatus::Archived, TaskStatus::Archived)
35 )
36 }
37}
38
39impl FromStr for TaskStatus {
40 type Err = String;
41
42 fn from_str(s: &str) -> Result<Self, Self::Err> {
43 match s {
44 Self::ACTIVE => Ok(TaskStatus::Active),
45 Self::ARCHIVED => Ok(TaskStatus::Archived),
46 _ => Err(format!("Invalid TaskStatus: {}", s)),
47 }
48 }
49}
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
57#[serde(rename_all = "lowercase")]
58pub enum TodoStatus {
59 Pending,
61 Done,
63 Cancelled,
65}
66
67impl TodoStatus {
68 pub const PENDING: &'static str = "pending";
69 pub const DONE: &'static str = "done";
70 pub const CANCELLED: &'static str = "cancelled";
71
72 pub fn as_str(&self) -> &str {
74 match self {
75 TodoStatus::Pending => Self::PENDING,
76 TodoStatus::Done => Self::DONE,
77 TodoStatus::Cancelled => Self::CANCELLED,
78 }
79 }
80
81 pub fn can_transition_to(self, target: Self) -> bool {
83 if Self::is_reopen_attempt(self, target) {
84 return false;
85 }
86
87 matches!(
88 (self, target),
89 (TodoStatus::Pending, TodoStatus::Pending)
90 | (TodoStatus::Pending, TodoStatus::Done)
91 | (TodoStatus::Pending, TodoStatus::Cancelled)
92 | (TodoStatus::Done, TodoStatus::Done)
93 | (TodoStatus::Cancelled, TodoStatus::Cancelled)
94 )
95 }
96
97 pub fn is_reopen_attempt(from: Self, to: Self) -> bool {
99 from != Self::Pending && to == Self::Pending
100 }
101
102 pub fn is_terminal(self) -> bool {
104 matches!(self, Self::Done | Self::Cancelled)
105 }
106}
107
108impl FromStr for TodoStatus {
109 type Err = String;
110
111 fn from_str(s: &str) -> Result<Self, Self::Err> {
112 match s {
113 Self::PENDING => Ok(TodoStatus::Pending),
114 Self::DONE => Ok(TodoStatus::Done),
115 Self::CANCELLED => Ok(TodoStatus::Cancelled),
116 _ => Err(format!("Invalid TodoStatus: {}", s)),
117 }
118 }
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124
125 #[test]
126 fn test_task_status_as_str() {
127 assert_eq!(TaskStatus::Active.as_str(), "active");
128 assert_eq!(TaskStatus::Archived.as_str(), "archived");
129 }
130
131 #[test]
132 fn test_task_status_from_str() {
133 assert!(matches!(
134 "active".parse::<TaskStatus>(),
135 Ok(TaskStatus::Active)
136 ));
137 assert!(matches!(
138 "archived".parse::<TaskStatus>(),
139 Ok(TaskStatus::Archived)
140 ));
141 assert!("invalid".parse::<TaskStatus>().is_err());
142 }
143
144 #[test]
145 fn test_task_status_transitions() {
146 assert!(TaskStatus::Active.can_transition_to(TaskStatus::Archived));
147 assert!(!TaskStatus::Archived.can_transition_to(TaskStatus::Active));
148 assert!(TaskStatus::Archived.can_transition_to(TaskStatus::Archived));
149 }
150
151 #[test]
152 fn test_todo_status_as_str() {
153 assert_eq!(TodoStatus::Pending.as_str(), "pending");
154 assert_eq!(TodoStatus::Done.as_str(), "done");
155 assert_eq!(TodoStatus::Cancelled.as_str(), "cancelled");
156 }
157
158 #[test]
159 fn test_todo_status_from_str() {
160 assert!(matches!(
161 "pending".parse::<TodoStatus>(),
162 Ok(TodoStatus::Pending)
163 ));
164 assert!(matches!("done".parse::<TodoStatus>(), Ok(TodoStatus::Done)));
165 assert!(matches!(
166 "cancelled".parse::<TodoStatus>(),
167 Ok(TodoStatus::Cancelled)
168 ));
169 assert!("invalid".parse::<TodoStatus>().is_err());
170 }
171
172 #[test]
173 fn test_todo_status_reopen_is_forbidden() {
174 assert!(TodoStatus::is_reopen_attempt(
175 TodoStatus::Done,
176 TodoStatus::Pending
177 ));
178 assert!(TodoStatus::is_reopen_attempt(
179 TodoStatus::Cancelled,
180 TodoStatus::Pending
181 ));
182 assert!(!TodoStatus::is_reopen_attempt(
183 TodoStatus::Pending,
184 TodoStatus::Pending
185 ));
186 assert!(!TodoStatus::can_transition_to(
187 TodoStatus::Done,
188 TodoStatus::Pending
189 ));
190 assert!(!TodoStatus::can_transition_to(
191 TodoStatus::Cancelled,
192 TodoStatus::Pending
193 ));
194 assert!(TodoStatus::Done.is_terminal());
195 assert!(TodoStatus::Cancelled.is_terminal());
196 assert!(!TodoStatus::Pending.is_terminal());
197 }
198}