1#[derive(Debug, Default, Clone)]
10pub struct Filter {
11 pub queue: Option<String>,
12 pub assignee: Option<String>,
13 pub status: Option<String>,
14 pub tags: Vec<String>,
15}
16
17impl Filter {
18 #[must_use]
19 pub fn is_empty(&self) -> bool {
20 self.queue.is_none()
21 && self.assignee.is_none()
22 && self.status.is_none()
23 && self.tags.is_empty()
24 }
25
26 #[must_use]
28 pub fn to_query(&self) -> String {
29 let mut clauses: Vec<String> = Vec::new();
30
31 if let Some(queue) = &self.queue {
32 clauses.push(format!("Queue: {}", value(queue)));
33 }
34 if let Some(assignee) = &self.assignee {
35 clauses.push(format!("Assignee: {}", assignee_value(assignee)));
36 }
37 if let Some(status) = &self.status {
38 clauses.push(format!("Status: {}", value(status)));
39 }
40 if !self.tags.is_empty() {
41 let tags: Vec<String> = self.tags.iter().map(|tag| value(tag)).collect();
42 clauses.push(format!("Tags: {}", tags.join(", ")));
43 }
44
45 clauses.join(" AND ")
46 }
47}
48
49fn assignee_value(assignee: &str) -> String {
52 if assignee.eq_ignore_ascii_case("me") {
53 return "me()".to_owned();
54 }
55 value(assignee)
56}
57
58fn value(raw: &str) -> String {
65 format!("\"{}\"", raw.replace('\\', "\\\\").replace('"', "\\\""))
66}
67
68#[cfg(test)]
69mod tests {
70 use super::*;
71
72 fn filter() -> Filter {
73 Filter {
74 queue: Some("PROJ".to_owned()),
75 assignee: Some("ilubenets".to_owned()),
76 status: Some("In Progress".to_owned()),
77 tags: vec!["regression".to_owned()],
78 }
79 }
80
81 #[test]
82 fn clauses_join_with_and_in_a_fixed_order() {
83 assert_eq!(
84 filter().to_query(),
85 r#"Queue: "PROJ" AND Assignee: "ilubenets" AND Status: "In Progress" AND Tags: "regression""#
86 );
87 }
88
89 #[test]
90 fn me_becomes_trackers_own_function_rather_than_a_lookup() {
91 let filter = Filter {
92 assignee: Some("me".to_owned()),
93 ..Filter::default()
94 };
95 assert_eq!(filter.to_query(), "Assignee: me()");
96 }
97
98 #[test]
99 fn me_is_matched_regardless_of_case() {
100 let filter = Filter {
101 assignee: Some("ME".to_owned()),
102 ..Filter::default()
103 };
104 assert_eq!(filter.to_query(), "Assignee: me()");
105 }
106
107 #[test]
110 fn embedded_quotes_are_escaped_not_dropped() {
111 let filter = Filter {
112 status: Some(r#"say "hi""#.to_owned()),
113 ..Filter::default()
114 };
115 assert_eq!(filter.to_query(), r#"Status: "say \"hi\"""#);
116 }
117
118 #[test]
119 fn several_tags_become_one_clause() {
120 let filter = Filter {
121 tags: vec!["a".to_owned(), "b".to_owned()],
122 ..Filter::default()
123 };
124 assert_eq!(filter.to_query(), r#"Tags: "a", "b""#);
125 }
126
127 #[test]
128 fn an_empty_filter_is_recognisable() {
129 assert!(Filter::default().is_empty());
130 assert!(!filter().is_empty());
131 }
132}