Skip to main content

track/models/
status.rs

1use serde::Serialize;
2use std::str::FromStr;
3
4/// Status of a task.
5///
6/// Tasks can be either active (currently being worked on) or archived (completed or abandoned).
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
8#[serde(rename_all = "lowercase")]
9pub enum TaskStatus {
10    /// Task is currently active and can be worked on
11    Active,
12    /// Task has been archived and is no longer active
13    Archived,
14}
15
16impl TaskStatus {
17    pub const ACTIVE: &'static str = "active";
18    pub const ARCHIVED: &'static str = "archived";
19
20    /// Converts the status to its string representation.
21    pub fn as_str(&self) -> &str {
22        match self {
23            TaskStatus::Active => Self::ACTIVE,
24            TaskStatus::Archived => Self::ARCHIVED,
25        }
26    }
27
28    /// Returns whether a transition from `self` to `target` is allowed.
29    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/// Status of a TODO item.
52///
53/// TODOs progress through different states during their lifecycle.
54/// Reopening a completed or cancelled TODO (transition back to `Pending`) is not allowed;
55/// add a new TODO instead.
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
57#[serde(rename_all = "lowercase")]
58pub enum TodoStatus {
59    /// TODO is pending and needs to be completed
60    Pending,
61    /// TODO has been completed
62    Done,
63    /// TODO has been cancelled and will not be completed
64    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    /// Converts the status to its string representation.
73    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    /// Returns whether a transition from `self` to `target` is allowed.
82    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    /// Returns true when moving a terminal TODO back to pending (reopen).
98    pub fn is_reopen_attempt(from: Self, to: Self) -> bool {
99        from != Self::Pending && to == Self::Pending
100    }
101
102    /// Returns true when the TODO can no longer be worked on.
103    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}