Skip to main content

boarddown_core/
query.rs

1use boarddown_schema::{Board, Status, Task, TaskId, Metadata, ColumnRef};
2
3#[derive(Debug, Clone, Default)]
4pub struct Query {
5    pub status: Option<Vec<Status>>,
6    pub tags: Option<Vec<String>>,
7    pub assignee: Option<String>,
8    pub column: Option<String>,
9    pub due_before: Option<chrono::DateTime<chrono::Utc>>,
10    pub due_after: Option<chrono::DateTime<chrono::Utc>>,
11    pub depends_on: Option<TaskId>,
12    pub depends_on_existing: Option<bool>,
13    pub metadata: Option<Metadata>,
14    pub order_by: Option<OrderBy>,
15    pub limit: Option<usize>,
16    pub offset: Option<usize>,
17}
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum OrderBy {
21    Created,
22    Updated,
23    Due,
24    Priority,
25    Title,
26}
27
28impl Query {
29    pub fn new() -> Self {
30        Self::default()
31    }
32}
33
34pub struct QueryBuilder {
35    query: Query,
36    board: Option<Board>,
37}
38
39impl QueryBuilder {
40    pub fn new() -> Self {
41        Self {
42            query: Query::new(),
43            board: None,
44        }
45    }
46
47    pub fn with_board(mut self, board: Board) -> Self {
48        self.board = Some(board);
49        self
50    }
51
52    pub fn status(mut self, status: Status) -> Self {
53        self.query.status = Some(vec![status]);
54        self
55    }
56
57    pub fn status_in(mut self, statuses: impl IntoIterator<Item = Status>) -> Self {
58        self.query.status = Some(statuses.into_iter().collect());
59        self
60    }
61
62    pub fn tag(mut self, tag: impl Into<String>) -> Self {
63        self.query.tags.get_or_insert_with(Vec::new).push(tag.into());
64        self
65    }
66
67    pub fn tags(mut self, tags: impl IntoIterator<Item = impl Into<String>>) -> Self {
68        self.query.tags = Some(tags.into_iter().map(|t| t.into()).collect());
69        self
70    }
71
72    pub fn assignee(mut self, assignee: impl Into<String>) -> Self {
73        self.query.assignee = Some(assignee.into());
74        self
75    }
76
77    pub fn column(mut self, column: impl Into<String>) -> Self {
78        self.query.column = Some(column.into());
79        self
80    }
81
82    pub fn due_before(mut self, date: chrono::DateTime<chrono::Utc>) -> Self {
83        self.query.due_before = Some(date);
84        self
85    }
86
87    pub fn due_after(mut self, date: chrono::DateTime<chrono::Utc>) -> Self {
88        self.query.due_after = Some(date);
89        self
90    }
91
92    pub fn depends_on(mut self, task_id: TaskId) -> Self {
93        self.query.depends_on = Some(task_id);
94        self
95    }
96
97    pub fn depends_on_existing(mut self, exists: bool) -> Self {
98        self.query.depends_on_existing = Some(exists);
99        self
100    }
101
102    pub fn metadata(mut self, key: &str, value: impl Into<serde_json::Value>) -> Self {
103        let mut metadata = self.query.metadata.take().unwrap_or_default();
104        metadata = metadata.with(key, value.into());
105        self.query.metadata = Some(metadata);
106        self
107    }
108
109    pub fn order_by(mut self, order: OrderBy) -> Self {
110        self.query.order_by = Some(order);
111        self
112    }
113
114    pub fn limit(mut self, limit: usize) -> Self {
115        self.query.limit = Some(limit);
116        self
117    }
118
119    pub fn offset(mut self, offset: usize) -> Self {
120        self.query.offset = Some(offset);
121        self
122    }
123
124    pub fn build(self) -> Query {
125        self.query
126    }
127
128    pub async fn execute(self) -> Result<Vec<Task>, crate::Error> {
129        let board = self.board.ok_or_else(|| crate::Error::NotImplemented)?;
130        Ok(execute_query(&board, self.query))
131    }
132}
133
134impl Default for QueryBuilder {
135    fn default() -> Self {
136        Self::new()
137    }
138}
139
140pub fn execute_query(board: &Board, query: Query) -> Vec<Task> {
141    let mut results: Vec<Task> = board.tasks.values()
142        .filter(|task| {
143            if let Some(statuses) = &query.status {
144                if !statuses.contains(&task.status) {
145                    return false;
146                }
147            }
148            
149            if let Some(column) = &query.column {
150                match &task.column {
151                    ColumnRef::Name(name) if name == column => {}
152                    _ => return false,
153                }
154            }
155            
156            if let Some(meta) = &query.metadata {
157                for (key, value) in meta.iter() {
158                    if task.metadata.get(&key) != Some(value.clone()) {
159                        return false;
160                    }
161                }
162            }
163            
164            if let Some(assignee) = &query.assignee {
165                if task.metadata.assign.as_ref() != Some(assignee) {
166                    return false;
167                }
168            }
169            
170            if let Some(tags) = &query.tags {
171                for tag in tags {
172                    if !task.metadata.tags.contains(tag) {
173                        return false;
174                    }
175                }
176            }
177            
178            if let Some(dep_id) = &query.depends_on {
179                if !task.dependencies.contains(dep_id) {
180                    return false;
181                }
182            }
183            
184            if let Some(due_before) = &query.due_before {
185                if let Some(due) = &task.metadata.due {
186                    if due >= due_before {
187                        return false;
188                    }
189                } else {
190                    return false;
191                }
192            }
193            
194            if let Some(due_after) = &query.due_after {
195                if let Some(due) = &task.metadata.due {
196                    if due <= due_after {
197                        return false;
198                    }
199                } else {
200                    return false;
201                }
202            }
203            
204            true
205        })
206        .cloned()
207        .collect();
208    
209    if let Some(order) = query.order_by {
210        match order {
211            OrderBy::Created => results.sort_by_key(|t| t.created_at),
212            OrderBy::Updated => results.sort_by_key(|t| t.updated_at),
213            OrderBy::Title => results.sort_by(|a, b| a.title.cmp(&b.title)),
214            OrderBy::Priority => results.sort_by(|a, b| {
215                let pa: u8 = a.metadata.priority.map(|p| p as u8).unwrap_or(0);
216                let pb: u8 = b.metadata.priority.map(|p| p as u8).unwrap_or(0);
217                pa.cmp(&pb)
218            }),
219            OrderBy::Due => results.sort_by_key(|t| t.metadata.due),
220        }
221    }
222    
223    if let Some(offset) = query.offset {
224        results = results.into_iter().skip(offset).collect();
225    }
226    
227    if let Some(limit) = query.limit {
228        results = results.into_iter().take(limit).collect();
229    }
230    
231    results
232}