phabricator_api/maniphest/
search.rs1use crate::types::Phid;
2use crate::utils::{deserialize_timestamp, deserialize_timestamp_option};
3use crate::ApiRequest;
4use chrono::DateTime;
5use chrono::Utc;
6use rust_decimal::prelude::*;
7use serde::de::Deserializer;
8use serde::Deserialize;
9use serde::Serialize;
10use serde_json::value::Value as JsonValue;
11use std::collections::HashMap;
12use std::default::Default;
13use std::ops::Not;
14
15fn deserialize_raw_string<'de, D>(d: D) -> Result<String, D::Error>
16where
17 D: Deserializer<'de>,
18{
19 #[derive(Deserialize)]
20 struct Raw {
21 raw: String,
22 }
23 let r = Raw::deserialize(d)?;
24 Ok(r.raw)
25}
26
27pub trait Serializable: erased_serde::Serialize + std::fmt::Debug {}
28impl<T: erased_serde::Serialize + std::fmt::Debug> Serializable for T {}
29erased_serde::serialize_trait_object!(Serializable);
30
31#[derive(Serialize, Debug, Default)]
32pub struct Constraints {
33 pub ids: Option<Vec<u32>>,
34 pub phids: Option<Vec<Phid>>,
35 pub query: Option<String>,
36 pub projects: Option<Vec<String>>,
37 #[serde(flatten)]
38 pub custom: Option<HashMap<String, Box<dyn Serializable + Send + Sync>>>,
39}
40
41#[derive(Serialize, Debug, Default)]
42pub struct Attachments {
43 #[serde(skip_serializing_if = "<&bool>::not")]
44 pub subscribers: bool,
45 #[serde(skip_serializing_if = "<&bool>::not")]
46 pub columns: bool,
47 #[serde(skip_serializing_if = "<&bool>::not")]
48 pub projects: bool,
49}
50
51pub type Search = crate::types::Search<Attachments, Constraints>;
52pub type SearchCursor<'a> = crate::types::SearchCursor<'a, Attachments, Constraints>;
53
54pub type SearchData = crate::types::SearchData<AttachmentsResult, Fields>;
55pub type SearchResult = crate::types::SearchResult<AttachmentsResult, Fields>;
56
57#[derive(Deserialize, Debug)]
58pub struct Status {
59 pub value: String,
60 pub name: String,
61 pub color: Option<String>,
62}
63
64#[derive(Deserialize, Debug)]
65pub struct Priority {
66 pub value: u32,
67 pub name: String,
68 pub color: String,
69}
70
71#[derive(Deserialize, Debug)]
72pub struct Fields {
73 pub name: String,
74 #[serde(deserialize_with = "deserialize_raw_string")]
75 pub description: String,
76 #[serde(rename = "authorPHID")]
77 pub author_phid: Phid,
78 #[serde(rename = "ownerPHID")]
79 pub owner_phid: Option<Phid>,
80 #[serde(rename = "closerPHID")]
81 pub closer_phid: Option<Phid>,
82 pub status: Status,
83 pub priority: Priority,
84 pub points: Option<Decimal>,
85 #[serde(rename = "dateCreated", deserialize_with = "deserialize_timestamp")]
86 pub created: DateTime<Utc>,
87 #[serde(rename = "dateModified", deserialize_with = "deserialize_timestamp")]
88 pub modified: DateTime<Utc>,
89 #[serde(
90 rename = "dateClosed",
91 deserialize_with = "deserialize_timestamp_option"
92 )]
93 pub closed: Option<DateTime<Utc>>,
94 policy: JsonValue,
95}
96
97#[derive(Deserialize, Debug)]
98pub struct Subscriber {
99 #[serde(rename = "subscriberCount")]
100 pub count: u32,
101 #[serde(rename = "subscriberPHIDs")]
102 pub phids: Vec<Phid>,
103 #[serde(rename = "viewerIsSubscribed")]
104 pub viewer_is_subscribed: bool,
105}
106
107#[derive(Deserialize, Debug)]
108pub struct Projects {
109 #[serde(rename = "projectPHIDs")]
110 pub projects: Vec<Phid>,
111}
112
113#[derive(Deserialize, Debug)]
114pub struct Column {
115 pub id: u32,
116 pub name: String,
117 pub phid: Phid,
118}
119
120#[derive(Deserialize, Debug)]
121pub struct Columns {
122 pub columns: Vec<Column>,
123}
124
125#[derive(Deserialize, Debug)]
126pub struct Boards {
127 pub boards: HashMap<Phid, Columns>,
128}
129
130#[derive(Deserialize, Debug)]
131pub struct AttachmentsResult {
132 pub columns: Option<Boards>,
133 pub subscribers: Option<Subscriber>,
134 pub projects: Option<Projects>,
135}
136
137impl ApiRequest for Search {
138 type Reply = SearchResult;
139 const ROUTE: &'static str = "api/maniphest.search";
140}
141
142impl ApiRequest for SearchCursor<'_> {
143 type Reply = SearchResult;
144 const ROUTE: &'static str = "api/maniphest.search";
145}
146
147#[cfg(test)]
148mod test {
149 use super::*;
150 use phabricator_mock::task::Task;
151 use phabricator_mock::PhabMockServer;
152
153 fn compare_task(server: &Task, response: &SearchData) {
154 assert_eq!(server.id, response.id);
155 assert_eq!(server.full_name, response.fields.name);
156 assert_eq!(server.description, response.fields.description);
157 assert_eq!(server.points, response.fields.points);
158 }
160
161 #[tokio::test]
162 async fn basics() {
163 let m = PhabMockServer::start().await;
164 let user = m.new_user("user", "Test User");
165 m.new_simple_task(100, &user);
166 let status = m.new_status("foo", "bar", None);
167 let task = phabricator_mock::task()
168 .id(200)
169 .full_name("Test task")
170 .description("Test description")
171 .author(user.clone())
172 .owner(user.clone())
173 .priority(m.default_priority())
174 .status(status)
175 .build()
176 .unwrap();
177 m.add_task(task);
178
179 let client = crate::Client::new(m.uri(), m.token().to_string());
180 let s = Search {
181 constraints: Constraints {
182 ids: Some(vec![100, 200]),
183 ..Default::default()
184 },
185 ..Default::default()
186 };
187
188 let r = client.request(&s).await.unwrap();
189 assert_eq!(r.data.len(), 2);
190
191 let respond_task = r.data.iter().find(|d| d.id == 100).unwrap();
192 let server_task = m.get_task(100).unwrap();
193
194 compare_task(&server_task, respond_task);
195
196 let respond_task = r.data.iter().find(|d| d.id == 200).unwrap();
197 let server_task = m.get_task(200).unwrap();
198 compare_task(&server_task, respond_task);
199 }
200
201 #[tokio::test]
202 async fn subscribers() {
203 let m = PhabMockServer::start().await;
204 let user = m.new_user("user", "Test User");
205 let subscriber = m.new_user("subscriber", "Subscribed usr");
206
207 let task = phabricator_mock::task()
208 .id(100)
209 .full_name("Test task")
210 .description("Test description")
211 .author(user.clone())
212 .owner(user.clone())
213 .priority(m.default_priority())
214 .status(m.default_status())
215 .subscribers(vec![subscriber.clone()])
216 .build()
217 .unwrap();
218 m.add_task(task);
219
220 let client = crate::Client::new(m.uri(), m.token().to_string());
221 let s = Search {
222 constraints: Constraints {
223 ids: Some(vec![100]),
224 ..Default::default()
225 },
226 attachments: Attachments {
227 subscribers: true,
228 ..Default::default()
229 },
230 ..Default::default()
231 };
232
233 let r = client.request(&s).await.unwrap();
234 assert_eq!(r.data.len(), 1);
235
236 let respond_task = r.data.iter().find(|d| d.id == 100).unwrap();
237 let server_task = m.get_task(100).unwrap();
238
239 compare_task(&server_task, respond_task);
240
241 let s = respond_task
242 .attachments
243 .subscribers
244 .as_ref()
245 .expect("No subscribers");
246 assert_eq!(1, s.count);
247 assert_eq!(1, s.phids.len());
248 assert_eq!(&subscriber.phid.to_string(), &s.phids[0].0);
249 }
250
251 #[tokio::test]
252 async fn projects() {
253 let m = PhabMockServer::start().await;
254 let user = m.new_user("user", "Test User");
255 let project = phabricator_mock::project()
256 .id(25)
257 .name("Project")
258 .build()
259 .unwrap();
260 m.add_project(project.clone());
261
262 let task = phabricator_mock::task()
263 .id(100)
264 .full_name("Test task")
265 .description("Test description")
266 .author(user.clone())
267 .owner(user.clone())
268 .priority(m.default_priority())
269 .status(m.default_status())
270 .projects(vec![project.clone()])
271 .build()
272 .unwrap();
273 m.add_task(task);
274
275 let client = crate::Client::new(m.uri(), m.token().to_string());
276 let s = Search {
277 constraints: Constraints {
278 ids: Some(vec![100]),
279 ..Default::default()
280 },
281 attachments: Attachments {
282 projects: true,
283 ..Default::default()
284 },
285 ..Default::default()
286 };
287
288 let r = client.request(&s).await.unwrap();
289 assert_eq!(r.data.len(), 1);
290
291 let respond_task = r.data.iter().find(|d| d.id == 100).unwrap();
292 let server_task = m.get_task(100).unwrap();
293
294 compare_task(&server_task, respond_task);
295
296 let p = respond_task
297 .attachments
298 .projects
299 .as_ref()
300 .expect("No projects");
301 assert_eq!(1, p.projects.len());
302 assert_eq!(&project.phid.to_string(), &p.projects[0].0);
303 }
304
305 #[tokio::test]
306 async fn columns() {
307 let m = PhabMockServer::start().await;
308 let user = m.new_user("user", "Test User");
309 let project = phabricator_mock::project()
310 .id(25)
311 .name("Project")
312 .build()
313 .unwrap();
314 let column = phabricator_mock::column()
315 .id(15)
316 .name("Backlog")
317 .project(project.clone())
318 .build()
319 .unwrap();
320 project.add_column(column.clone());
321
322 m.add_project(project.clone());
323
324 let task = phabricator_mock::task()
325 .id(100)
326 .full_name("Test task")
327 .description("Test description")
328 .author(user.clone())
329 .owner(user.clone())
330 .priority(m.default_priority())
331 .status(m.default_status())
332 .columns(vec![column.clone()])
333 .build()
334 .unwrap();
335 m.add_task(task);
336
337 let client = crate::Client::new(m.uri(), m.token().to_string());
338 let s = Search {
339 constraints: Constraints {
340 ids: Some(vec![100]),
341 ..Default::default()
342 },
343 attachments: Attachments {
344 columns: true,
345 ..Default::default()
346 },
347 ..Default::default()
348 };
349
350 let r = client.request(&s).await.unwrap();
351 assert_eq!(r.data.len(), 1);
352
353 let respond_task = r.data.iter().find(|d| d.id == 100).unwrap();
354 let server_task = m.get_task(100).unwrap();
355
356 compare_task(&server_task, respond_task);
357
358 let b = respond_task
359 .attachments
360 .columns
361 .as_ref()
362 .expect("No Columns");
363
364 let c = &b.boards[&Phid(project.phid.to_string())];
365 assert_eq!(1, c.columns.len());
366
367 let c = &c.columns[0];
368 assert_eq!(15, c.id);
369 assert_eq!("Backlog", c.name);
370 assert_eq!(column.phid.to_string(), c.phid.0);
371 }
372
373 #[tokio::test]
374 async fn points() {
375 let m = PhabMockServer::start().await;
376 let user = m.new_user("user", "Test User");
377 let task = phabricator_mock::task()
378 .id(100)
379 .points(Decimal::new(25, 2))
380 .full_name("Test task")
381 .description("Test description")
382 .author(user.clone())
383 .owner(user.clone())
384 .priority(m.default_priority())
385 .status(m.default_status())
386 .build()
387 .unwrap();
388 m.add_task(task);
389
390 let client = crate::Client::new(m.uri(), m.token().to_string());
391 let s = Search {
392 constraints: Constraints {
393 ids: Some(vec![100]),
394 ..Default::default()
395 },
396 ..Default::default()
397 };
398
399 let r = client.request(&s).await.unwrap();
400 assert_eq!(r.data.len(), 1);
401
402 let respond_task = r.data.iter().find(|d| d.id == 100).unwrap();
403 let server_task = m.get_task(100).unwrap();
404
405 compare_task(&server_task, respond_task);
406 }
407
408 #[tokio::test]
409 async fn no_result() {
410 let m = PhabMockServer::start().await;
411 let client = crate::Client::new(m.uri(), m.token().to_string());
412
413 let s = Search {
414 constraints: Constraints {
415 ids: Some(vec![100, 200]),
416 ..Default::default()
417 },
418 ..Default::default()
419 };
420
421 let r = client.request(&s).await.unwrap();
422 assert_eq!(0, r.data.len());
423 }
424}