nimbus_cli/sources/
filter.rs
1use crate::{cli::ExperimentListFilterArgs, output::info::ExperimentInfo};
6use anyhow::Result;
7use serde_json::Value;
8
9#[derive(Clone, Debug, Default, PartialEq)]
10pub(crate) struct ExperimentListFilter {
11 slug_pattern: Option<String>,
12 app: Option<String>,
13 feature_pattern: Option<String>,
14 active_on: Option<String>,
15 enrolling_on: Option<String>,
16 channel: Option<String>,
17 is_rollout: Option<bool>,
18}
19
20impl From<&ExperimentListFilterArgs> for ExperimentListFilter {
21 fn from(value: &ExperimentListFilterArgs) -> Self {
22 ExperimentListFilter {
23 slug_pattern: value.slug.clone(),
24 feature_pattern: value.feature.clone(),
25 active_on: value.active_on.clone(),
26 enrolling_on: value.enrolling_on.clone(),
27 channel: value.channel.clone(),
28 is_rollout: value.is_rollout,
29 ..Default::default()
30 }
31 }
32}
33
34impl ExperimentListFilter {
35 pub(crate) fn for_app(app: &str) -> Self {
36 ExperimentListFilter {
37 app: Some(app.to_string()),
38 ..Default::default()
39 }
40 }
41}
42
43#[cfg(test)]
44impl ExperimentListFilter {
45 pub(crate) fn for_feature(feature_pattern: &str) -> Self {
46 ExperimentListFilter {
47 feature_pattern: Some(feature_pattern.to_string()),
48 ..Default::default()
49 }
50 }
51
52 pub(crate) fn for_active_on(date: &str) -> Self {
53 ExperimentListFilter {
54 active_on: Some(date.to_string()),
55 ..Default::default()
56 }
57 }
58
59 pub(crate) fn for_enrolling_on(date: &str) -> Self {
60 ExperimentListFilter {
61 enrolling_on: Some(date.to_string()),
62 ..Default::default()
63 }
64 }
65}
66
67impl ExperimentListFilter {
68 pub(crate) fn is_empty(&self) -> bool {
69 self == &Default::default()
70 }
71
72 pub(crate) fn matches(&self, value: &Value) -> Result<bool> {
73 let info: ExperimentInfo = match value.try_into() {
74 Ok(e) => e,
75 _ => return Ok(false),
76 };
77 Ok(self.matches_info(info))
78 }
79
80 fn matches_info(&self, info: ExperimentInfo) -> bool {
81 match self.slug_pattern.as_deref() {
82 Some(s) if !info.slug.contains(s) => return false,
83 _ => (),
84 };
85
86 match self.app.as_deref() {
87 Some(s) if s != info.app_name => return false,
88 _ => (),
89 };
90
91 match self.channel.as_deref() {
92 Some(s) if s != info.channel => return false,
93 _ => (),
94 };
95
96 match self.is_rollout {
97 Some(s) if s != info.is_rollout => return false,
98 _ => (),
99 };
100
101 match self.feature_pattern.as_deref() {
102 Some(f) if !info.features.iter().any(|s| s.contains(f)) => return false,
103 _ => (),
104 };
105
106 match self.active_on.as_deref() {
107 Some(date) if !info.active().contains(date) => return false,
108 _ => (),
109 };
110
111 match self.enrolling_on.as_deref() {
112 Some(date) if !info.enrollment().contains(date) => return false,
113 _ => (),
114 };
115
116 true
117 }
118}
119
120#[cfg(test)]
121mod unit_tests {
122 use crate::output::info::DateRange;
123
124 use super::*;
125
126 #[test]
127 fn test_matches_app() -> Result<()> {
128 let filter = ExperimentListFilter {
129 app: Some("my-app".to_string()),
130 ..Default::default()
131 };
132
133 let positive = ExperimentInfo {
134 app_name: "my-app",
135 ..Default::default()
136 };
137 assert!(filter.matches_info(positive));
138
139 let negative = ExperimentInfo {
140 app_name: "not-my-app",
141 ..Default::default()
142 };
143 assert!(!filter.matches_info(negative));
144
145 Ok(())
146 }
147
148 #[test]
149 fn test_matches_slug() -> Result<()> {
150 let filter = ExperimentListFilter {
151 slug_pattern: Some("my-app".to_string()),
152 ..Default::default()
153 };
154
155 let positive = ExperimentInfo {
156 slug: "my-app",
157 ..Default::default()
158 };
159 assert!(filter.matches_info(positive));
160
161 let negative = ExperimentInfo {
162 slug: "my-other-app",
163 ..Default::default()
164 };
165 assert!(!filter.matches_info(negative));
166
167 Ok(())
168 }
169
170 #[test]
171 fn test_matches_channel() -> Result<()> {
172 let filter = ExperimentListFilter {
173 channel: Some("release".to_string()),
174 ..Default::default()
175 };
176
177 let positive = ExperimentInfo {
178 channel: "release",
179 ..Default::default()
180 };
181 assert!(filter.matches_info(positive));
182
183 let negative = ExperimentInfo {
184 channel: "beta",
185 ..Default::default()
186 };
187 assert!(!filter.matches_info(negative));
188
189 Ok(())
190 }
191
192 #[test]
193 fn test_matches_is_rollout() -> Result<()> {
194 let filter = ExperimentListFilter {
195 is_rollout: Some(false),
196 ..Default::default()
197 };
198
199 let positive = ExperimentInfo {
200 is_rollout: false,
201 ..Default::default()
202 };
203 assert!(filter.matches_info(positive));
204
205 let negative = ExperimentInfo {
206 is_rollout: true,
207 ..Default::default()
208 };
209 assert!(!filter.matches_info(negative));
210
211 Ok(())
212 }
213
214 #[test]
215 fn test_matches_conjunction() -> Result<()> {
216 let filter = ExperimentListFilter {
217 app: Some("my-app".to_string()),
218 channel: Some("release".to_string()),
219 ..Default::default()
220 };
221
222 let positive = ExperimentInfo {
223 app_name: "my-app",
224 channel: "release",
225 ..Default::default()
226 };
227 assert!(filter.matches_info(positive));
228
229 let negative = ExperimentInfo {
230 app_name: "not-my-app",
231 channel: "release",
232 ..Default::default()
233 };
234 assert!(!filter.matches_info(negative));
235
236 let negative = ExperimentInfo {
237 app_name: "my-app",
238 channel: "not-release",
239 ..Default::default()
240 };
241 assert!(!filter.matches_info(negative));
242
243 Ok(())
244 }
245
246 #[test]
247 fn test_matches_features() -> Result<()> {
248 let filter = ExperimentListFilter {
249 feature_pattern: Some("another".to_string()),
250 ..Default::default()
251 };
252
253 let positive = ExperimentInfo {
254 features: vec!["my-feature", "another-feature"],
255 ..Default::default()
256 };
257 assert!(filter.matches_info(positive));
258
259 let negative = ExperimentInfo {
260 features: vec!["my-feature", "not-this-feature"],
261 ..Default::default()
262 };
263 assert!(!filter.matches_info(negative));
264
265 Ok(())
266 }
267
268 #[test]
269 fn test_matches_enrolling_on() -> Result<()> {
270 let filter = ExperimentListFilter {
271 enrolling_on: Some("2023-07-18".to_string()),
272 ..Default::default()
273 };
274
275 let positive = ExperimentInfo {
276 enrollment: DateRange::from_str("2023-07-01", "2023-07-31", 0),
277 ..Default::default()
278 };
279 assert!(filter.matches_info(positive));
280
281 let negative = ExperimentInfo {
282 enrollment: DateRange::from_str("2023-06-01", "2023-06-30", 0),
283 ..Default::default()
284 };
285 assert!(!filter.matches_info(negative));
286
287 Ok(())
288 }
289
290 #[test]
291 fn test_matches_active_on() -> Result<()> {
292 let filter = ExperimentListFilter {
293 active_on: Some("2023-07-18".to_string()),
294 ..Default::default()
295 };
296
297 let positive = ExperimentInfo {
298 duration: DateRange::from_str("2023-07-01", "2023-07-31", 0),
299 ..Default::default()
300 };
301 assert!(filter.matches_info(positive));
302
303 let negative = ExperimentInfo {
304 duration: DateRange::from_str("2023-06-01", "2023-06-30", 0),
305 ..Default::default()
306 };
307 assert!(!filter.matches_info(negative));
308
309 Ok(())
310 }
311}