Skip to main content

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/// How to handle child tasks when deleting a parent
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
35pub enum DeleteChildHandling {
36    /// Return error if task has children (default)
37    #[serde(rename = "error")]
38    Error,
39    /// Delete parent and all children
40    #[serde(rename = "cascade")]
41    Cascade,
42    /// Delete parent only, orphan children
43    #[serde(rename = "orphan")]
44    Orphan,
45}
46
47/// Main task entity
48#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct Task {
50    /// Unique identifier
51    pub uuid: Uuid,
52    /// Task title
53    pub title: String,
54    /// Task type
55    pub task_type: TaskType,
56    /// Task status
57    pub status: TaskStatus,
58    /// Optional notes
59    pub notes: Option<String>,
60    /// Start date
61    pub start_date: Option<NaiveDate>,
62    /// Deadline
63    pub deadline: Option<NaiveDate>,
64    /// Creation timestamp
65    pub created: DateTime<Utc>,
66    /// Last modification timestamp
67    pub modified: DateTime<Utc>,
68    /// Completion timestamp (when status changed to completed)
69    pub stop_date: Option<DateTime<Utc>>,
70    /// Parent project UUID
71    pub project_uuid: Option<Uuid>,
72    /// Parent area UUID
73    pub area_uuid: Option<Uuid>,
74    /// Parent task UUID
75    pub parent_uuid: Option<Uuid>,
76    /// Associated tags
77    pub tags: Vec<String>,
78    /// Child tasks
79    pub children: Vec<Task>,
80}
81
82/// Project entity
83#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct Project {
85    /// Unique identifier
86    pub uuid: Uuid,
87    /// Project title
88    pub title: String,
89    /// Optional notes
90    pub notes: Option<String>,
91    /// Start date
92    pub start_date: Option<NaiveDate>,
93    /// Deadline
94    pub deadline: Option<NaiveDate>,
95    /// Creation timestamp
96    pub created: DateTime<Utc>,
97    /// Last modification timestamp
98    pub modified: DateTime<Utc>,
99    /// Parent area UUID
100    pub area_uuid: Option<Uuid>,
101    /// Associated tags
102    pub tags: Vec<String>,
103    /// Project status
104    pub status: TaskStatus,
105    /// Child tasks
106    pub tasks: Vec<Task>,
107}
108
109/// Area entity
110#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct Area {
112    /// Unique identifier
113    pub uuid: Uuid,
114    /// Area title
115    pub title: String,
116    /// Optional notes
117    pub notes: Option<String>,
118    /// Creation timestamp
119    pub created: DateTime<Utc>,
120    /// Last modification timestamp
121    pub modified: DateTime<Utc>,
122    /// Associated tags
123    pub tags: Vec<String>,
124    /// Child projects
125    pub projects: Vec<Project>,
126}
127
128/// Tag entity (enhanced with duplicate prevention support)
129#[derive(Debug, Clone, Serialize, Deserialize)]
130pub struct Tag {
131    /// Unique identifier
132    pub uuid: Uuid,
133    /// Tag title (display form, preserves case)
134    pub title: String,
135    /// Keyboard shortcut
136    pub shortcut: Option<String>,
137    /// Parent tag UUID (for nested tags)
138    pub parent_uuid: Option<Uuid>,
139    /// Creation timestamp
140    pub created: DateTime<Utc>,
141    /// Last modification timestamp
142    pub modified: DateTime<Utc>,
143    /// How many tasks use this tag
144    pub usage_count: u32,
145    /// Last time this tag was used
146    pub last_used: Option<DateTime<Utc>>,
147}
148
149/// Tag creation request
150#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct CreateTagRequest {
152    /// Tag title (required)
153    pub title: String,
154    /// Keyboard shortcut
155    pub shortcut: Option<String>,
156    /// Parent tag UUID (for nested tags)
157    pub parent_uuid: Option<Uuid>,
158}
159
160/// Tag update request
161#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct UpdateTagRequest {
163    /// Tag UUID
164    pub uuid: Uuid,
165    /// New title
166    pub title: Option<String>,
167    /// New shortcut
168    pub shortcut: Option<String>,
169    /// New parent UUID
170    pub parent_uuid: Option<Uuid>,
171}
172
173/// Tag match type classification
174#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
175pub enum TagMatchType {
176    /// Exact match (case-insensitive)
177    #[serde(rename = "exact")]
178    Exact,
179    /// Same text, different case
180    #[serde(rename = "case_mismatch")]
181    CaseMismatch,
182    /// Fuzzy match (high similarity via Levenshtein distance)
183    #[serde(rename = "similar")]
184    Similar,
185    /// Substring/contains match
186    #[serde(rename = "partial")]
187    PartialMatch,
188}
189
190/// Tag search result with similarity scoring
191#[derive(Debug, Clone, Serialize, Deserialize)]
192pub struct TagMatch {
193    /// The matched tag
194    pub tag: Tag,
195    /// Similarity score (0.0 to 1.0, higher is better)
196    pub similarity_score: f32,
197    /// Type of match
198    pub match_type: TagMatchType,
199}
200
201/// Result of tag creation with duplicate checking
202#[derive(Debug, Clone, Serialize, Deserialize)]
203pub enum TagCreationResult {
204    /// New tag was created
205    #[serde(rename = "created")]
206    Created {
207        /// UUID of created tag
208        uuid: Uuid,
209        /// Always true for this variant
210        is_new: bool,
211    },
212    /// Existing exact match found
213    #[serde(rename = "existing")]
214    Existing {
215        /// The existing tag
216        tag: Tag,
217        /// Always false for this variant
218        is_new: bool,
219    },
220    /// Similar tags found (user decision needed)
221    #[serde(rename = "similar_found")]
222    SimilarFound {
223        /// Tags similar to requested title
224        similar_tags: Vec<TagMatch>,
225        /// The title user requested
226        requested_title: String,
227    },
228}
229
230/// Result of tag assignment to task
231#[derive(Debug, Clone, Serialize, Deserialize)]
232pub enum TagAssignmentResult {
233    /// Tag assigned successfully
234    #[serde(rename = "assigned")]
235    Assigned {
236        /// UUID of the tag that was assigned
237        tag_uuid: Uuid,
238    },
239    /// Similar tags found (user decision needed)
240    #[serde(rename = "suggestions")]
241    Suggestions {
242        /// Suggested alternative tags
243        similar_tags: Vec<TagMatch>,
244    },
245}
246
247/// Tag auto-completion suggestion
248#[derive(Debug, Clone, Serialize, Deserialize)]
249pub struct TagCompletion {
250    /// The tag
251    pub tag: Tag,
252    /// Priority score (based on usage, recency, match quality)
253    pub score: f32,
254}
255
256/// Tag statistics for analytics
257#[derive(Debug, Clone, Serialize, Deserialize)]
258pub struct TagStatistics {
259    /// Tag UUID
260    pub uuid: Uuid,
261    /// Tag title
262    pub title: String,
263    /// Total usage count
264    pub usage_count: u32,
265    /// Task UUIDs using this tag
266    pub task_uuids: Vec<Uuid>,
267    /// Related tags (frequently used together)
268    pub related_tags: Vec<(String, u32)>, // (tag_title, co_occurrence_count)
269}
270
271/// Pair of similar tags (for duplicate detection)
272#[derive(Debug, Clone, Serialize, Deserialize)]
273pub struct TagPair {
274    /// First tag
275    pub tag1: Tag,
276    /// Second tag
277    pub tag2: Tag,
278    /// Similarity score between them
279    pub similarity: f32,
280}
281
282/// Task creation request
283#[derive(Debug, Clone, Serialize, Deserialize)]
284pub struct CreateTaskRequest {
285    /// Task title (required)
286    pub title: String,
287    /// Task type (defaults to Todo)
288    pub task_type: Option<TaskType>,
289    /// Optional notes
290    pub notes: Option<String>,
291    /// Start date
292    pub start_date: Option<NaiveDate>,
293    /// Deadline
294    pub deadline: Option<NaiveDate>,
295    /// Project UUID (validated if provided)
296    pub project_uuid: Option<Uuid>,
297    /// Area UUID (validated if provided)
298    pub area_uuid: Option<Uuid>,
299    /// Parent task UUID (for subtasks)
300    pub parent_uuid: Option<Uuid>,
301    /// Tags (as string names)
302    pub tags: Option<Vec<String>>,
303    /// Initial status (defaults to Incomplete)
304    pub status: Option<TaskStatus>,
305}
306
307/// Task update request
308#[derive(Debug, Clone, Serialize, Deserialize)]
309pub struct UpdateTaskRequest {
310    /// Task UUID
311    pub uuid: Uuid,
312    /// New title
313    pub title: Option<String>,
314    /// New notes
315    pub notes: Option<String>,
316    /// New start date
317    pub start_date: Option<NaiveDate>,
318    /// New deadline
319    pub deadline: Option<NaiveDate>,
320    /// New status
321    pub status: Option<TaskStatus>,
322    /// New project UUID
323    pub project_uuid: Option<Uuid>,
324    /// New area UUID
325    pub area_uuid: Option<Uuid>,
326    /// New tags
327    pub tags: Option<Vec<String>>,
328}
329
330/// Task filters for queries
331#[derive(Debug, Clone, Serialize, Deserialize, Default)]
332pub struct TaskFilters {
333    /// Filter by status
334    pub status: Option<TaskStatus>,
335    /// Filter by task type
336    pub task_type: Option<TaskType>,
337    /// Filter by project UUID
338    pub project_uuid: Option<Uuid>,
339    /// Filter by area UUID
340    pub area_uuid: Option<Uuid>,
341    /// Filter by tags (AND semantics — task must contain every listed tag).
342    pub tags: Option<Vec<String>>,
343    /// Filter by start date range
344    pub start_date_from: Option<NaiveDate>,
345    pub start_date_to: Option<NaiveDate>,
346    /// Filter by deadline range
347    pub deadline_from: Option<NaiveDate>,
348    pub deadline_to: Option<NaiveDate>,
349    /// Search query
350    pub search_query: Option<String>,
351    /// Limit results
352    pub limit: Option<usize>,
353    /// Offset for pagination
354    pub offset: Option<usize>,
355}
356
357/// A task paired with its fuzzy-match relevance score.
358///
359/// Returned by [`crate::query::TaskQueryBuilder::execute_ranked`].
360///
361/// Requires the `advanced-queries` feature flag.
362#[cfg(feature = "advanced-queries")]
363#[derive(Debug, Clone, Serialize, Deserialize)]
364pub struct RankedTask {
365    /// The matched task.
366    pub task: Task,
367    /// Relevance score in `[0.0, 1.0]`; higher is a better match.
368    pub score: f32,
369}
370
371/// Project creation request
372#[derive(Debug, Clone, Serialize, Deserialize)]
373pub struct CreateProjectRequest {
374    /// Project title (required)
375    pub title: String,
376    /// Optional notes
377    pub notes: Option<String>,
378    /// Area UUID (validated if provided)
379    pub area_uuid: Option<Uuid>,
380    /// Start date
381    pub start_date: Option<NaiveDate>,
382    /// Deadline
383    pub deadline: Option<NaiveDate>,
384    /// Tags (as string names)
385    pub tags: Option<Vec<String>>,
386}
387
388/// Project update request
389#[derive(Debug, Clone, Serialize, Deserialize)]
390pub struct UpdateProjectRequest {
391    /// Project UUID
392    pub uuid: Uuid,
393    /// New title
394    pub title: Option<String>,
395    /// New notes
396    pub notes: Option<String>,
397    /// New area UUID
398    pub area_uuid: Option<Uuid>,
399    /// New start date
400    pub start_date: Option<NaiveDate>,
401    /// New deadline
402    pub deadline: Option<NaiveDate>,
403    /// New tags
404    pub tags: Option<Vec<String>>,
405}
406
407/// Area creation request
408#[derive(Debug, Clone, Serialize, Deserialize)]
409pub struct CreateAreaRequest {
410    /// Area title (required)
411    pub title: String,
412}
413
414/// Area update request
415#[derive(Debug, Clone, Serialize, Deserialize)]
416pub struct UpdateAreaRequest {
417    /// Area UUID
418    pub uuid: Uuid,
419    /// New title
420    pub title: String,
421}
422
423/// How to handle child tasks when completing/deleting a project
424#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
425pub enum ProjectChildHandling {
426    /// Return error if project has child tasks (default, safest)
427    #[serde(rename = "error")]
428    #[default]
429    Error,
430    /// Complete/delete all child tasks
431    #[serde(rename = "cascade")]
432    Cascade,
433    /// Move child tasks to inbox (orphan them)
434    #[serde(rename = "orphan")]
435    Orphan,
436}
437
438// ============================================================================
439// Bulk Operation Models
440// ============================================================================
441
442/// Request to move multiple tasks to a project or area
443#[derive(Debug, Clone, Serialize, Deserialize)]
444pub struct BulkMoveRequest {
445    /// Task UUIDs to move
446    pub task_uuids: Vec<Uuid>,
447    /// Target project UUID (optional)
448    pub project_uuid: Option<Uuid>,
449    /// Target area UUID (optional)
450    pub area_uuid: Option<Uuid>,
451}
452
453/// Request to update dates for multiple tasks
454#[derive(Debug, Clone, Serialize, Deserialize)]
455pub struct BulkUpdateDatesRequest {
456    /// Task UUIDs to update
457    pub task_uuids: Vec<Uuid>,
458    /// New start date (None means don't update)
459    pub start_date: Option<NaiveDate>,
460    /// New deadline (None means don't update)
461    pub deadline: Option<NaiveDate>,
462    /// Clear start date (set to NULL)
463    pub clear_start_date: bool,
464    /// Clear deadline (set to NULL)
465    pub clear_deadline: bool,
466}
467
468/// Request to complete multiple tasks
469#[derive(Debug, Clone, Serialize, Deserialize)]
470pub struct BulkCompleteRequest {
471    /// Task UUIDs to complete
472    pub task_uuids: Vec<Uuid>,
473}
474
475/// Request to delete multiple tasks
476#[derive(Debug, Clone, Serialize, Deserialize)]
477pub struct BulkDeleteRequest {
478    /// Task UUIDs to delete
479    pub task_uuids: Vec<Uuid>,
480}
481
482/// Result of a bulk operation
483#[derive(Debug, Clone, Serialize, Deserialize)]
484pub struct BulkOperationResult {
485    /// Whether the operation succeeded
486    pub success: bool,
487    /// Number of tasks processed
488    pub processed_count: usize,
489    /// Result message
490    pub message: String,
491}
492
493#[cfg(test)]
494mod tests {
495    use super::*;
496    use chrono::NaiveDate;
497
498    #[test]
499    fn test_task_status_serialization() {
500        let status = TaskStatus::Incomplete;
501        let serialized = serde_json::to_string(&status).unwrap();
502        assert_eq!(serialized, "\"incomplete\"");
503
504        let status = TaskStatus::Completed;
505        let serialized = serde_json::to_string(&status).unwrap();
506        assert_eq!(serialized, "\"completed\"");
507
508        let status = TaskStatus::Canceled;
509        let serialized = serde_json::to_string(&status).unwrap();
510        assert_eq!(serialized, "\"canceled\"");
511
512        let status = TaskStatus::Trashed;
513        let serialized = serde_json::to_string(&status).unwrap();
514        assert_eq!(serialized, "\"trashed\"");
515    }
516
517    #[test]
518    fn test_task_status_deserialization() {
519        let deserialized: TaskStatus = serde_json::from_str("\"incomplete\"").unwrap();
520        assert_eq!(deserialized, TaskStatus::Incomplete);
521
522        let deserialized: TaskStatus = serde_json::from_str("\"completed\"").unwrap();
523        assert_eq!(deserialized, TaskStatus::Completed);
524
525        let deserialized: TaskStatus = serde_json::from_str("\"canceled\"").unwrap();
526        assert_eq!(deserialized, TaskStatus::Canceled);
527
528        let deserialized: TaskStatus = serde_json::from_str("\"trashed\"").unwrap();
529        assert_eq!(deserialized, TaskStatus::Trashed);
530    }
531
532    #[test]
533    fn test_task_type_serialization() {
534        let task_type = TaskType::Todo;
535        let serialized = serde_json::to_string(&task_type).unwrap();
536        assert_eq!(serialized, "\"to-do\"");
537
538        let task_type = TaskType::Project;
539        let serialized = serde_json::to_string(&task_type).unwrap();
540        assert_eq!(serialized, "\"project\"");
541
542        let task_type = TaskType::Heading;
543        let serialized = serde_json::to_string(&task_type).unwrap();
544        assert_eq!(serialized, "\"heading\"");
545
546        let task_type = TaskType::Area;
547        let serialized = serde_json::to_string(&task_type).unwrap();
548        assert_eq!(serialized, "\"area\"");
549    }
550
551    #[test]
552    fn test_task_type_deserialization() {
553        let deserialized: TaskType = serde_json::from_str("\"to-do\"").unwrap();
554        assert_eq!(deserialized, TaskType::Todo);
555
556        let deserialized: TaskType = serde_json::from_str("\"project\"").unwrap();
557        assert_eq!(deserialized, TaskType::Project);
558
559        let deserialized: TaskType = serde_json::from_str("\"heading\"").unwrap();
560        assert_eq!(deserialized, TaskType::Heading);
561
562        let deserialized: TaskType = serde_json::from_str("\"area\"").unwrap();
563        assert_eq!(deserialized, TaskType::Area);
564    }
565
566    #[test]
567    fn test_task_creation() {
568        let uuid = Uuid::new_v4();
569        let now = Utc::now();
570        let start_date = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
571        let deadline = NaiveDate::from_ymd_opt(2024, 12, 31).unwrap();
572
573        let task = Task {
574            uuid,
575            title: "Test Task".to_string(),
576            task_type: TaskType::Todo,
577            status: TaskStatus::Incomplete,
578            notes: Some("Test notes".to_string()),
579            start_date: Some(start_date),
580            deadline: Some(deadline),
581            created: now,
582            modified: now,
583            stop_date: None,
584            project_uuid: None,
585            area_uuid: None,
586            parent_uuid: None,
587            tags: vec!["work".to_string(), "urgent".to_string()],
588            children: vec![],
589        };
590
591        assert_eq!(task.uuid, uuid);
592        assert_eq!(task.title, "Test Task");
593        assert_eq!(task.task_type, TaskType::Todo);
594        assert_eq!(task.status, TaskStatus::Incomplete);
595        assert_eq!(task.notes, Some("Test notes".to_string()));
596        assert_eq!(task.start_date, Some(start_date));
597        assert_eq!(task.deadline, Some(deadline));
598        assert_eq!(task.tags.len(), 2);
599        assert!(task.tags.contains(&"work".to_string()));
600        assert!(task.tags.contains(&"urgent".to_string()));
601    }
602
603    #[test]
604    fn test_task_serialization() {
605        let uuid = Uuid::new_v4();
606        let now = Utc::now();
607
608        let task = Task {
609            uuid,
610            title: "Test Task".to_string(),
611            task_type: TaskType::Todo,
612            status: TaskStatus::Incomplete,
613            notes: None,
614            start_date: None,
615            deadline: None,
616            created: now,
617            modified: now,
618            stop_date: None,
619            project_uuid: None,
620            area_uuid: None,
621            parent_uuid: None,
622            tags: vec![],
623            children: vec![],
624        };
625
626        let serialized = serde_json::to_string(&task).unwrap();
627        let deserialized: Task = serde_json::from_str(&serialized).unwrap();
628
629        assert_eq!(deserialized.uuid, task.uuid);
630        assert_eq!(deserialized.title, task.title);
631        assert_eq!(deserialized.task_type, task.task_type);
632        assert_eq!(deserialized.status, task.status);
633    }
634
635    #[test]
636    fn test_project_creation() {
637        let uuid = Uuid::new_v4();
638        let area_uuid = Uuid::new_v4();
639        let now = Utc::now();
640
641        let project = Project {
642            uuid,
643            title: "Test Project".to_string(),
644            notes: Some("Project notes".to_string()),
645            start_date: None,
646            deadline: None,
647            created: now,
648            modified: now,
649            area_uuid: Some(area_uuid),
650            tags: vec!["project".to_string()],
651            status: TaskStatus::Incomplete,
652            tasks: vec![],
653        };
654
655        assert_eq!(project.uuid, uuid);
656        assert_eq!(project.title, "Test Project");
657        assert_eq!(project.area_uuid, Some(area_uuid));
658        assert_eq!(project.status, TaskStatus::Incomplete);
659        assert_eq!(project.tags.len(), 1);
660    }
661
662    #[test]
663    fn test_project_serialization() {
664        let uuid = Uuid::new_v4();
665        let now = Utc::now();
666
667        let project = Project {
668            uuid,
669            title: "Test Project".to_string(),
670            notes: None,
671            start_date: None,
672            deadline: None,
673            created: now,
674            modified: now,
675            area_uuid: None,
676            tags: vec![],
677            status: TaskStatus::Incomplete,
678            tasks: vec![],
679        };
680
681        let serialized = serde_json::to_string(&project).unwrap();
682        let deserialized: Project = serde_json::from_str(&serialized).unwrap();
683
684        assert_eq!(deserialized.uuid, project.uuid);
685        assert_eq!(deserialized.title, project.title);
686        assert_eq!(deserialized.status, project.status);
687    }
688
689    #[test]
690    fn test_area_creation() {
691        let uuid = Uuid::new_v4();
692        let now = Utc::now();
693
694        let area = Area {
695            uuid,
696            title: "Test Area".to_string(),
697            notes: Some("Area notes".to_string()),
698            created: now,
699            modified: now,
700            tags: vec!["area".to_string()],
701            projects: vec![],
702        };
703
704        assert_eq!(area.uuid, uuid);
705        assert_eq!(area.title, "Test Area");
706        assert_eq!(area.notes, Some("Area notes".to_string()));
707        assert_eq!(area.tags.len(), 1);
708    }
709
710    #[test]
711    fn test_area_serialization() {
712        let uuid = Uuid::new_v4();
713        let now = Utc::now();
714
715        let area = Area {
716            uuid,
717            title: "Test Area".to_string(),
718            notes: None,
719            created: now,
720            modified: now,
721            tags: vec![],
722            projects: vec![],
723        };
724
725        let serialized = serde_json::to_string(&area).unwrap();
726        let deserialized: Area = serde_json::from_str(&serialized).unwrap();
727
728        assert_eq!(deserialized.uuid, area.uuid);
729        assert_eq!(deserialized.title, area.title);
730    }
731
732    #[test]
733    fn test_tag_creation() {
734        let uuid = Uuid::new_v4();
735        let parent_uuid = Uuid::new_v4();
736        let now = Utc::now();
737
738        let tag = Tag {
739            uuid,
740            title: "work".to_string(),
741            shortcut: Some("w".to_string()),
742            parent_uuid: Some(parent_uuid),
743            created: now,
744            modified: now,
745            usage_count: 5,
746            last_used: Some(now),
747        };
748
749        assert_eq!(tag.uuid, uuid);
750        assert_eq!(tag.title, "work");
751        assert_eq!(tag.shortcut, Some("w".to_string()));
752        assert_eq!(tag.parent_uuid, Some(parent_uuid));
753        assert_eq!(tag.usage_count, 5);
754        assert_eq!(tag.last_used, Some(now));
755    }
756
757    #[test]
758    fn test_tag_serialization() {
759        let uuid = Uuid::new_v4();
760        let now = Utc::now();
761
762        let tag = Tag {
763            uuid,
764            title: "test".to_string(),
765            shortcut: None,
766            parent_uuid: None,
767            created: now,
768            modified: now,
769            usage_count: 0,
770            last_used: None,
771        };
772
773        let serialized = serde_json::to_string(&tag).unwrap();
774        let deserialized: Tag = serde_json::from_str(&serialized).unwrap();
775
776        assert_eq!(deserialized.uuid, tag.uuid);
777        assert_eq!(deserialized.title, tag.title);
778        assert_eq!(deserialized.usage_count, tag.usage_count);
779    }
780
781    #[test]
782    fn test_create_task_request() {
783        let project_uuid = Uuid::new_v4();
784        let area_uuid = Uuid::new_v4();
785        let start_date = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
786
787        let request = CreateTaskRequest {
788            title: "New Task".to_string(),
789            task_type: None,
790            notes: Some("Task notes".to_string()),
791            start_date: Some(start_date),
792            deadline: None,
793            project_uuid: Some(project_uuid),
794            area_uuid: Some(area_uuid),
795            parent_uuid: None,
796            tags: Some(vec!["new".to_string()]),
797            status: None,
798        };
799
800        assert_eq!(request.title, "New Task");
801        assert_eq!(request.project_uuid, Some(project_uuid));
802        assert_eq!(request.area_uuid, Some(area_uuid));
803        assert_eq!(request.start_date, Some(start_date));
804        assert_eq!(request.tags.as_ref().unwrap().len(), 1);
805    }
806
807    #[test]
808    fn test_create_task_request_serialization() {
809        let request = CreateTaskRequest {
810            title: "Test".to_string(),
811            task_type: None,
812            notes: None,
813            start_date: None,
814            deadline: None,
815            project_uuid: None,
816            area_uuid: None,
817            parent_uuid: None,
818            tags: None,
819            status: None,
820        };
821
822        let serialized = serde_json::to_string(&request).unwrap();
823        let deserialized: CreateTaskRequest = serde_json::from_str(&serialized).unwrap();
824
825        assert_eq!(deserialized.title, request.title);
826    }
827
828    #[test]
829    fn test_update_task_request() {
830        let uuid = Uuid::new_v4();
831
832        let request = UpdateTaskRequest {
833            uuid,
834            title: Some("Updated Title".to_string()),
835            notes: Some("Updated notes".to_string()),
836            start_date: None,
837            deadline: None,
838            status: Some(TaskStatus::Completed),
839            project_uuid: None,
840            area_uuid: None,
841            tags: Some(vec!["updated".to_string()]),
842        };
843
844        assert_eq!(request.uuid, uuid);
845        assert_eq!(request.title, Some("Updated Title".to_string()));
846        assert_eq!(request.status, Some(TaskStatus::Completed));
847        assert_eq!(request.tags, Some(vec!["updated".to_string()]));
848    }
849
850    #[test]
851    fn test_update_task_request_serialization() {
852        let uuid = Uuid::new_v4();
853
854        let request = UpdateTaskRequest {
855            uuid,
856            title: None,
857            notes: None,
858            start_date: None,
859            deadline: None,
860            status: None,
861            project_uuid: None,
862            area_uuid: None,
863            tags: None,
864        };
865
866        let serialized = serde_json::to_string(&request).unwrap();
867        let deserialized: UpdateTaskRequest = serde_json::from_str(&serialized).unwrap();
868
869        assert_eq!(deserialized.uuid, request.uuid);
870    }
871
872    #[test]
873    fn test_task_filters_default() {
874        let filters = TaskFilters::default();
875
876        assert!(filters.status.is_none());
877        assert!(filters.task_type.is_none());
878        assert!(filters.project_uuid.is_none());
879        assert!(filters.area_uuid.is_none());
880        assert!(filters.tags.is_none());
881        assert!(filters.start_date_from.is_none());
882        assert!(filters.start_date_to.is_none());
883        assert!(filters.deadline_from.is_none());
884        assert!(filters.deadline_to.is_none());
885        assert!(filters.search_query.is_none());
886        assert!(filters.limit.is_none());
887        assert!(filters.offset.is_none());
888    }
889
890    #[test]
891    fn test_task_filters_creation() {
892        let project_uuid = Uuid::new_v4();
893        let start_date = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
894
895        let filters = TaskFilters {
896            status: Some(TaskStatus::Incomplete),
897            task_type: Some(TaskType::Todo),
898            project_uuid: Some(project_uuid),
899            area_uuid: None,
900            tags: Some(vec!["work".to_string()]),
901            start_date_from: Some(start_date),
902            start_date_to: None,
903            deadline_from: None,
904            deadline_to: None,
905            search_query: Some("test".to_string()),
906            limit: Some(10),
907            offset: Some(0),
908        };
909
910        assert_eq!(filters.status, Some(TaskStatus::Incomplete));
911        assert_eq!(filters.task_type, Some(TaskType::Todo));
912        assert_eq!(filters.project_uuid, Some(project_uuid));
913        assert_eq!(filters.search_query, Some("test".to_string()));
914        assert_eq!(filters.limit, Some(10));
915        assert_eq!(filters.offset, Some(0));
916    }
917
918    #[test]
919    fn test_task_filters_serialization() {
920        let filters = TaskFilters {
921            status: Some(TaskStatus::Completed),
922            task_type: Some(TaskType::Project),
923            project_uuid: None,
924            area_uuid: None,
925            tags: None,
926            start_date_from: None,
927            start_date_to: None,
928            deadline_from: None,
929            deadline_to: None,
930            search_query: None,
931            limit: None,
932            offset: None,
933        };
934
935        let serialized = serde_json::to_string(&filters).unwrap();
936        let deserialized: TaskFilters = serde_json::from_str(&serialized).unwrap();
937
938        assert_eq!(deserialized.status, filters.status);
939        assert_eq!(deserialized.task_type, filters.task_type);
940    }
941
942    #[test]
943    fn test_task_status_equality() {
944        assert_eq!(TaskStatus::Incomplete, TaskStatus::Incomplete);
945        assert_ne!(TaskStatus::Incomplete, TaskStatus::Completed);
946        assert_ne!(TaskStatus::Completed, TaskStatus::Canceled);
947        assert_ne!(TaskStatus::Canceled, TaskStatus::Trashed);
948    }
949
950    #[test]
951    fn test_task_type_equality() {
952        assert_eq!(TaskType::Todo, TaskType::Todo);
953        assert_ne!(TaskType::Todo, TaskType::Project);
954        assert_ne!(TaskType::Project, TaskType::Heading);
955        assert_ne!(TaskType::Heading, TaskType::Area);
956    }
957
958    #[test]
959    fn test_task_with_children() {
960        let parent_uuid = Uuid::new_v4();
961        let child_uuid = Uuid::new_v4();
962        let now = Utc::now();
963
964        let child_task = Task {
965            uuid: child_uuid,
966            title: "Child Task".to_string(),
967            task_type: TaskType::Todo,
968            status: TaskStatus::Incomplete,
969            notes: None,
970            start_date: None,
971            deadline: None,
972            created: now,
973            modified: now,
974            stop_date: None,
975            project_uuid: None,
976            area_uuid: None,
977            parent_uuid: Some(parent_uuid),
978            tags: vec![],
979            children: vec![],
980        };
981
982        let parent_task = Task {
983            uuid: parent_uuid,
984            title: "Parent Task".to_string(),
985            task_type: TaskType::Heading,
986            status: TaskStatus::Incomplete,
987            notes: None,
988            start_date: None,
989            deadline: None,
990            created: now,
991            modified: now,
992            stop_date: None,
993            project_uuid: None,
994            area_uuid: None,
995            parent_uuid: None,
996            tags: vec![],
997            children: vec![child_task],
998        };
999
1000        assert_eq!(parent_task.children.len(), 1);
1001        assert_eq!(parent_task.children[0].parent_uuid, Some(parent_uuid));
1002        assert_eq!(parent_task.children[0].title, "Child Task");
1003    }
1004
1005    #[test]
1006    fn test_project_with_tasks() {
1007        let project_uuid = Uuid::new_v4();
1008        let task_uuid = Uuid::new_v4();
1009        let now = Utc::now();
1010
1011        let task = Task {
1012            uuid: task_uuid,
1013            title: "Project Task".to_string(),
1014            task_type: TaskType::Todo,
1015            status: TaskStatus::Incomplete,
1016            notes: None,
1017            start_date: None,
1018            deadline: None,
1019            created: now,
1020            modified: now,
1021            stop_date: None,
1022            project_uuid: Some(project_uuid),
1023            area_uuid: None,
1024            parent_uuid: None,
1025            tags: vec![],
1026            children: vec![],
1027        };
1028
1029        let project = Project {
1030            uuid: project_uuid,
1031            title: "Test Project".to_string(),
1032            notes: None,
1033            start_date: None,
1034            deadline: None,
1035            created: now,
1036            modified: now,
1037            area_uuid: None,
1038            tags: vec![],
1039            status: TaskStatus::Incomplete,
1040            tasks: vec![task],
1041        };
1042
1043        assert_eq!(project.tasks.len(), 1);
1044        assert_eq!(project.tasks[0].project_uuid, Some(project_uuid));
1045        assert_eq!(project.tasks[0].title, "Project Task");
1046    }
1047
1048    #[test]
1049    fn test_area_with_projects() {
1050        let area_uuid = Uuid::new_v4();
1051        let project_uuid = Uuid::new_v4();
1052        let now = Utc::now();
1053
1054        let project = Project {
1055            uuid: project_uuid,
1056            title: "Area Project".to_string(),
1057            notes: None,
1058            start_date: None,
1059            deadline: None,
1060            created: now,
1061            modified: now,
1062            area_uuid: Some(area_uuid),
1063            tags: vec![],
1064            status: TaskStatus::Incomplete,
1065            tasks: vec![],
1066        };
1067
1068        let area = Area {
1069            uuid: area_uuid,
1070            title: "Test Area".to_string(),
1071            notes: None,
1072            created: now,
1073            modified: now,
1074            tags: vec![],
1075            projects: vec![project],
1076        };
1077
1078        assert_eq!(area.projects.len(), 1);
1079        assert_eq!(area.projects[0].area_uuid, Some(area_uuid));
1080        assert_eq!(area.projects[0].title, "Area Project");
1081    }
1082}