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}