1use indexmap::IndexSet;
2use serde::{Deserialize, Serialize};
3use str_slug::slug;
4
5#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone)]
6pub struct Task {
7 pub id: usize,
8 pub description: String,
9 pub state: State,
10 pub tags: IndexSet<String>,
11 pub project: String,
12}
13
14#[derive(
15 Debug,
16 Deserialize,
17 Serialize,
18 Default,
19 PartialEq,
20 Eq,
21 Copy,
22 Clone,
23 PartialOrd,
24 Ord,
25)]
26pub enum State {
27 #[default]
28 ToDo,
29 Doing,
30 Waiting,
31 Done,
32}
33
34impl Task {
35 pub fn create(description: impl Into<String>) -> TaskBuilder {
36 TaskBuilder {
37 id: 0,
38 description: description.into(),
39 state: State::default(),
40 tags: None,
41 project: None,
42 }
43 }
44
45 pub fn add_tag(&mut self, tag: impl Into<String>) {
46 self.tags.insert(slug(tag.into()));
47 }
48
49 pub fn add_tags(
50 &mut self,
51 tags: impl IntoIterator<Item = impl Into<String>>,
52 ) {
53 self.tags
54 .extend(tags.into_iter().map(|tag| slug(tag.into())));
55 }
56
57 pub fn replace_tags(
58 &mut self,
59 tags: impl IntoIterator<Item = impl Into<String>>,
60 ) {
61 self.tags = tags.into_iter().map(|tag| slug(tag.into())).collect();
62 }
63
64 pub fn change_description(&mut self, description: impl Into<String>) {
65 self.description = description.into();
66 }
67
68 pub fn change_state(&mut self, state: State) {
69 self.state = state;
70 }
71}
72
73#[derive(Debug)]
74pub struct TaskBuilder {
75 id: usize,
76 description: String,
77 state: State,
78 tags: Option<IndexSet<String>>,
79 project: Option<String>,
80}
81
82impl TaskBuilder {
83 pub fn id(&mut self, id: usize) -> &mut Self {
84 self.id = id;
85 self
86 }
87
88 pub fn state(&mut self, state: State) -> &mut Self {
89 self.state = state;
90 self
91 }
92
93 pub fn project(&mut self, project: impl Into<String>) -> &mut Self {
94 self.project = Some(project.into());
95 self
96 }
97
98 pub fn tag(&mut self, tag: impl Into<String>) -> &mut Self {
99 if self.tags.is_none() {
100 let mut tags = IndexSet::new();
101 tags.insert(slug(tag.into()));
102
103 self.tags = Some(tags);
104 } else {
105 self.tags.as_mut().map(|tags| tags.insert(slug(tag.into())));
106 }
107
108 self
109 }
110
111 pub fn tags(
112 &mut self,
113 tags: impl IntoIterator<Item = impl Into<String>>,
114 ) -> &mut Self {
115 self.tags =
116 Some(tags.into_iter().map(|tag| slug(tag.into())).collect());
117 self
118 }
119
120 #[must_use]
121 pub fn build(&self) -> Task {
122 Task {
123 id: self.id,
124 description: self.description.clone(),
125 state: self.state,
126 tags: self.tags.clone().unwrap_or_default(),
127 project: self
128 .project
129 .clone()
130 .unwrap_or_else(|| "Inbox".to_string()),
131 }
132 }
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138
139 #[test]
140 fn task_builder_works() {
141 let task = Task::create("This is a test").build();
142
143 assert_eq!(
144 task,
145 Task {
146 id: 0,
147 description: "This is a test".to_string(),
148 state: State::ToDo,
149 tags: IndexSet::new(),
150 project: "Inbox".to_string()
151 }
152 );
153 }
154
155 #[test]
156 fn task_builder_change_state_works() {
157 let task = Task::create("This is a test").state(State::Waiting).build();
158
159 assert_eq!(
160 task,
161 Task {
162 id: 0,
163 description: "This is a test".to_string(),
164 state: State::Waiting,
165 tags: IndexSet::new(),
166 project: "Inbox".to_string()
167 }
168 );
169 }
170
171 #[test]
172 fn task_builder_change_tag_works() {
173 let task = Task::create("This is a test").tag("Test").build();
174 let mut set = IndexSet::new();
175 set.insert("test".to_string());
176
177 assert_eq!(
178 task,
179 Task {
180 id: 0,
181 description: "This is a test".to_string(),
182 state: State::ToDo,
183 tags: set,
184 project: "Inbox".to_string()
185 }
186 );
187 }
188
189 #[test]
190 fn task_builder_change_tags_works() {
191 let task = Task::create("This is a test")
192 .tags(["Test 1", "Test 2"])
193 .build();
194
195 let mut set = IndexSet::new();
196 set.insert("test-1".to_string());
197 set.insert("test-2".to_string());
198
199 assert_eq!(
200 task,
201 Task {
202 id: 0,
203 description: "This is a test".to_string(),
204 state: State::ToDo,
205 tags: set,
206 project: "Inbox".to_string()
207 }
208 );
209 }
210
211 #[test]
212 fn task_builder_change_project_works() {
213 let task = Task::create("This is a test").project("Testing").build();
214
215 assert_eq!(
216 task,
217 Task {
218 id: 0,
219 description: "This is a test".to_string(),
220 state: State::ToDo,
221 tags: IndexSet::new(),
222 project: "Testing".to_string()
223 }
224 );
225 }
226
227 #[test]
228 fn add_tag_works() {
229 let mut task =
230 Task::create("This is a test").project("Testing").build();
231 task.add_tag("Testing tags");
232
233 assert_eq!(
234 task,
235 Task {
236 id: 0,
237 description: "This is a test".to_string(),
238 state: State::ToDo,
239 tags: IndexSet::from(["testing-tags".to_string()]),
240 project: "Testing".to_string()
241 }
242 );
243 }
244
245 #[test]
246 fn add_tags_works() {
247 let mut task =
248 Task::create("This is a test").project("Testing").build();
249 task.add_tags(["Testing tags", "another tag", "Yet Another Tag"]);
250
251 assert_eq!(
252 task,
253 Task {
254 id: 0,
255 description: "This is a test".to_string(),
256 state: State::ToDo,
257 tags: IndexSet::from([
258 "testing-tags".to_string(),
259 "another-tag".to_string(),
260 "yet-another-tag".to_string()
261 ]),
262 project: "Testing".to_string()
263 }
264 );
265 }
266}