phabricator_api/maniphest/
search.rs

1use 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        // TODO compare more fields
159    }
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}