things3_core/
models.rs

1//! Data models for Things 3 entities
2
3use chrono::{DateTime, NaiveDate, Utc};
4use serde::{Deserialize, Serialize};
5use uuid::Uuid;
6
7/// Task status enumeration
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
9pub enum TaskStatus {
10    #[serde(rename = "incomplete")]
11    Incomplete,
12    #[serde(rename = "completed")]
13    Completed,
14    #[serde(rename = "canceled")]
15    Canceled,
16    #[serde(rename = "trashed")]
17    Trashed,
18}
19
20/// Task type enumeration
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
22pub enum TaskType {
23    #[serde(rename = "to-do")]
24    Todo,
25    #[serde(rename = "project")]
26    Project,
27    #[serde(rename = "heading")]
28    Heading,
29    #[serde(rename = "area")]
30    Area,
31}
32
33/// Main task entity
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct Task {
36    /// Unique identifier
37    pub uuid: Uuid,
38    /// Task title
39    pub title: String,
40    /// Task type
41    pub task_type: TaskType,
42    /// Task status
43    pub status: TaskStatus,
44    /// Optional notes
45    pub notes: Option<String>,
46    /// Start date
47    pub start_date: Option<NaiveDate>,
48    /// Deadline
49    pub deadline: Option<NaiveDate>,
50    /// Creation timestamp
51    pub created: DateTime<Utc>,
52    /// Last modification timestamp
53    pub modified: DateTime<Utc>,
54    /// Parent project UUID
55    pub project_uuid: Option<Uuid>,
56    /// Parent area UUID
57    pub area_uuid: Option<Uuid>,
58    /// Parent task UUID
59    pub parent_uuid: Option<Uuid>,
60    /// Associated tags
61    pub tags: Vec<String>,
62    /// Child tasks
63    pub children: Vec<Task>,
64}
65
66/// Project entity
67#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct Project {
69    /// Unique identifier
70    pub uuid: Uuid,
71    /// Project title
72    pub title: String,
73    /// Optional notes
74    pub notes: Option<String>,
75    /// Start date
76    pub start_date: Option<NaiveDate>,
77    /// Deadline
78    pub deadline: Option<NaiveDate>,
79    /// Creation timestamp
80    pub created: DateTime<Utc>,
81    /// Last modification timestamp
82    pub modified: DateTime<Utc>,
83    /// Parent area UUID
84    pub area_uuid: Option<Uuid>,
85    /// Associated tags
86    pub tags: Vec<String>,
87    /// Project status
88    pub status: TaskStatus,
89    /// Child tasks
90    pub tasks: Vec<Task>,
91}
92
93/// Area entity
94#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct Area {
96    /// Unique identifier
97    pub uuid: Uuid,
98    /// Area title
99    pub title: String,
100    /// Optional notes
101    pub notes: Option<String>,
102    /// Creation timestamp
103    pub created: DateTime<Utc>,
104    /// Last modification timestamp
105    pub modified: DateTime<Utc>,
106    /// Associated tags
107    pub tags: Vec<String>,
108    /// Child projects
109    pub projects: Vec<Project>,
110}
111
112/// Tag entity
113#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct Tag {
115    /// Unique identifier
116    pub uuid: Uuid,
117    /// Tag title
118    pub title: String,
119    /// Usage count
120    pub usage_count: u32,
121    /// Associated tasks
122    pub tasks: Vec<Uuid>,
123}
124
125/// Task creation request
126#[derive(Debug, Clone, Serialize, Deserialize)]
127pub struct CreateTaskRequest {
128    /// Task title
129    pub title: String,
130    /// Optional notes
131    pub notes: Option<String>,
132    /// Start date
133    pub start_date: Option<NaiveDate>,
134    /// Deadline
135    pub deadline: Option<NaiveDate>,
136    /// Parent project UUID
137    pub project_uuid: Option<Uuid>,
138    /// Parent area UUID
139    pub area_uuid: Option<Uuid>,
140    /// Associated tags
141    pub tags: Vec<String>,
142}
143
144/// Task update request
145#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct UpdateTaskRequest {
147    /// Task UUID
148    pub uuid: Uuid,
149    /// New title
150    pub title: Option<String>,
151    /// New notes
152    pub notes: Option<String>,
153    /// New start date
154    pub start_date: Option<NaiveDate>,
155    /// New deadline
156    pub deadline: Option<NaiveDate>,
157    /// New status
158    pub status: Option<TaskStatus>,
159    /// New tags
160    pub tags: Option<Vec<String>>,
161}
162
163/// Task filters for queries
164#[derive(Debug, Clone, Serialize, Deserialize, Default)]
165pub struct TaskFilters {
166    /// Filter by status
167    pub status: Option<TaskStatus>,
168    /// Filter by task type
169    pub task_type: Option<TaskType>,
170    /// Filter by project UUID
171    pub project_uuid: Option<Uuid>,
172    /// Filter by area UUID
173    pub area_uuid: Option<Uuid>,
174    /// Filter by tags
175    pub tags: Option<Vec<String>>,
176    /// Filter by start date range
177    pub start_date_from: Option<NaiveDate>,
178    pub start_date_to: Option<NaiveDate>,
179    /// Filter by deadline range
180    pub deadline_from: Option<NaiveDate>,
181    pub deadline_to: Option<NaiveDate>,
182    /// Search query
183    pub search_query: Option<String>,
184    /// Limit results
185    pub limit: Option<usize>,
186    /// Offset for pagination
187    pub offset: Option<usize>,
188}
189
190#[cfg(test)]
191mod tests {
192    use super::*;
193    use chrono::NaiveDate;
194
195    #[test]
196    fn test_task_status_serialization() {
197        let status = TaskStatus::Incomplete;
198        let serialized = serde_json::to_string(&status).unwrap();
199        assert_eq!(serialized, "\"incomplete\"");
200
201        let status = TaskStatus::Completed;
202        let serialized = serde_json::to_string(&status).unwrap();
203        assert_eq!(serialized, "\"completed\"");
204
205        let status = TaskStatus::Canceled;
206        let serialized = serde_json::to_string(&status).unwrap();
207        assert_eq!(serialized, "\"canceled\"");
208
209        let status = TaskStatus::Trashed;
210        let serialized = serde_json::to_string(&status).unwrap();
211        assert_eq!(serialized, "\"trashed\"");
212    }
213
214    #[test]
215    fn test_task_status_deserialization() {
216        let deserialized: TaskStatus = serde_json::from_str("\"incomplete\"").unwrap();
217        assert_eq!(deserialized, TaskStatus::Incomplete);
218
219        let deserialized: TaskStatus = serde_json::from_str("\"completed\"").unwrap();
220        assert_eq!(deserialized, TaskStatus::Completed);
221
222        let deserialized: TaskStatus = serde_json::from_str("\"canceled\"").unwrap();
223        assert_eq!(deserialized, TaskStatus::Canceled);
224
225        let deserialized: TaskStatus = serde_json::from_str("\"trashed\"").unwrap();
226        assert_eq!(deserialized, TaskStatus::Trashed);
227    }
228
229    #[test]
230    fn test_task_type_serialization() {
231        let task_type = TaskType::Todo;
232        let serialized = serde_json::to_string(&task_type).unwrap();
233        assert_eq!(serialized, "\"to-do\"");
234
235        let task_type = TaskType::Project;
236        let serialized = serde_json::to_string(&task_type).unwrap();
237        assert_eq!(serialized, "\"project\"");
238
239        let task_type = TaskType::Heading;
240        let serialized = serde_json::to_string(&task_type).unwrap();
241        assert_eq!(serialized, "\"heading\"");
242
243        let task_type = TaskType::Area;
244        let serialized = serde_json::to_string(&task_type).unwrap();
245        assert_eq!(serialized, "\"area\"");
246    }
247
248    #[test]
249    fn test_task_type_deserialization() {
250        let deserialized: TaskType = serde_json::from_str("\"to-do\"").unwrap();
251        assert_eq!(deserialized, TaskType::Todo);
252
253        let deserialized: TaskType = serde_json::from_str("\"project\"").unwrap();
254        assert_eq!(deserialized, TaskType::Project);
255
256        let deserialized: TaskType = serde_json::from_str("\"heading\"").unwrap();
257        assert_eq!(deserialized, TaskType::Heading);
258
259        let deserialized: TaskType = serde_json::from_str("\"area\"").unwrap();
260        assert_eq!(deserialized, TaskType::Area);
261    }
262
263    #[test]
264    fn test_task_creation() {
265        let uuid = Uuid::new_v4();
266        let now = Utc::now();
267        let start_date = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
268        let deadline = NaiveDate::from_ymd_opt(2024, 12, 31).unwrap();
269
270        let task = Task {
271            uuid,
272            title: "Test Task".to_string(),
273            task_type: TaskType::Todo,
274            status: TaskStatus::Incomplete,
275            notes: Some("Test notes".to_string()),
276            start_date: Some(start_date),
277            deadline: Some(deadline),
278            created: now,
279            modified: now,
280            project_uuid: None,
281            area_uuid: None,
282            parent_uuid: None,
283            tags: vec!["work".to_string(), "urgent".to_string()],
284            children: vec![],
285        };
286
287        assert_eq!(task.uuid, uuid);
288        assert_eq!(task.title, "Test Task");
289        assert_eq!(task.task_type, TaskType::Todo);
290        assert_eq!(task.status, TaskStatus::Incomplete);
291        assert_eq!(task.notes, Some("Test notes".to_string()));
292        assert_eq!(task.start_date, Some(start_date));
293        assert_eq!(task.deadline, Some(deadline));
294        assert_eq!(task.tags.len(), 2);
295        assert!(task.tags.contains(&"work".to_string()));
296        assert!(task.tags.contains(&"urgent".to_string()));
297    }
298
299    #[test]
300    fn test_task_serialization() {
301        let uuid = Uuid::new_v4();
302        let now = Utc::now();
303
304        let task = Task {
305            uuid,
306            title: "Test Task".to_string(),
307            task_type: TaskType::Todo,
308            status: TaskStatus::Incomplete,
309            notes: None,
310            start_date: None,
311            deadline: None,
312            created: now,
313            modified: now,
314            project_uuid: None,
315            area_uuid: None,
316            parent_uuid: None,
317            tags: vec![],
318            children: vec![],
319        };
320
321        let serialized = serde_json::to_string(&task).unwrap();
322        let deserialized: Task = serde_json::from_str(&serialized).unwrap();
323
324        assert_eq!(deserialized.uuid, task.uuid);
325        assert_eq!(deserialized.title, task.title);
326        assert_eq!(deserialized.task_type, task.task_type);
327        assert_eq!(deserialized.status, task.status);
328    }
329
330    #[test]
331    fn test_project_creation() {
332        let uuid = Uuid::new_v4();
333        let area_uuid = Uuid::new_v4();
334        let now = Utc::now();
335
336        let project = Project {
337            uuid,
338            title: "Test Project".to_string(),
339            notes: Some("Project notes".to_string()),
340            start_date: None,
341            deadline: None,
342            created: now,
343            modified: now,
344            area_uuid: Some(area_uuid),
345            tags: vec!["project".to_string()],
346            status: TaskStatus::Incomplete,
347            tasks: vec![],
348        };
349
350        assert_eq!(project.uuid, uuid);
351        assert_eq!(project.title, "Test Project");
352        assert_eq!(project.area_uuid, Some(area_uuid));
353        assert_eq!(project.status, TaskStatus::Incomplete);
354        assert_eq!(project.tags.len(), 1);
355    }
356
357    #[test]
358    fn test_project_serialization() {
359        let uuid = Uuid::new_v4();
360        let now = Utc::now();
361
362        let project = Project {
363            uuid,
364            title: "Test Project".to_string(),
365            notes: None,
366            start_date: None,
367            deadline: None,
368            created: now,
369            modified: now,
370            area_uuid: None,
371            tags: vec![],
372            status: TaskStatus::Incomplete,
373            tasks: vec![],
374        };
375
376        let serialized = serde_json::to_string(&project).unwrap();
377        let deserialized: Project = serde_json::from_str(&serialized).unwrap();
378
379        assert_eq!(deserialized.uuid, project.uuid);
380        assert_eq!(deserialized.title, project.title);
381        assert_eq!(deserialized.status, project.status);
382    }
383
384    #[test]
385    fn test_area_creation() {
386        let uuid = Uuid::new_v4();
387        let now = Utc::now();
388
389        let area = Area {
390            uuid,
391            title: "Test Area".to_string(),
392            notes: Some("Area notes".to_string()),
393            created: now,
394            modified: now,
395            tags: vec!["area".to_string()],
396            projects: vec![],
397        };
398
399        assert_eq!(area.uuid, uuid);
400        assert_eq!(area.title, "Test Area");
401        assert_eq!(area.notes, Some("Area notes".to_string()));
402        assert_eq!(area.tags.len(), 1);
403    }
404
405    #[test]
406    fn test_area_serialization() {
407        let uuid = Uuid::new_v4();
408        let now = Utc::now();
409
410        let area = Area {
411            uuid,
412            title: "Test Area".to_string(),
413            notes: None,
414            created: now,
415            modified: now,
416            tags: vec![],
417            projects: vec![],
418        };
419
420        let serialized = serde_json::to_string(&area).unwrap();
421        let deserialized: Area = serde_json::from_str(&serialized).unwrap();
422
423        assert_eq!(deserialized.uuid, area.uuid);
424        assert_eq!(deserialized.title, area.title);
425    }
426
427    #[test]
428    fn test_tag_creation() {
429        let uuid = Uuid::new_v4();
430        let task_uuid = Uuid::new_v4();
431
432        let tag = Tag {
433            uuid,
434            title: "work".to_string(),
435            usage_count: 5,
436            tasks: vec![task_uuid],
437        };
438
439        assert_eq!(tag.uuid, uuid);
440        assert_eq!(tag.title, "work");
441        assert_eq!(tag.usage_count, 5);
442        assert_eq!(tag.tasks.len(), 1);
443        assert_eq!(tag.tasks[0], task_uuid);
444    }
445
446    #[test]
447    fn test_tag_serialization() {
448        let uuid = Uuid::new_v4();
449
450        let tag = Tag {
451            uuid,
452            title: "test".to_string(),
453            usage_count: 0,
454            tasks: vec![],
455        };
456
457        let serialized = serde_json::to_string(&tag).unwrap();
458        let deserialized: Tag = serde_json::from_str(&serialized).unwrap();
459
460        assert_eq!(deserialized.uuid, tag.uuid);
461        assert_eq!(deserialized.title, tag.title);
462        assert_eq!(deserialized.usage_count, tag.usage_count);
463    }
464
465    #[test]
466    fn test_create_task_request() {
467        let project_uuid = Uuid::new_v4();
468        let area_uuid = Uuid::new_v4();
469        let start_date = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
470
471        let request = CreateTaskRequest {
472            title: "New Task".to_string(),
473            notes: Some("Task notes".to_string()),
474            start_date: Some(start_date),
475            deadline: None,
476            project_uuid: Some(project_uuid),
477            area_uuid: Some(area_uuid),
478            tags: vec!["new".to_string()],
479        };
480
481        assert_eq!(request.title, "New Task");
482        assert_eq!(request.project_uuid, Some(project_uuid));
483        assert_eq!(request.area_uuid, Some(area_uuid));
484        assert_eq!(request.start_date, Some(start_date));
485        assert_eq!(request.tags.len(), 1);
486    }
487
488    #[test]
489    fn test_create_task_request_serialization() {
490        let request = CreateTaskRequest {
491            title: "Test".to_string(),
492            notes: None,
493            start_date: None,
494            deadline: None,
495            project_uuid: None,
496            area_uuid: None,
497            tags: vec![],
498        };
499
500        let serialized = serde_json::to_string(&request).unwrap();
501        let deserialized: CreateTaskRequest = serde_json::from_str(&serialized).unwrap();
502
503        assert_eq!(deserialized.title, request.title);
504    }
505
506    #[test]
507    fn test_update_task_request() {
508        let uuid = Uuid::new_v4();
509
510        let request = UpdateTaskRequest {
511            uuid,
512            title: Some("Updated Title".to_string()),
513            notes: Some("Updated notes".to_string()),
514            start_date: None,
515            deadline: None,
516            status: Some(TaskStatus::Completed),
517            tags: Some(vec!["updated".to_string()]),
518        };
519
520        assert_eq!(request.uuid, uuid);
521        assert_eq!(request.title, Some("Updated Title".to_string()));
522        assert_eq!(request.status, Some(TaskStatus::Completed));
523        assert_eq!(request.tags, Some(vec!["updated".to_string()]));
524    }
525
526    #[test]
527    fn test_update_task_request_serialization() {
528        let uuid = Uuid::new_v4();
529
530        let request = UpdateTaskRequest {
531            uuid,
532            title: None,
533            notes: None,
534            start_date: None,
535            deadline: None,
536            status: None,
537            tags: None,
538        };
539
540        let serialized = serde_json::to_string(&request).unwrap();
541        let deserialized: UpdateTaskRequest = serde_json::from_str(&serialized).unwrap();
542
543        assert_eq!(deserialized.uuid, request.uuid);
544    }
545
546    #[test]
547    fn test_task_filters_default() {
548        let filters = TaskFilters::default();
549
550        assert!(filters.status.is_none());
551        assert!(filters.task_type.is_none());
552        assert!(filters.project_uuid.is_none());
553        assert!(filters.area_uuid.is_none());
554        assert!(filters.tags.is_none());
555        assert!(filters.start_date_from.is_none());
556        assert!(filters.start_date_to.is_none());
557        assert!(filters.deadline_from.is_none());
558        assert!(filters.deadline_to.is_none());
559        assert!(filters.search_query.is_none());
560        assert!(filters.limit.is_none());
561        assert!(filters.offset.is_none());
562    }
563
564    #[test]
565    fn test_task_filters_creation() {
566        let project_uuid = Uuid::new_v4();
567        let start_date = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
568
569        let filters = TaskFilters {
570            status: Some(TaskStatus::Incomplete),
571            task_type: Some(TaskType::Todo),
572            project_uuid: Some(project_uuid),
573            area_uuid: None,
574            tags: Some(vec!["work".to_string()]),
575            start_date_from: Some(start_date),
576            start_date_to: None,
577            deadline_from: None,
578            deadline_to: None,
579            search_query: Some("test".to_string()),
580            limit: Some(10),
581            offset: Some(0),
582        };
583
584        assert_eq!(filters.status, Some(TaskStatus::Incomplete));
585        assert_eq!(filters.task_type, Some(TaskType::Todo));
586        assert_eq!(filters.project_uuid, Some(project_uuid));
587        assert_eq!(filters.search_query, Some("test".to_string()));
588        assert_eq!(filters.limit, Some(10));
589        assert_eq!(filters.offset, Some(0));
590    }
591
592    #[test]
593    fn test_task_filters_serialization() {
594        let filters = TaskFilters {
595            status: Some(TaskStatus::Completed),
596            task_type: Some(TaskType::Project),
597            project_uuid: None,
598            area_uuid: None,
599            tags: None,
600            start_date_from: None,
601            start_date_to: None,
602            deadline_from: None,
603            deadline_to: None,
604            search_query: None,
605            limit: None,
606            offset: None,
607        };
608
609        let serialized = serde_json::to_string(&filters).unwrap();
610        let deserialized: TaskFilters = serde_json::from_str(&serialized).unwrap();
611
612        assert_eq!(deserialized.status, filters.status);
613        assert_eq!(deserialized.task_type, filters.task_type);
614    }
615
616    #[test]
617    fn test_task_status_equality() {
618        assert_eq!(TaskStatus::Incomplete, TaskStatus::Incomplete);
619        assert_ne!(TaskStatus::Incomplete, TaskStatus::Completed);
620        assert_ne!(TaskStatus::Completed, TaskStatus::Canceled);
621        assert_ne!(TaskStatus::Canceled, TaskStatus::Trashed);
622    }
623
624    #[test]
625    fn test_task_type_equality() {
626        assert_eq!(TaskType::Todo, TaskType::Todo);
627        assert_ne!(TaskType::Todo, TaskType::Project);
628        assert_ne!(TaskType::Project, TaskType::Heading);
629        assert_ne!(TaskType::Heading, TaskType::Area);
630    }
631
632    #[test]
633    fn test_task_with_children() {
634        let parent_uuid = Uuid::new_v4();
635        let child_uuid = Uuid::new_v4();
636        let now = Utc::now();
637
638        let child_task = Task {
639            uuid: child_uuid,
640            title: "Child Task".to_string(),
641            task_type: TaskType::Todo,
642            status: TaskStatus::Incomplete,
643            notes: None,
644            start_date: None,
645            deadline: None,
646            created: now,
647            modified: now,
648            project_uuid: None,
649            area_uuid: None,
650            parent_uuid: Some(parent_uuid),
651            tags: vec![],
652            children: vec![],
653        };
654
655        let parent_task = Task {
656            uuid: parent_uuid,
657            title: "Parent Task".to_string(),
658            task_type: TaskType::Heading,
659            status: TaskStatus::Incomplete,
660            notes: None,
661            start_date: None,
662            deadline: None,
663            created: now,
664            modified: now,
665            project_uuid: None,
666            area_uuid: None,
667            parent_uuid: None,
668            tags: vec![],
669            children: vec![child_task],
670        };
671
672        assert_eq!(parent_task.children.len(), 1);
673        assert_eq!(parent_task.children[0].parent_uuid, Some(parent_uuid));
674        assert_eq!(parent_task.children[0].title, "Child Task");
675    }
676
677    #[test]
678    fn test_project_with_tasks() {
679        let project_uuid = Uuid::new_v4();
680        let task_uuid = Uuid::new_v4();
681        let now = Utc::now();
682
683        let task = Task {
684            uuid: task_uuid,
685            title: "Project Task".to_string(),
686            task_type: TaskType::Todo,
687            status: TaskStatus::Incomplete,
688            notes: None,
689            start_date: None,
690            deadline: None,
691            created: now,
692            modified: now,
693            project_uuid: Some(project_uuid),
694            area_uuid: None,
695            parent_uuid: None,
696            tags: vec![],
697            children: vec![],
698        };
699
700        let project = Project {
701            uuid: project_uuid,
702            title: "Test Project".to_string(),
703            notes: None,
704            start_date: None,
705            deadline: None,
706            created: now,
707            modified: now,
708            area_uuid: None,
709            tags: vec![],
710            status: TaskStatus::Incomplete,
711            tasks: vec![task],
712        };
713
714        assert_eq!(project.tasks.len(), 1);
715        assert_eq!(project.tasks[0].project_uuid, Some(project_uuid));
716        assert_eq!(project.tasks[0].title, "Project Task");
717    }
718
719    #[test]
720    fn test_area_with_projects() {
721        let area_uuid = Uuid::new_v4();
722        let project_uuid = Uuid::new_v4();
723        let now = Utc::now();
724
725        let project = Project {
726            uuid: project_uuid,
727            title: "Area Project".to_string(),
728            notes: None,
729            start_date: None,
730            deadline: None,
731            created: now,
732            modified: now,
733            area_uuid: Some(area_uuid),
734            tags: vec![],
735            status: TaskStatus::Incomplete,
736            tasks: vec![],
737        };
738
739        let area = Area {
740            uuid: area_uuid,
741            title: "Test Area".to_string(),
742            notes: None,
743            created: now,
744            modified: now,
745            tags: vec![],
746            projects: vec![project],
747        };
748
749        assert_eq!(area.projects.len(), 1);
750        assert_eq!(area.projects[0].area_uuid, Some(area_uuid));
751        assert_eq!(area.projects[0].title, "Area Project");
752    }
753}