things3_core/
query.rs

1//! Query builder for filtering and searching tasks
2
3use crate::models::{TaskFilters, TaskStatus, TaskType};
4use chrono::NaiveDate;
5use uuid::Uuid;
6
7/// Builder for constructing task queries with filters
8#[derive(Debug, Clone)]
9pub struct TaskQueryBuilder {
10    filters: TaskFilters,
11}
12
13impl TaskQueryBuilder {
14    /// Create a new query builder
15    #[must_use]
16    pub fn new() -> Self {
17        Self {
18            filters: TaskFilters::default(),
19        }
20    }
21
22    /// Filter by status
23    #[must_use]
24    pub fn status(mut self, status: TaskStatus) -> Self {
25        self.filters.status = Some(status);
26        self
27    }
28
29    /// Filter by task type
30    #[must_use]
31    pub fn task_type(mut self, task_type: TaskType) -> Self {
32        self.filters.task_type = Some(task_type);
33        self
34    }
35
36    /// Filter by project UUID
37    #[must_use]
38    pub fn project_uuid(mut self, project_uuid: Uuid) -> Self {
39        self.filters.project_uuid = Some(project_uuid);
40        self
41    }
42
43    /// Filter by area UUID
44    #[must_use]
45    pub fn area_uuid(mut self, area_uuid: Uuid) -> Self {
46        self.filters.area_uuid = Some(area_uuid);
47        self
48    }
49
50    /// Filter by tags
51    #[must_use]
52    pub fn tags(mut self, tags: Vec<String>) -> Self {
53        self.filters.tags = Some(tags);
54        self
55    }
56
57    /// Filter by start date range
58    #[must_use]
59    pub fn start_date_range(mut self, from: Option<NaiveDate>, to: Option<NaiveDate>) -> Self {
60        self.filters.start_date_from = from;
61        self.filters.start_date_to = to;
62        self
63    }
64
65    /// Filter by deadline range
66    #[must_use]
67    pub fn deadline_range(mut self, from: Option<NaiveDate>, to: Option<NaiveDate>) -> Self {
68        self.filters.deadline_from = from;
69        self.filters.deadline_to = to;
70        self
71    }
72
73    /// Add search query
74    #[must_use]
75    pub fn search(mut self, query: &str) -> Self {
76        self.filters.search_query = Some(query.to_string());
77        self
78    }
79
80    /// Set limit
81    #[must_use]
82    pub fn limit(mut self, limit: usize) -> Self {
83        self.filters.limit = Some(limit);
84        self
85    }
86
87    /// Set offset for pagination
88    #[must_use]
89    pub fn offset(mut self, offset: usize) -> Self {
90        self.filters.offset = Some(offset);
91        self
92    }
93
94    /// Build the final filters
95    #[must_use]
96    pub fn build(self) -> TaskFilters {
97        self.filters
98    }
99}
100
101impl Default for TaskQueryBuilder {
102    fn default() -> Self {
103        Self::new()
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110    use chrono::NaiveDate;
111    use uuid::Uuid;
112
113    #[test]
114    fn test_task_query_builder_new() {
115        let builder = TaskQueryBuilder::new();
116        let filters = builder.build();
117
118        assert!(filters.status.is_none());
119        assert!(filters.task_type.is_none());
120        assert!(filters.project_uuid.is_none());
121        assert!(filters.area_uuid.is_none());
122        assert!(filters.tags.is_none());
123        assert!(filters.start_date_from.is_none());
124        assert!(filters.start_date_to.is_none());
125        assert!(filters.deadline_from.is_none());
126        assert!(filters.deadline_to.is_none());
127        assert!(filters.search_query.is_none());
128        assert!(filters.limit.is_none());
129        assert!(filters.offset.is_none());
130    }
131
132    #[test]
133    fn test_task_query_builder_default() {
134        let builder = TaskQueryBuilder::default();
135        let filters = builder.build();
136
137        assert!(filters.status.is_none());
138        assert!(filters.task_type.is_none());
139    }
140
141    #[test]
142    fn test_task_query_builder_status() {
143        let builder = TaskQueryBuilder::new().status(TaskStatus::Completed);
144        let filters = builder.build();
145
146        assert_eq!(filters.status, Some(TaskStatus::Completed));
147    }
148
149    #[test]
150    fn test_task_query_builder_task_type() {
151        let builder = TaskQueryBuilder::new().task_type(TaskType::Project);
152        let filters = builder.build();
153
154        assert_eq!(filters.task_type, Some(TaskType::Project));
155    }
156
157    #[test]
158    fn test_task_query_builder_project_uuid() {
159        let uuid = Uuid::new_v4();
160        let builder = TaskQueryBuilder::new().project_uuid(uuid);
161        let filters = builder.build();
162
163        assert_eq!(filters.project_uuid, Some(uuid));
164    }
165
166    #[test]
167    fn test_task_query_builder_area_uuid() {
168        let uuid = Uuid::new_v4();
169        let builder = TaskQueryBuilder::new().area_uuid(uuid);
170        let filters = builder.build();
171
172        assert_eq!(filters.area_uuid, Some(uuid));
173    }
174
175    #[test]
176    fn test_task_query_builder_tags() {
177        let tags = vec!["urgent".to_string(), "important".to_string()];
178        let builder = TaskQueryBuilder::new().tags(tags.clone());
179        let filters = builder.build();
180
181        assert_eq!(filters.tags, Some(tags));
182    }
183
184    #[test]
185    fn test_task_query_builder_start_date_range() {
186        let from = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
187        let to = NaiveDate::from_ymd_opt(2024, 12, 31).unwrap();
188        let builder = TaskQueryBuilder::new().start_date_range(Some(from), Some(to));
189        let filters = builder.build();
190
191        assert_eq!(filters.start_date_from, Some(from));
192        assert_eq!(filters.start_date_to, Some(to));
193    }
194
195    #[test]
196    fn test_task_query_builder_deadline_range() {
197        let from = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
198        let to = NaiveDate::from_ymd_opt(2024, 12, 31).unwrap();
199        let builder = TaskQueryBuilder::new().deadline_range(Some(from), Some(to));
200        let filters = builder.build();
201
202        assert_eq!(filters.deadline_from, Some(from));
203        assert_eq!(filters.deadline_to, Some(to));
204    }
205
206    #[test]
207    fn test_task_query_builder_search() {
208        let query = "test search";
209        let builder = TaskQueryBuilder::new().search(query);
210        let filters = builder.build();
211
212        assert_eq!(filters.search_query, Some(query.to_string()));
213    }
214
215    #[test]
216    fn test_task_query_builder_limit() {
217        let builder = TaskQueryBuilder::new().limit(50);
218        let filters = builder.build();
219
220        assert_eq!(filters.limit, Some(50));
221    }
222
223    #[test]
224    fn test_task_query_builder_offset() {
225        let builder = TaskQueryBuilder::new().offset(10);
226        let filters = builder.build();
227
228        assert_eq!(filters.offset, Some(10));
229    }
230
231    #[test]
232    fn test_task_query_builder_chaining() {
233        let uuid = Uuid::new_v4();
234        let tags = vec!["urgent".to_string()];
235        let from = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
236        let to = NaiveDate::from_ymd_opt(2024, 12, 31).unwrap();
237
238        let builder = TaskQueryBuilder::new()
239            .status(TaskStatus::Incomplete)
240            .task_type(TaskType::Todo)
241            .project_uuid(uuid)
242            .tags(tags.clone())
243            .start_date_range(Some(from), Some(to))
244            .search("test")
245            .limit(25)
246            .offset(5);
247
248        let filters = builder.build();
249
250        assert_eq!(filters.status, Some(TaskStatus::Incomplete));
251        assert_eq!(filters.task_type, Some(TaskType::Todo));
252        assert_eq!(filters.project_uuid, Some(uuid));
253        assert_eq!(filters.tags, Some(tags));
254        assert_eq!(filters.start_date_from, Some(from));
255        assert_eq!(filters.start_date_to, Some(to));
256        assert_eq!(filters.search_query, Some("test".to_string()));
257        assert_eq!(filters.limit, Some(25));
258        assert_eq!(filters.offset, Some(5));
259    }
260}