flagsmith_flag_engine/segments/
mod.rs

1use super::features;
2use super::types::{FlagsmithValue, FlagsmithValueType};
3use regex::Regex;
4use semver::Version;
5use serde::{Deserialize, Serialize};
6pub mod constants;
7pub mod evaluator;
8
9#[derive(Clone, Serialize, Deserialize, Debug)]
10pub struct SegmentCondition {
11    pub operator: String,
12    pub value: Option<String>,
13    #[serde(rename = "property_")]
14    pub property: Option<String>,
15}
16
17impl SegmentCondition {
18    pub fn matches_trait_value(&self, trait_value: &FlagsmithValue) -> bool {
19        if self.operator.as_str() == constants::MODULO {
20            return self.modulo_operations(trait_value, &self.value.as_ref().unwrap());
21        }
22        if self.operator.as_str() == constants::IN {
23            return match trait_value.value_type {
24                FlagsmithValueType::String => {
25                    self.in_operations(&trait_value.value, &self.value.as_ref().unwrap())
26                }
27                FlagsmithValueType::Integer => {
28                    let trait_value: String = trait_value.value.to_string();
29                    self.in_operations(&trait_value, &self.value.as_ref().unwrap())
30                }
31                _ => false,
32            };
33        }
34        return match trait_value.value_type {
35            FlagsmithValueType::Integer => {
36                let trait_value: i64 = trait_value.value.parse().unwrap();
37                let segment_condition_value: i64 = self.value.as_ref().unwrap().parse().unwrap();
38
39                self.number_operations(trait_value, segment_condition_value)
40            }
41            FlagsmithValueType::Float => {
42                let trait_value: f64 = trait_value.value.parse().unwrap();
43                let segment_condition_value: f64 = self.value.as_ref().unwrap().parse().unwrap();
44                self.number_operations(trait_value, segment_condition_value)
45            }
46            FlagsmithValueType::String if self.value.as_ref().unwrap().ends_with(":semver") => {
47                let trait_value = Version::parse(&trait_value.value).unwrap();
48                let segment_condition_value = Version::parse(
49                    &self.value.as_ref().unwrap()[..self.value.as_ref().unwrap().len() - 7],
50                )
51                .unwrap();
52                self.semver_operations(trait_value, segment_condition_value)
53            }
54            FlagsmithValueType::String => {
55                self.string_operations(&trait_value.value, &self.value.as_ref().unwrap())
56            }
57            FlagsmithValueType::Bool => {
58                let trait_value: bool = trait_value.value.parse().unwrap();
59                let segment_condition_value: bool = self.value.clone().unwrap().parse().unwrap();
60                self.bool_operations(trait_value, segment_condition_value)
61            }
62            _ => false,
63        };
64    }
65    fn string_operations(&self, trait_value: &str, segment_value: &str) -> bool {
66        match self.operator.as_str() {
67            constants::EQUAL => trait_value == segment_value,
68            constants::NOT_EQUAL => trait_value != segment_value,
69            constants::CONTAINS => trait_value.contains(segment_value),
70            constants::NOT_CONTAINS => !trait_value.contains(segment_value),
71            constants::REGEX => {
72                let re = Regex::new(segment_value).unwrap();
73                re.is_match(&trait_value)
74            }
75            _ => false,
76        }
77    }
78    fn modulo_operations(&self, trait_value: &FlagsmithValue, segment_value: &str) -> bool {
79        let values: Vec<&str> = segment_value.split("|").collect();
80        if values.len() != 2 {
81            return false;
82        }
83        let divisor: f64 = match values[0].parse() {
84            Ok(v) => v,
85            Err(_) => return false,
86        };
87        let remainder: f64 = match values[1].parse() {
88            Ok(v) => v,
89            Err(_) => return false,
90        };
91
92        let trait_value: f64 = match trait_value.value.parse() {
93            Ok(v) => v,
94            Err(_) => return false,
95        };
96        return (trait_value % divisor) == remainder;
97    }
98    fn semver_operations(&self, trait_value: Version, segment_value: Version) -> bool {
99        match self.operator.as_str() {
100            constants::EQUAL => trait_value == segment_value,
101            constants::NOT_EQUAL => trait_value != segment_value,
102            constants::GREATER_THAN => trait_value > segment_value,
103            constants::GREATER_THAN_INCLUSIVE => trait_value >= segment_value,
104            constants::LESS_THAN => trait_value < segment_value,
105            constants::LESS_THAN_INCLUSIVE => trait_value <= segment_value,
106            _ => false,
107        }
108    }
109    fn bool_operations(&self, trait_value: bool, segment_value: bool) -> bool {
110        match self.operator.as_str() {
111            constants::EQUAL => trait_value == segment_value,
112            constants::NOT_EQUAL => trait_value != segment_value,
113            _ => false,
114        }
115    }
116    fn number_operations<T: PartialOrd + PartialEq>(
117        &self,
118        trait_value: T,
119        segment_value: T,
120    ) -> bool {
121        match self.operator.as_str() {
122            constants::EQUAL => trait_value == segment_value,
123            constants::NOT_EQUAL => trait_value != segment_value,
124            constants::GREATER_THAN => trait_value > segment_value,
125            constants::GREATER_THAN_INCLUSIVE => trait_value >= segment_value,
126            constants::LESS_THAN => trait_value < segment_value,
127            constants::LESS_THAN_INCLUSIVE => trait_value <= segment_value,
128            _ => false,
129        }
130    }
131    fn in_operations(&self, trait_value: &str, segment_value: &str) -> bool {
132        segment_value.split(',').any(|x| x == trait_value)
133    }
134}
135
136#[derive(Clone, Serialize, Deserialize, Debug)]
137pub struct SegmentRule {
138    #[serde(rename = "type")]
139    pub segment_rule_type: String,
140    pub rules: Vec<Box<SegmentRule>>,
141    pub conditions: Vec<SegmentCondition>,
142}
143
144#[derive(Clone, Serialize, Deserialize, Debug)]
145pub struct Segment {
146    pub id: u32,
147    pub name: String,
148    pub rules: Vec<SegmentRule>,
149
150    #[serde(default)]
151    pub feature_states: Vec<features::FeatureState>,
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157    use rstest::*;
158
159    #[rstest]
160    #[case(constants::EQUAL, "bar", FlagsmithValueType::String, "bar", true)]
161    #[case(constants::EQUAL, "bar", FlagsmithValueType::String, "baz", false)]
162    #[case(constants::EQUAL, "1", FlagsmithValueType::Integer, "1", true)]
163    #[case(constants::EQUAL, "1", FlagsmithValueType::Integer, "2", false)]
164    #[case(constants::EQUAL, "true", FlagsmithValueType::Bool, "true", true)]
165    #[case(constants::EQUAL, "false", FlagsmithValueType::Bool, "false", true)]
166    #[case(constants::EQUAL, "true", FlagsmithValueType::Bool, "false", false)]
167    #[case(constants::EQUAL, "false", FlagsmithValueType::Bool, "true", false)]
168    #[case(constants::EQUAL, "1.23", FlagsmithValueType::Float, "1.23", true)]
169    #[case(constants::EQUAL, "1.23", FlagsmithValueType::Float, "1.25", false)]
170    #[case(constants::GREATER_THAN, "2", FlagsmithValueType::Integer, "1", true)]
171    #[case(constants::GREATER_THAN, "1", FlagsmithValueType::Integer, "2", false)]
172    #[case(constants::GREATER_THAN, "1", FlagsmithValueType::Integer, "1", false)]
173    #[case(constants::GREATER_THAN, "0", FlagsmithValueType::Integer, "1", false)]
174    #[case(constants::GREATER_THAN, "2.1", FlagsmithValueType::Float, "2.0", true)]
175    #[case(
176        constants::GREATER_THAN,
177        "2.1",
178        FlagsmithValueType::Float,
179        "2.2",
180        false
181    )]
182    #[case(
183        constants::GREATER_THAN,
184        "2.0",
185        FlagsmithValueType::Float,
186        "2.1",
187        false
188    )]
189    #[case(
190        constants::GREATER_THAN,
191        "2.0",
192        FlagsmithValueType::Float,
193        "2.1",
194        false
195    )]
196    #[case(
197        constants::GREATER_THAN,
198        "2.0",
199        FlagsmithValueType::Float,
200        "2.0",
201        false
202    )]
203    #[case(
204        constants::GREATER_THAN_INCLUSIVE,
205        "1",
206        FlagsmithValueType::Integer,
207        "1",
208        true
209    )]
210    #[case(
211        constants::GREATER_THAN_INCLUSIVE,
212        "2",
213        FlagsmithValueType::Integer,
214        "1",
215        true
216    )]
217    #[case(
218        constants::GREATER_THAN_INCLUSIVE,
219        "0",
220        FlagsmithValueType::Integer,
221        "1",
222        false
223    )]
224    #[case(
225        constants::GREATER_THAN_INCLUSIVE,
226        "2.0",
227        FlagsmithValueType::Float,
228        "2.0",
229        true
230    )]
231    #[case(
232        constants::GREATER_THAN_INCLUSIVE,
233        "2.1",
234        FlagsmithValueType::Float,
235        "2.0",
236        true
237    )]
238    #[case(
239        constants::GREATER_THAN_INCLUSIVE,
240        "2.1",
241        FlagsmithValueType::Float,
242        "2.2",
243        false
244    )]
245    #[case(constants::LESS_THAN, "2", FlagsmithValueType::Integer, "1", false)]
246    #[case(constants::LESS_THAN, "1", FlagsmithValueType::Integer, "2", true)]
247    #[case(constants::LESS_THAN, "1", FlagsmithValueType::Integer, "1", false)]
248    #[case(constants::LESS_THAN, "0", FlagsmithValueType::Integer, "1", true)]
249    #[case(constants::LESS_THAN, "2.1", FlagsmithValueType::Float, "2.0", false)]
250    #[case(constants::LESS_THAN, "2.1", FlagsmithValueType::Float, "2.2", true)]
251    #[case(constants::LESS_THAN, "2.0", FlagsmithValueType::Float, "2.1", true)]
252    #[case(constants::LESS_THAN, "2.0", FlagsmithValueType::Float, "2.1", true)]
253    #[case(constants::LESS_THAN, "2.0", FlagsmithValueType::Float, "2.0", false)]
254    #[case(
255        constants::LESS_THAN_INCLUSIVE,
256        "1",
257        FlagsmithValueType::Integer,
258        "1",
259        true
260    )]
261    #[case(
262        constants::LESS_THAN_INCLUSIVE,
263        "2",
264        FlagsmithValueType::Integer,
265        "1",
266        false
267    )]
268    #[case(
269        constants::LESS_THAN_INCLUSIVE,
270        "1",
271        FlagsmithValueType::Integer,
272        "2",
273        true
274    )]
275    #[case(
276        constants::LESS_THAN_INCLUSIVE,
277        "2.0",
278        FlagsmithValueType::Float,
279        "2.0",
280        true
281    )]
282    #[case(
283        constants::LESS_THAN_INCLUSIVE,
284        "2.1",
285        FlagsmithValueType::Float,
286        "2.0",
287        false
288    )]
289    #[case(
290        constants::LESS_THAN_INCLUSIVE,
291        "2.2",
292        FlagsmithValueType::Float,
293        "2.3",
294        true
295    )]
296    #[case(constants::NOT_EQUAL, "bar", FlagsmithValueType::String, "bar", false)]
297    #[case(constants::NOT_EQUAL, "bar", FlagsmithValueType::String, "baz", true)]
298    #[case(constants::NOT_EQUAL, "1", FlagsmithValueType::Integer, "1", false)]
299    #[case(constants::NOT_EQUAL, "1", FlagsmithValueType::Integer, "2", true)]
300    #[case(constants::NOT_EQUAL, "true", FlagsmithValueType::Bool, "true", false)]
301    #[case(
302        constants::NOT_EQUAL,
303        "false",
304        FlagsmithValueType::Bool,
305        "false",
306        false
307    )]
308    #[case(constants::NOT_EQUAL, "true", FlagsmithValueType::Bool, "false", true)]
309    #[case(constants::NOT_EQUAL, "false", FlagsmithValueType::Bool, "true", true)]
310    #[case(constants::NOT_EQUAL, "1.23", FlagsmithValueType::Float, "1.23", false)]
311    #[case(constants::NOT_EQUAL, "1.23", FlagsmithValueType::Float, "1.25", true)]
312    #[case(constants::CONTAINS, "bar", FlagsmithValueType::String, "b", true)]
313    #[case(constants::CONTAINS, "bar", FlagsmithValueType::String, "bar", true)]
314    #[case(constants::CONTAINS, "bar", FlagsmithValueType::String, "baz", false)]
315    #[case(constants::NOT_CONTAINS, "bar", FlagsmithValueType::String, "b", false)]
316    #[case(
317        constants::NOT_CONTAINS,
318        "bar",
319        FlagsmithValueType::String,
320        "bar",
321        false
322    )]
323    #[case(
324        constants::NOT_CONTAINS,
325        "bar",
326        FlagsmithValueType::String,
327        "baz",
328        true
329    )]
330    #[case(constants::REGEX, "foo", FlagsmithValueType::String, r"[a-z]+", true)]
331    #[case(constants::IN, "foo", FlagsmithValueType::String, "", false)]
332    #[case(constants::IN, "foo", FlagsmithValueType::String, "foo,bar", true)]
333    #[case(constants::IN, "bar", FlagsmithValueType::String, "foo,bar", true)]
334    #[case(constants::IN, "ba", FlagsmithValueType::String, "foo,bar", false)]
335    #[case(constants::IN, "foo", FlagsmithValueType::String, "foo", true)]
336    #[case(constants::IN, "1", FlagsmithValueType::Integer, "1,2,3,4", true)]
337    #[case(constants::IN, "1", FlagsmithValueType::Integer, "", false)]
338    #[case(constants::IN, "1", FlagsmithValueType::Integer, "1", true)]
339    // Flagsmith's engine does not evaluate `IN` condition for floats/doubles and booleans
340    // due to ambiguous serialization across supported platforms.
341    #[case(constants::IN, "1.5", FlagsmithValueType::Float, "1.5", false)]
342    #[case(constants::IN, "false", FlagsmithValueType::Bool, "false", false)]
343    fn segemnt_condition_matches_trait_value(
344        #[case] operator: &str,
345        #[case] trait_value: &str,
346        #[case] trait_value_type: FlagsmithValueType,
347        #[case] value: &str,
348        #[case] result: bool,
349    ) {
350        let trait_value = FlagsmithValue {
351            value: trait_value.to_string(),
352            value_type: trait_value_type,
353        };
354        let segment_condition = SegmentCondition {
355            operator: operator.to_string(),
356            value: Some(value.to_string()),
357            property: Some("foo".to_string()),
358        };
359        assert_eq!(segment_condition.matches_trait_value(&trait_value), result)
360    }
361
362    #[rstest]
363    #[case(
364        constants::EQUAL,
365        "1.0.0",
366        FlagsmithValueType::String,
367        "1.0.0:semver",
368        true
369    )]
370    #[case(
371        constants::EQUAL,
372        "1.0.0",
373        FlagsmithValueType::String,
374        "1.0.1:semver",
375        false
376    )]
377    #[case(
378        constants::NOT_EQUAL,
379        "1.0.0",
380        FlagsmithValueType::String,
381        "1.0.1:semver",
382        true
383    )]
384    #[case(
385        constants::NOT_EQUAL,
386        "1.0.0",
387        FlagsmithValueType::String,
388        "1.0.0:semver",
389        false
390    )]
391    #[case(
392        constants::GREATER_THAN,
393        "1.0.1",
394        FlagsmithValueType::String,
395        "1.0.0:semver",
396        true
397    )]
398    #[case(
399        constants::GREATER_THAN,
400        "1.0.1",
401        FlagsmithValueType::String,
402        "1.0.1:semver",
403        false
404    )]
405    #[case(
406        constants::GREATER_THAN,
407        "1.0.0",
408        FlagsmithValueType::String,
409        "1.0.0-beta:semver",
410        true
411    )]
412    #[case(
413        constants::GREATER_THAN,
414        "1.0.1",
415        FlagsmithValueType::String,
416        "1.0.2-beta:semver",
417        false
418    )]
419    #[case(
420        constants::GREATER_THAN,
421        "1.2.3",
422        FlagsmithValueType::String,
423        "1.2.3-pre.2+build.4:semver",
424        true
425    )]
426    #[case(
427        constants::LESS_THAN,
428        "1.0.1",
429        FlagsmithValueType::String,
430        "1.0.2:semver",
431        true
432    )]
433    #[case(
434        constants::LESS_THAN,
435        "1.0.1",
436        FlagsmithValueType::String,
437        "1.0.1:semver",
438        false
439    )]
440    #[case(
441        constants::LESS_THAN,
442        "1.0.2",
443        FlagsmithValueType::String,
444        "1.0.1:semver",
445        false
446    )]
447    #[case(
448        constants::LESS_THAN,
449        "1.0.0-rc.2",
450        FlagsmithValueType::String,
451        "1.0.0-rc.3:semver",
452        true
453    )]
454    #[case(
455        constants::GREATER_THAN_INCLUSIVE,
456        "1.0.1",
457        FlagsmithValueType::String,
458        "1.0.0:semver",
459        true
460    )]
461    #[case(
462        constants::GREATER_THAN_INCLUSIVE,
463        "1.0.1",
464        FlagsmithValueType::String,
465        "1.0.1:semver",
466        true
467    )]
468    #[case(
469        constants::GREATER_THAN_INCLUSIVE,
470        "1.0.1",
471        FlagsmithValueType::String,
472        "1.0.2-beta:semver",
473        false
474    )]
475    #[case(
476        constants::GREATER_THAN_INCLUSIVE,
477        "1.2.3",
478        FlagsmithValueType::String,
479        "1.2.3-pre.2+build.4:semver",
480        true
481    )]
482    #[case(
483        constants::LESS_THAN_INCLUSIVE,
484        "1.0.1",
485        FlagsmithValueType::String,
486        "1.0.2:semver",
487        true
488    )]
489    #[case(
490        constants::LESS_THAN_INCLUSIVE,
491        "1.0.1",
492        FlagsmithValueType::String,
493        "1.0.1:semver",
494        true
495    )]
496    #[case(
497        constants::LESS_THAN_INCLUSIVE,
498        "1.0.2",
499        FlagsmithValueType::String,
500        "1.0.1:semver",
501        false
502    )]
503    #[case(
504        constants::LESS_THAN_INCLUSIVE,
505        "1.0.0-rc.2",
506        FlagsmithValueType::String,
507        "1.0.0-rc.3:semver",
508        true
509    )]
510
511    fn segemnt_condition_matches_trait_value_semver(
512        #[case] operator: &str,
513        #[case] trait_value: &str,
514        #[case] trait_value_type: FlagsmithValueType,
515        #[case] value: &str,
516        #[case] result: bool,
517    ) {
518        let trait_value = FlagsmithValue {
519            value: trait_value.to_string(),
520            value_type: trait_value_type,
521        };
522        let segment_condition = SegmentCondition {
523            operator: operator.to_string(),
524            value: Some(value.to_string()),
525            property: Some("foo".to_string()),
526        };
527        assert_eq!(segment_condition.matches_trait_value(&trait_value), result)
528    }
529
530    #[rstest]
531    #[case("1", FlagsmithValueType::Integer, "2|0", false)]
532    #[case("1.1", FlagsmithValueType::Float, "2.1|1.1", true)]
533    #[case("2", FlagsmithValueType::Integer, "2|0", true)]
534    #[case("3", FlagsmithValueType::Integer, "2|0", false)]
535    #[case("34.2", FlagsmithValueType::Float, "4|3", false)]
536    #[case("35.0", FlagsmithValueType::Float, "4|3", true)]
537    #[case("bar", FlagsmithValueType::String, "3|0", false)]
538    #[case("1.0.0", FlagsmithValueType::String, "3|0", false)]
539    #[case("false", FlagsmithValueType::Bool, "1|3", false)]
540    fn segment_condition_matches_trait_value_modulo(
541        #[case] trait_value: &str,
542        #[case] trait_value_type: FlagsmithValueType,
543        #[case] value: &str,
544        #[case] result: bool,
545    ) {
546        let trait_value = FlagsmithValue {
547            value: trait_value.to_string(),
548            value_type: trait_value_type,
549        };
550        let segment_condition = SegmentCondition {
551            operator: constants::MODULO.to_string(),
552            value: Some(value.to_string()),
553            property: Some("foo".to_string()),
554        };
555        assert_eq!(segment_condition.matches_trait_value(&trait_value), result)
556    }
557}