1use chrono::{DateTime, NaiveDate, Utc};
2use regex::Regex;
3use serde::{Deserialize, Serialize};
4use sha1::{Digest, Sha1};
5use std::collections::HashMap;
6use std::fmt;
7use std::sync::{Mutex, OnceLock};
8
9static REGEX_CACHE: OnceLock<Mutex<HashMap<String, Option<Regex>>>> = OnceLock::new();
11
12const ROLLOUT_HASH_SALT: &str = "";
16
17const VARIANT_HASH_SALT: &str = "variant";
20
21fn get_cached_regex(pattern: &str) -> Option<Regex> {
22 let cache = REGEX_CACHE.get_or_init(|| Mutex::new(HashMap::new()));
23 let mut cache_guard = match cache.lock() {
24 Ok(guard) => guard,
25 Err(_) => {
26 tracing::warn!(
27 pattern,
28 "Regex cache mutex poisoned, treating as cache miss"
29 );
30 return None;
31 }
32 };
33
34 if let Some(cached) = cache_guard.get(pattern) {
35 return cached.clone();
36 }
37
38 let compiled = Regex::new(pattern).ok();
39 cache_guard.insert(pattern.to_string(), compiled.clone());
40 compiled
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
48#[serde(untagged)]
49pub enum FlagValue {
50 Boolean(bool),
52 String(String),
54}
55
56#[derive(Debug)]
64pub struct InconclusiveMatchError {
65 pub message: String,
67}
68
69impl InconclusiveMatchError {
70 pub fn new(message: &str) -> Self {
72 Self {
73 message: message.to_string(),
74 }
75 }
76}
77
78impl fmt::Display for InconclusiveMatchError {
79 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80 write!(f, "{}", self.message)
81 }
82}
83
84impl std::error::Error for InconclusiveMatchError {}
85
86impl Default for FlagValue {
87 fn default() -> Self {
88 FlagValue::Boolean(false)
89 }
90}
91
92#[derive(Debug, Clone, Serialize, Deserialize)]
97pub struct FeatureFlag {
98 pub key: String,
100 pub active: bool,
102 #[serde(default)]
104 pub filters: FeatureFlagFilters,
105}
106
107#[derive(Debug, Clone, Serialize, Deserialize, Default)]
109pub struct FeatureFlagFilters {
110 #[serde(default)]
112 pub groups: Vec<FeatureFlagCondition>,
113 #[serde(default)]
115 pub multivariate: Option<MultivariateFilter>,
116 #[serde(default)]
118 pub payloads: HashMap<String, serde_json::Value>,
119 #[serde(default)]
122 pub aggregation_group_type_index: Option<i32>,
123 #[serde(default)]
129 pub early_exit: bool,
130}
131
132#[derive(Debug, Clone, Serialize, Deserialize)]
137pub struct FeatureFlagCondition {
138 #[serde(default)]
140 pub properties: Vec<Property>,
141 pub rollout_percentage: Option<f64>,
143 pub variant: Option<String>,
145 #[serde(default)]
149 pub aggregation_group_type_index: Option<i32>,
150}
151
152#[derive(Debug, Clone, Serialize, Deserialize)]
156pub struct Property {
157 pub key: String,
159 pub value: serde_json::Value,
161 #[serde(default = "default_operator")]
167 pub operator: String,
168 #[serde(rename = "type")]
171 pub property_type: Option<String>,
172}
173
174fn default_operator() -> String {
175 "exact".to_string()
176}
177
178#[derive(Debug, Clone, Serialize, Deserialize)]
180pub struct CohortDefinition {
181 pub id: String,
183 #[serde(default)]
187 pub properties: serde_json::Value,
188}
189
190impl CohortDefinition {
191 pub fn new(id: String, properties: Vec<Property>) -> Self {
198 Self {
199 id,
200 properties: serde_json::to_value(properties).unwrap_or_default(),
201 }
202 }
203
204 pub fn parse_properties(&self) -> Vec<Property> {
209 if let Some(arr) = self.properties.as_array() {
211 return arr
212 .iter()
213 .filter_map(|v| serde_json::from_value::<Property>(v.clone()).ok())
214 .collect();
215 }
216
217 if let Some(obj) = self.properties.as_object() {
219 if let Some(values) = obj.get("values") {
220 if let Some(values_arr) = values.as_array() {
221 return values_arr
222 .iter()
223 .filter_map(|v| {
224 if v.get("type").and_then(|t| t.as_str()) == Some("property") {
226 serde_json::from_value::<Property>(v.clone()).ok()
227 } else if let Some(inner_values) = v.get("values") {
228 inner_values.as_array().and_then(|arr| {
230 arr.iter()
231 .filter_map(|inner| {
232 serde_json::from_value::<Property>(inner.clone()).ok()
233 })
234 .next()
235 })
236 } else {
237 None
238 }
239 })
240 .collect();
241 }
242 }
243 }
244
245 Vec::new()
246 }
247}
248
249pub struct EvaluationContext<'a> {
257 pub cohorts: &'a HashMap<String, CohortDefinition>,
259 pub flags: &'a HashMap<String, FeatureFlag>,
262 pub distinct_id: &'a str,
264 pub groups: &'a HashMap<String, String>,
266 pub group_properties: &'a HashMap<String, HashMap<String, serde_json::Value>>,
269 pub group_type_mapping: &'a HashMap<String, String>,
271}
272
273#[derive(Debug, Clone, Serialize, Deserialize, Default)]
275pub struct MultivariateFilter {
276 pub variants: Vec<MultivariateVariant>,
278}
279
280#[derive(Debug, Clone, Serialize, Deserialize)]
282pub struct MultivariateVariant {
283 pub key: String,
285 pub rollout_percentage: f64,
287}
288
289#[derive(Debug, Clone, Serialize, Deserialize)]
294#[serde(untagged)]
295pub enum FeatureFlagsResponse {
296 V2 {
298 flags: HashMap<String, FlagDetail>,
300 #[serde(rename = "errorsWhileComputingFlags")]
302 #[serde(default)]
303 errors_while_computing_flags: bool,
304 #[serde(rename = "quotaLimited")]
307 #[serde(default)]
308 quota_limited: bool,
309 #[serde(rename = "requestId")]
313 #[serde(default)]
314 request_id: Option<String>,
315 },
316 Legacy {
318 #[serde(rename = "featureFlags")]
320 feature_flags: HashMap<String, FlagValue>,
321 #[serde(rename = "featureFlagPayloads")]
323 #[serde(default)]
324 feature_flag_payloads: HashMap<String, serde_json::Value>,
325 #[serde(default)]
327 errors: Option<Vec<String>>,
328 },
329}
330
331impl FeatureFlagsResponse {
332 pub fn normalize(
339 self,
340 ) -> (
341 HashMap<String, FlagValue>,
342 HashMap<String, serde_json::Value>,
343 ) {
344 match self {
345 FeatureFlagsResponse::V2 { flags, .. } => {
346 let mut feature_flags = HashMap::new();
347 let mut payloads = HashMap::new();
348
349 for (key, detail) in flags {
350 if detail.enabled {
351 if let Some(variant) = detail.variant {
352 feature_flags.insert(key.clone(), FlagValue::String(variant));
353 } else {
354 feature_flags.insert(key.clone(), FlagValue::Boolean(true));
355 }
356 } else {
357 feature_flags.insert(key.clone(), FlagValue::Boolean(false));
358 }
359
360 if let Some(metadata) = detail.metadata {
361 if let Some(payload) = metadata.payload {
362 payloads.insert(key, payload);
363 }
364 }
365 }
366
367 (feature_flags, payloads)
368 }
369 FeatureFlagsResponse::Legacy {
370 feature_flags,
371 feature_flag_payloads,
372 ..
373 } => (feature_flags, feature_flag_payloads),
374 }
375 }
376}
377
378#[derive(Debug, Clone, Serialize, Deserialize)]
383pub struct FlagDetail {
384 pub key: String,
386 pub enabled: bool,
388 pub variant: Option<String>,
390 #[serde(default)]
392 pub reason: Option<FlagReason>,
393 #[serde(default)]
395 pub metadata: Option<FlagMetadata>,
396}
397
398#[derive(Debug, Clone, Serialize, Deserialize)]
400pub struct FlagReason {
401 pub code: String,
403 #[serde(default)]
405 pub condition_index: Option<usize>,
406 #[serde(default)]
408 pub description: Option<String>,
409}
410
411#[derive(Debug, Clone, Serialize, Deserialize)]
413pub struct FlagMetadata {
414 pub id: u64,
416 pub version: u32,
418 pub description: Option<String>,
420 pub payload: Option<serde_json::Value>,
422}
423
424const LONG_SCALE: f64 = 0xFFFFFFFFFFFFFFFu64 as f64; pub fn hash_key(key: &str, distinct_id: &str, salt: &str) -> f64 {
432 let hash_key = format!("{key}.{distinct_id}{salt}");
433 let mut hasher = Sha1::new();
434 hasher.update(hash_key.as_bytes());
435 let result = hasher.finalize();
436 let hex_str = format!("{result:x}");
437 let hash_val = u64::from_str_radix(&hex_str[..15], 16).unwrap_or(0);
438 hash_val as f64 / LONG_SCALE
439}
440
441pub fn get_matching_variant(flag: &FeatureFlag, distinct_id: &str) -> Option<String> {
447 let hash_value = hash_key(&flag.key, distinct_id, VARIANT_HASH_SALT);
448 let variants = flag.filters.multivariate.as_ref()?.variants.as_slice();
449
450 let mut value_min = 0.0;
451 for variant in variants {
452 let value_max = value_min + variant.rollout_percentage / 100.0;
453 if hash_value >= value_min && hash_value < value_max {
454 return Some(variant.key.clone());
455 }
456 value_min = value_max;
457 }
458 None
459}
460
461enum ConditionTarget<'a> {
463 Use {
465 bucketing: String,
466 properties: &'a HashMap<String, serde_json::Value>,
467 },
468 Skip,
470 Inconclusive,
473}
474
475fn resolve_condition_target<'a>(
481 condition: &FeatureFlagCondition,
482 flag_aggregation: Option<i32>,
483 distinct_id: &str,
484 person_properties: &'a HashMap<String, serde_json::Value>,
485 groups: &HashMap<String, String>,
486 group_properties: &'a HashMap<String, HashMap<String, serde_json::Value>>,
487 group_type_mapping: &HashMap<String, String>,
488) -> ConditionTarget<'a> {
489 let effective_aggregation = condition.aggregation_group_type_index.or(flag_aggregation);
492
493 match effective_aggregation {
494 None => ConditionTarget::Use {
495 bucketing: distinct_id.to_string(),
496 properties: person_properties,
497 },
498 Some(idx) => {
499 let key = idx.to_string();
500 let Some(group_type) = group_type_mapping.get(&key) else {
501 return ConditionTarget::Skip;
502 };
503 let Some(group_key) = groups.get(group_type) else {
504 return ConditionTarget::Skip;
505 };
506 let Some(props) = group_properties.get(group_type) else {
507 return ConditionTarget::Inconclusive;
508 };
509 ConditionTarget::Use {
510 bucketing: group_key.clone(),
511 properties: props,
512 }
513 }
514 }
515}
516
517#[must_use = "feature flag evaluation result should be used"]
541#[allow(clippy::too_many_arguments)]
542pub fn match_feature_flag(
543 flag: &FeatureFlag,
544 distinct_id: &str,
545 person_properties: &HashMap<String, serde_json::Value>,
546 groups: &HashMap<String, String>,
547 group_properties: &HashMap<String, HashMap<String, serde_json::Value>>,
548 group_type_mapping: &HashMap<String, String>,
549) -> Result<FlagValue, InconclusiveMatchError> {
550 if !flag.active {
551 return Ok(FlagValue::Boolean(false));
552 }
553
554 let conditions = &flag.filters.groups;
555 let flag_aggregation = flag.filters.aggregation_group_type_index;
556
557 let mut sorted_conditions = conditions.clone();
559 sorted_conditions.sort_by_key(|c| if c.variant.is_some() { 0 } else { 1 });
560
561 let mut is_inconclusive = false;
562
563 for condition in sorted_conditions {
564 let (effective_bucketing, effective_properties) = match resolve_condition_target(
565 &condition,
566 flag_aggregation,
567 distinct_id,
568 person_properties,
569 groups,
570 group_properties,
571 group_type_mapping,
572 ) {
573 ConditionTarget::Use {
574 bucketing,
575 properties,
576 } => (bucketing, properties),
577 ConditionTarget::Skip => continue,
578 ConditionTarget::Inconclusive => {
579 is_inconclusive = true;
580 continue;
581 }
582 };
583
584 match is_condition_match(flag, &effective_bucketing, &condition, effective_properties) {
585 Ok(ConditionMatch::Match) => {
586 if let Some(variant_override) = &condition.variant {
587 if let Some(ref multivariate) = flag.filters.multivariate {
589 let valid_variants: Vec<String> = multivariate
590 .variants
591 .iter()
592 .map(|v| v.key.clone())
593 .collect();
594
595 if valid_variants.contains(variant_override) {
596 return Ok(FlagValue::String(variant_override.clone()));
597 }
598 }
599 }
600
601 if let Some(variant) = get_matching_variant(flag, &effective_bucketing) {
603 return Ok(FlagValue::String(variant));
604 }
605 return Ok(FlagValue::Boolean(true));
606 }
607 Ok(ConditionMatch::OutOfRolloutBound) => {
608 if flag.filters.early_exit && !is_inconclusive {
615 return Ok(FlagValue::Boolean(false));
616 }
617 }
618 Ok(ConditionMatch::NoMatch) => continue,
619 Err(_) => {
620 is_inconclusive = true;
621 }
622 }
623 }
624
625 if is_inconclusive {
626 return Err(InconclusiveMatchError::new(
627 "Can't determine if feature flag is enabled or not with given properties",
628 ));
629 }
630
631 Ok(FlagValue::Boolean(false))
632}
633
634#[derive(Debug, Clone, Copy, PartialEq, Eq)]
639enum ConditionMatch {
640 Match,
643 NoMatch,
645 OutOfRolloutBound,
648}
649
650fn is_condition_match(
651 flag: &FeatureFlag,
652 bucketing_id: &str,
653 condition: &FeatureFlagCondition,
654 properties: &HashMap<String, serde_json::Value>,
655) -> Result<ConditionMatch, InconclusiveMatchError> {
656 for prop in &condition.properties {
658 if !match_property(prop, properties)? {
659 return Ok(ConditionMatch::NoMatch);
660 }
661 }
662
663 if let Some(rollout_percentage) = condition.rollout_percentage {
665 let hash_value = hash_key(&flag.key, bucketing_id, ROLLOUT_HASH_SALT);
666 if hash_value > (rollout_percentage / 100.0) {
667 return Ok(ConditionMatch::OutOfRolloutBound);
668 }
669 }
670
671 Ok(ConditionMatch::Match)
672}
673
674#[must_use = "feature flag evaluation result should be used"]
686pub fn match_feature_flag_with_context(
687 flag: &FeatureFlag,
688 person_properties: &HashMap<String, serde_json::Value>,
689 ctx: &EvaluationContext,
690) -> Result<FlagValue, InconclusiveMatchError> {
691 if !flag.active {
692 return Ok(FlagValue::Boolean(false));
693 }
694
695 let conditions = &flag.filters.groups;
696 let flag_aggregation = flag.filters.aggregation_group_type_index;
697
698 let mut sorted_conditions = conditions.clone();
700 sorted_conditions.sort_by_key(|c| if c.variant.is_some() { 0 } else { 1 });
701
702 let mut is_inconclusive = false;
703
704 for condition in sorted_conditions {
705 let (effective_bucketing, effective_properties) = match resolve_condition_target(
706 &condition,
707 flag_aggregation,
708 ctx.distinct_id,
709 person_properties,
710 ctx.groups,
711 ctx.group_properties,
712 ctx.group_type_mapping,
713 ) {
714 ConditionTarget::Use {
715 bucketing,
716 properties,
717 } => (bucketing, properties),
718 ConditionTarget::Skip => continue,
719 ConditionTarget::Inconclusive => {
720 is_inconclusive = true;
721 continue;
722 }
723 };
724
725 match is_condition_match_with_context(
726 flag,
727 &effective_bucketing,
728 &condition,
729 effective_properties,
730 ctx,
731 ) {
732 Ok(ConditionMatch::Match) => {
733 if let Some(variant_override) = &condition.variant {
734 if let Some(ref multivariate) = flag.filters.multivariate {
736 let valid_variants: Vec<String> = multivariate
737 .variants
738 .iter()
739 .map(|v| v.key.clone())
740 .collect();
741
742 if valid_variants.contains(variant_override) {
743 return Ok(FlagValue::String(variant_override.clone()));
744 }
745 }
746 }
747
748 if let Some(variant) = get_matching_variant(flag, &effective_bucketing) {
750 return Ok(FlagValue::String(variant));
751 }
752 return Ok(FlagValue::Boolean(true));
753 }
754 Ok(ConditionMatch::OutOfRolloutBound) => {
755 if flag.filters.early_exit && !is_inconclusive {
762 return Ok(FlagValue::Boolean(false));
763 }
764 }
765 Ok(ConditionMatch::NoMatch) => continue,
766 Err(_) => {
767 is_inconclusive = true;
768 }
769 }
770 }
771
772 if is_inconclusive {
773 return Err(InconclusiveMatchError::new(
774 "Can't determine if feature flag is enabled or not with given properties",
775 ));
776 }
777
778 Ok(FlagValue::Boolean(false))
779}
780
781fn is_condition_match_with_context(
782 flag: &FeatureFlag,
783 bucketing_id: &str,
784 condition: &FeatureFlagCondition,
785 properties: &HashMap<String, serde_json::Value>,
786 ctx: &EvaluationContext,
787) -> Result<ConditionMatch, InconclusiveMatchError> {
788 for prop in &condition.properties {
790 if !match_property_with_context(prop, properties, ctx)? {
791 return Ok(ConditionMatch::NoMatch);
792 }
793 }
794
795 if let Some(rollout_percentage) = condition.rollout_percentage {
797 let hash_value = hash_key(&flag.key, bucketing_id, ROLLOUT_HASH_SALT);
798 if hash_value > (rollout_percentage / 100.0) {
799 return Ok(ConditionMatch::OutOfRolloutBound);
800 }
801 }
802
803 Ok(ConditionMatch::Match)
804}
805
806pub fn match_property_with_context(
817 property: &Property,
818 properties: &HashMap<String, serde_json::Value>,
819 ctx: &EvaluationContext,
820) -> Result<bool, InconclusiveMatchError> {
821 if property.property_type.as_deref() == Some("cohort") {
823 return match_cohort_property(property, properties, ctx);
824 }
825
826 if property.key.starts_with("$feature/") {
828 return match_flag_dependency_property(property, ctx);
829 }
830
831 match_property(property, properties)
833}
834
835fn match_cohort_property(
837 property: &Property,
838 properties: &HashMap<String, serde_json::Value>,
839 ctx: &EvaluationContext,
840) -> Result<bool, InconclusiveMatchError> {
841 let cohort_id = property
842 .value
843 .as_str()
844 .ok_or_else(|| InconclusiveMatchError::new("Cohort ID must be a string"))?;
845
846 let cohort = ctx.cohorts.get(cohort_id).ok_or_else(|| {
847 InconclusiveMatchError::new(&format!("Cohort '{}' not found in local cache", cohort_id))
848 })?;
849
850 let cohort_properties = cohort.parse_properties();
852 let mut is_in_cohort = true;
853 for cohort_prop in &cohort_properties {
854 match match_property(cohort_prop, properties) {
855 Ok(true) => continue,
856 Ok(false) => {
857 is_in_cohort = false;
858 break;
859 }
860 Err(e) => {
861 return Err(InconclusiveMatchError::new(&format!(
863 "Cannot evaluate cohort '{}' property '{}': {}",
864 cohort_id, cohort_prop.key, e.message
865 )));
866 }
867 }
868 }
869
870 Ok(match property.operator.as_str() {
872 "in" => is_in_cohort,
873 "not_in" => !is_in_cohort,
874 op => {
875 return Err(InconclusiveMatchError::new(&format!(
876 "Unknown cohort operator: {}",
877 op
878 )));
879 }
880 })
881}
882
883fn match_flag_dependency_property(
885 property: &Property,
886 ctx: &EvaluationContext,
887) -> Result<bool, InconclusiveMatchError> {
888 let flag_key = property
890 .key
891 .strip_prefix("$feature/")
892 .ok_or_else(|| InconclusiveMatchError::new("Invalid flag dependency format"))?;
893
894 let flag = ctx.flags.get(flag_key).ok_or_else(|| {
895 InconclusiveMatchError::new(&format!("Flag '{}' not found in local cache", flag_key))
896 })?;
897
898 let empty_props = HashMap::new();
901 let flag_value = match_feature_flag(
902 flag,
903 ctx.distinct_id,
904 &empty_props,
905 ctx.groups,
906 ctx.group_properties,
907 ctx.group_type_mapping,
908 )?;
909
910 let expected = &property.value;
912
913 let matches = match (&flag_value, expected) {
914 (FlagValue::Boolean(b), serde_json::Value::Bool(expected_b)) => b == expected_b,
915 (FlagValue::String(s), serde_json::Value::String(expected_s)) => {
916 s.eq_ignore_ascii_case(expected_s)
917 }
918 (FlagValue::Boolean(true), serde_json::Value::String(s)) => {
919 s.is_empty() || s == "true"
922 }
923 (FlagValue::Boolean(false), serde_json::Value::String(s)) => s.is_empty() || s == "false",
924 (FlagValue::String(s), serde_json::Value::Bool(true)) => {
925 !s.is_empty()
927 }
928 (FlagValue::String(_), serde_json::Value::Bool(false)) => false,
929 _ => false,
930 };
931
932 Ok(match property.operator.as_str() {
934 "exact" => matches,
935 "is_not" => !matches,
936 op => {
937 return Err(InconclusiveMatchError::new(&format!(
938 "Unknown flag dependency operator: {}",
939 op
940 )));
941 }
942 })
943}
944
945fn parse_relative_date(value: &str) -> Option<DateTime<Utc>> {
948 let value = value.trim();
949 if value.len() < 3 || !value.starts_with('-') {
951 return None;
952 }
953
954 let (num_str, unit) = value[1..].split_at(value.len() - 2);
955 let num: i64 = num_str.parse().ok()?;
956
957 let duration = match unit {
958 "h" => chrono::Duration::hours(num),
959 "d" => chrono::Duration::days(num),
960 "w" => chrono::Duration::weeks(num),
961 "m" => chrono::Duration::days(num * 30), "y" => chrono::Duration::days(num * 365), _ => return None,
964 };
965
966 Some(Utc::now() - duration)
967}
968
969fn parse_date_value(value: &serde_json::Value) -> Option<DateTime<Utc>> {
971 let date_str = value.as_str()?;
972
973 if date_str.starts_with('-') && date_str.len() > 1 {
975 if let Some(dt) = parse_relative_date(date_str) {
976 return Some(dt);
977 }
978 }
979
980 if let Ok(dt) = DateTime::parse_from_rfc3339(date_str) {
982 return Some(dt.with_timezone(&Utc));
983 }
984
985 if let Ok(date) = NaiveDate::parse_from_str(date_str, "%Y-%m-%d") {
987 return Some(
988 date.and_hms_opt(0, 0, 0)
989 .expect("midnight is always valid")
990 .and_utc(),
991 );
992 }
993
994 None
995}
996
997type SemverTuple = (u64, u64, u64);
999
1000fn parse_semver(value: &str) -> Option<SemverTuple> {
1012 let value = value.trim();
1013 if value.is_empty() {
1014 return None;
1015 }
1016
1017 let value = value
1019 .strip_prefix('v')
1020 .or_else(|| value.strip_prefix('V'))
1021 .unwrap_or(value);
1022 if value.is_empty() {
1023 return None;
1024 }
1025
1026 let value = value.split(['-', '+']).next().unwrap_or(value);
1028 if value.is_empty() {
1029 return None;
1030 }
1031
1032 if value.starts_with('.') {
1034 return None;
1035 }
1036
1037 let parts: Vec<&str> = value.split('.').collect();
1039 if parts.is_empty() {
1040 return None;
1041 }
1042
1043 let major = parse_semver_numeric(parts.first()?)?;
1044 let minor = parts.get(1).map_or(Some(0), |s| parse_semver_numeric(s))?;
1045 let patch = parts.get(2).map_or(Some(0), |s| parse_semver_numeric(s))?;
1046
1047 Some((major, minor, patch))
1048}
1049
1050fn parse_semver_numeric(part: &str) -> Option<u64> {
1055 if part.is_empty() || !part.bytes().all(|b| b.is_ascii_digit()) {
1056 return None;
1057 }
1058 if part.len() > 1 && part.starts_with('0') {
1059 return None;
1060 }
1061 part.parse().ok()
1062}
1063
1064fn parse_semver_wildcard(pattern: &str) -> Option<(SemverTuple, SemverTuple)> {
1067 let pattern = pattern.trim();
1068 if pattern.is_empty() {
1069 return None;
1070 }
1071
1072 let pattern = pattern
1074 .strip_prefix('v')
1075 .or_else(|| pattern.strip_prefix('V'))
1076 .unwrap_or(pattern);
1077 if pattern.is_empty() {
1078 return None;
1079 }
1080
1081 let parts: Vec<&str> = pattern.split('.').collect();
1082
1083 match parts.as_slice() {
1084 [major_str, "*"] => {
1086 let major = parse_semver_numeric(major_str)?;
1087 Some(((major, 0, 0), (major + 1, 0, 0)))
1088 }
1089 [major_str, minor_str, "*"] => {
1091 let major = parse_semver_numeric(major_str)?;
1092 let minor = parse_semver_numeric(minor_str)?;
1093 Some(((major, minor, 0), (major, minor + 1, 0)))
1094 }
1095 _ => None,
1096 }
1097}
1098
1099fn compute_tilde_bounds(version: SemverTuple) -> (SemverTuple, SemverTuple) {
1101 let (major, minor, patch) = version;
1102 ((major, minor, patch), (major, minor + 1, 0))
1103}
1104
1105fn compute_caret_bounds(version: SemverTuple) -> (SemverTuple, SemverTuple) {
1110 let (major, minor, patch) = version;
1111 if major > 0 {
1112 ((major, minor, patch), (major + 1, 0, 0))
1113 } else if minor > 0 {
1114 ((0, minor, patch), (0, minor + 1, 0))
1115 } else {
1116 ((0, 0, patch), (0, 0, patch + 1))
1117 }
1118}
1119
1120fn parse_target_semver(
1121 target_value: &serde_json::Value,
1122) -> Result<SemverTuple, InconclusiveMatchError> {
1123 let target_str = value_to_string(target_value);
1124 parse_semver(&target_str).ok_or_else(|| {
1125 InconclusiveMatchError::new(&format!(
1126 "Unable to parse target semver value: {:?}",
1127 target_value
1128 ))
1129 })
1130}
1131
1132fn match_property(
1133 property: &Property,
1134 properties: &HashMap<String, serde_json::Value>,
1135) -> Result<bool, InconclusiveMatchError> {
1136 let value = match properties.get(&property.key) {
1137 Some(v) => v,
1138 None => {
1139 if property.operator == "is_not_set" {
1141 return Ok(true);
1142 }
1143 if property.operator == "is_set" {
1145 return Ok(false);
1146 }
1147 return Err(InconclusiveMatchError::new(&format!(
1149 "Property '{}' not found in provided properties",
1150 property.key
1151 )));
1152 }
1153 };
1154
1155 let parse_property_semver = || {
1156 let prop_str = value_to_string(value);
1157 parse_semver(&prop_str).ok_or_else(|| {
1158 InconclusiveMatchError::new(&format!(
1159 "Unable to parse property semver value for '{}': {:?}",
1160 property.key, value
1161 ))
1162 })
1163 };
1164 let parse_semver_operands = || {
1165 Ok((
1166 parse_property_semver()?,
1167 parse_target_semver(&property.value)?,
1168 ))
1169 };
1170
1171 Ok(match property.operator.as_str() {
1172 "exact" => {
1173 if property.value.is_array() {
1174 if let Some(arr) = property.value.as_array() {
1175 for val in arr {
1176 if compare_values(val, value) {
1177 return Ok(true);
1178 }
1179 }
1180 return Ok(false);
1181 }
1182 }
1183 compare_values(&property.value, value)
1184 }
1185 "is_not" => {
1186 if property.value.is_array() {
1187 if let Some(arr) = property.value.as_array() {
1188 for val in arr {
1189 if compare_values(val, value) {
1190 return Ok(false);
1191 }
1192 }
1193 return Ok(true);
1194 }
1195 }
1196 !compare_values(&property.value, value)
1197 }
1198 "is_set" => true, "is_not_set" => false, "icontains" => {
1201 let prop_str = value_to_string(value);
1202 let search_str = value_to_string(&property.value);
1203 prop_str.to_lowercase().contains(&search_str.to_lowercase())
1204 }
1205 "not_icontains" => {
1206 let prop_str = value_to_string(value);
1207 let search_str = value_to_string(&property.value);
1208 !prop_str.to_lowercase().contains(&search_str.to_lowercase())
1209 }
1210 "regex" => {
1211 let prop_str = value_to_string(value);
1212 let regex_str = value_to_string(&property.value);
1213 get_cached_regex(®ex_str)
1214 .map(|re| re.is_match(&prop_str))
1215 .unwrap_or(false)
1216 }
1217 "not_regex" => {
1218 let prop_str = value_to_string(value);
1219 let regex_str = value_to_string(&property.value);
1220 get_cached_regex(®ex_str)
1221 .map(|re| !re.is_match(&prop_str))
1222 .unwrap_or(true)
1223 }
1224 "gt" | "gte" | "lt" | "lte" => compare_numeric(&property.operator, &property.value, value),
1225 "is_date_before" | "is_date_after" => {
1226 let target_date = parse_date_value(&property.value).ok_or_else(|| {
1227 InconclusiveMatchError::new(&format!(
1228 "Unable to parse target date value: {:?}",
1229 property.value
1230 ))
1231 })?;
1232
1233 let prop_date = parse_date_value(value).ok_or_else(|| {
1234 InconclusiveMatchError::new(&format!(
1235 "Unable to parse property date value for '{}': {:?}",
1236 property.key, value
1237 ))
1238 })?;
1239
1240 if property.operator == "is_date_before" {
1241 prop_date < target_date
1242 } else {
1243 prop_date > target_date
1244 }
1245 }
1246 "semver_eq" | "semver_neq" | "semver_gt" | "semver_gte" | "semver_lt" | "semver_lte" => {
1248 let (prop_version, target_version) = parse_semver_operands()?;
1249
1250 match property.operator.as_str() {
1251 "semver_eq" => prop_version == target_version,
1252 "semver_neq" => prop_version != target_version,
1253 "semver_gt" => prop_version > target_version,
1254 "semver_gte" => prop_version >= target_version,
1255 "semver_lt" => prop_version < target_version,
1256 "semver_lte" => prop_version <= target_version,
1257 _ => unreachable!(),
1258 }
1259 }
1260 "semver_tilde" => {
1261 let (prop_version, target_version) = parse_semver_operands()?;
1262 let (lower, upper) = compute_tilde_bounds(target_version);
1263 prop_version >= lower && prop_version < upper
1264 }
1265 "semver_caret" => {
1266 let (prop_version, target_version) = parse_semver_operands()?;
1267 let (lower, upper) = compute_caret_bounds(target_version);
1268 prop_version >= lower && prop_version < upper
1269 }
1270 "semver_wildcard" => {
1271 let prop_version = parse_property_semver()?;
1272 let target_str = value_to_string(&property.value);
1273
1274 let (lower, upper) = parse_semver_wildcard(&target_str).ok_or_else(|| {
1275 InconclusiveMatchError::new(&format!(
1276 "Unable to parse target semver wildcard pattern: {:?}",
1277 property.value
1278 ))
1279 })?;
1280
1281 prop_version >= lower && prop_version < upper
1282 }
1283 unknown => {
1284 return Err(InconclusiveMatchError::new(&format!(
1285 "Unknown operator: {}",
1286 unknown
1287 )));
1288 }
1289 })
1290}
1291
1292fn compare_values(a: &serde_json::Value, b: &serde_json::Value) -> bool {
1293 if let (Some(a_str), Some(b_str)) = (a.as_str(), b.as_str()) {
1295 return a_str.eq_ignore_ascii_case(b_str);
1296 }
1297
1298 a == b
1300}
1301
1302fn value_to_string(value: &serde_json::Value) -> String {
1303 match value {
1304 serde_json::Value::String(s) => s.clone(),
1305 serde_json::Value::Number(n) => n.to_string(),
1306 serde_json::Value::Bool(b) => b.to_string(),
1307 _ => value.to_string(),
1308 }
1309}
1310
1311fn compare_numeric(
1312 operator: &str,
1313 property_value: &serde_json::Value,
1314 value: &serde_json::Value,
1315) -> bool {
1316 let prop_num = match property_value {
1317 serde_json::Value::Number(n) => n.as_f64(),
1318 serde_json::Value::String(s) => s.parse::<f64>().ok(),
1319 _ => None,
1320 };
1321
1322 let val_num = match value {
1323 serde_json::Value::Number(n) => n.as_f64(),
1324 serde_json::Value::String(s) => s.parse::<f64>().ok(),
1325 _ => None,
1326 };
1327
1328 if let (Some(prop), Some(val)) = (prop_num, val_num) {
1329 match operator {
1330 "gt" => val > prop,
1331 "gte" => val >= prop,
1332 "lt" => val < prop,
1333 "lte" => val <= prop,
1334 _ => false,
1335 }
1336 } else {
1337 let prop_str = value_to_string(property_value);
1339 let val_str = value_to_string(value);
1340 match operator {
1341 "gt" => val_str > prop_str,
1342 "gte" => val_str >= prop_str,
1343 "lt" => val_str < prop_str,
1344 "lte" => val_str <= prop_str,
1345 _ => false,
1346 }
1347 }
1348}
1349
1350#[cfg(test)]
1351mod tests {
1352 use super::*;
1353 use serde_json::json;
1354
1355 const TEST_SALT: &str = "test-salt";
1357
1358 #[test]
1359 fn test_hash_key() {
1360 let hash = hash_key("test-flag", "user-123", TEST_SALT);
1361 assert!((0.0..=1.0).contains(&hash));
1362
1363 let hash2 = hash_key("test-flag", "user-123", TEST_SALT);
1365 assert_eq!(hash, hash2);
1366
1367 let hash3 = hash_key("test-flag", "user-456", TEST_SALT);
1369 assert_ne!(hash, hash3);
1370 }
1371
1372 #[test]
1373 fn test_simple_flag_match() {
1374 let flag = FeatureFlag {
1375 key: "test-flag".to_string(),
1376 active: true,
1377 filters: FeatureFlagFilters {
1378 groups: vec![FeatureFlagCondition {
1379 properties: vec![],
1380 rollout_percentage: Some(100.0),
1381 variant: None,
1382 aggregation_group_type_index: None,
1383 }],
1384 multivariate: None,
1385 payloads: HashMap::new(),
1386 aggregation_group_type_index: None,
1387 early_exit: false,
1388 },
1389 };
1390
1391 let properties = HashMap::new();
1392 let result = match_feature_flag(
1393 &flag,
1394 "user-123",
1395 &properties,
1396 &HashMap::new(),
1397 &HashMap::new(),
1398 &HashMap::new(),
1399 )
1400 .unwrap();
1401 assert_eq!(result, FlagValue::Boolean(true));
1402 }
1403
1404 #[test]
1405 fn test_property_matching() {
1406 let prop = Property {
1407 key: "country".to_string(),
1408 value: json!("US"),
1409 operator: "exact".to_string(),
1410 property_type: None,
1411 };
1412
1413 let mut properties = HashMap::new();
1414 properties.insert("country".to_string(), json!("US"));
1415
1416 assert!(match_property(&prop, &properties).unwrap());
1417
1418 properties.insert("country".to_string(), json!("UK"));
1419 assert!(!match_property(&prop, &properties).unwrap());
1420 }
1421
1422 #[test]
1423 fn test_multivariate_variants() {
1424 let flag = FeatureFlag {
1425 key: "test-flag".to_string(),
1426 active: true,
1427 filters: FeatureFlagFilters {
1428 groups: vec![FeatureFlagCondition {
1429 properties: vec![],
1430 rollout_percentage: Some(100.0),
1431 variant: None,
1432 aggregation_group_type_index: None,
1433 }],
1434 multivariate: Some(MultivariateFilter {
1435 variants: vec![
1436 MultivariateVariant {
1437 key: "control".to_string(),
1438 rollout_percentage: 50.0,
1439 },
1440 MultivariateVariant {
1441 key: "test".to_string(),
1442 rollout_percentage: 50.0,
1443 },
1444 ],
1445 }),
1446 payloads: HashMap::new(),
1447 aggregation_group_type_index: None,
1448 early_exit: false,
1449 },
1450 };
1451
1452 let properties = HashMap::new();
1453 let result = match_feature_flag(
1454 &flag,
1455 "user-123",
1456 &properties,
1457 &HashMap::new(),
1458 &HashMap::new(),
1459 &HashMap::new(),
1460 )
1461 .unwrap();
1462
1463 match result {
1464 FlagValue::String(variant) => {
1465 assert!(variant == "control" || variant == "test");
1466 }
1467 _ => panic!("Expected string variant"),
1468 }
1469 }
1470
1471 #[test]
1472 fn test_inactive_flag() {
1473 let flag = FeatureFlag {
1474 key: "inactive-flag".to_string(),
1475 active: false,
1476 filters: FeatureFlagFilters {
1477 groups: vec![FeatureFlagCondition {
1478 properties: vec![],
1479 rollout_percentage: Some(100.0),
1480 variant: None,
1481 aggregation_group_type_index: None,
1482 }],
1483 multivariate: None,
1484 payloads: HashMap::new(),
1485 aggregation_group_type_index: None,
1486 early_exit: false,
1487 },
1488 };
1489
1490 let properties = HashMap::new();
1491 let result = match_feature_flag(
1492 &flag,
1493 "user-123",
1494 &properties,
1495 &HashMap::new(),
1496 &HashMap::new(),
1497 &HashMap::new(),
1498 )
1499 .unwrap();
1500 assert_eq!(result, FlagValue::Boolean(false));
1501 }
1502
1503 #[test]
1504 fn test_rollout_percentage() {
1505 let flag = FeatureFlag {
1506 key: "rollout-flag".to_string(),
1507 active: true,
1508 filters: FeatureFlagFilters {
1509 groups: vec![FeatureFlagCondition {
1510 properties: vec![],
1511 rollout_percentage: Some(30.0), variant: None,
1513 aggregation_group_type_index: None,
1514 }],
1515 multivariate: None,
1516 payloads: HashMap::new(),
1517 aggregation_group_type_index: None,
1518 early_exit: false,
1519 },
1520 };
1521
1522 let properties = HashMap::new();
1523
1524 let mut enabled_count = 0;
1526 for i in 0..1000 {
1527 let result = match_feature_flag(
1528 &flag,
1529 &format!("user-{}", i),
1530 &properties,
1531 &HashMap::new(),
1532 &HashMap::new(),
1533 &HashMap::new(),
1534 )
1535 .unwrap();
1536 if result == FlagValue::Boolean(true) {
1537 enabled_count += 1;
1538 }
1539 }
1540
1541 assert!(enabled_count > 250 && enabled_count < 350);
1543 }
1544
1545 #[test]
1546 fn test_regex_operator() {
1547 let prop = Property {
1548 key: "email".to_string(),
1549 value: json!(".*@company\\.com$"),
1550 operator: "regex".to_string(),
1551 property_type: None,
1552 };
1553
1554 let mut properties = HashMap::new();
1555 properties.insert("email".to_string(), json!("user@company.com"));
1556 assert!(match_property(&prop, &properties).unwrap());
1557
1558 properties.insert("email".to_string(), json!("user@example.com"));
1559 assert!(!match_property(&prop, &properties).unwrap());
1560 }
1561
1562 #[test]
1563 fn test_icontains_operator() {
1564 let prop = Property {
1565 key: "name".to_string(),
1566 value: json!("ADMIN"),
1567 operator: "icontains".to_string(),
1568 property_type: None,
1569 };
1570
1571 let mut properties = HashMap::new();
1572 properties.insert("name".to_string(), json!("admin_user"));
1573 assert!(match_property(&prop, &properties).unwrap());
1574
1575 properties.insert("name".to_string(), json!("regular_user"));
1576 assert!(!match_property(&prop, &properties).unwrap());
1577 }
1578
1579 #[test]
1580 fn test_numeric_operators() {
1581 let prop_gt = Property {
1583 key: "age".to_string(),
1584 value: json!(18),
1585 operator: "gt".to_string(),
1586 property_type: None,
1587 };
1588
1589 let mut properties = HashMap::new();
1590 properties.insert("age".to_string(), json!(25));
1591 assert!(match_property(&prop_gt, &properties).unwrap());
1592
1593 properties.insert("age".to_string(), json!(15));
1594 assert!(!match_property(&prop_gt, &properties).unwrap());
1595
1596 let prop_lte = Property {
1598 key: "score".to_string(),
1599 value: json!(100),
1600 operator: "lte".to_string(),
1601 property_type: None,
1602 };
1603
1604 properties.insert("score".to_string(), json!(100));
1605 assert!(match_property(&prop_lte, &properties).unwrap());
1606
1607 properties.insert("score".to_string(), json!(101));
1608 assert!(!match_property(&prop_lte, &properties).unwrap());
1609 }
1610
1611 #[test]
1612 fn test_is_set_operator() {
1613 let prop = Property {
1614 key: "email".to_string(),
1615 value: json!(true),
1616 operator: "is_set".to_string(),
1617 property_type: None,
1618 };
1619
1620 let mut properties = HashMap::new();
1621 properties.insert("email".to_string(), json!("test@example.com"));
1622 assert!(match_property(&prop, &properties).unwrap());
1623
1624 properties.remove("email");
1625 assert!(!match_property(&prop, &properties).unwrap());
1626 }
1627
1628 #[test]
1629 fn test_is_not_set_operator() {
1630 let prop = Property {
1631 key: "phone".to_string(),
1632 value: json!(true),
1633 operator: "is_not_set".to_string(),
1634 property_type: None,
1635 };
1636
1637 let mut properties = HashMap::new();
1638 assert!(match_property(&prop, &properties).unwrap());
1639
1640 properties.insert("phone".to_string(), json!("+1234567890"));
1641 assert!(!match_property(&prop, &properties).unwrap());
1642 }
1643
1644 #[test]
1645 fn test_empty_groups() {
1646 let flag = FeatureFlag {
1647 key: "empty-groups".to_string(),
1648 active: true,
1649 filters: FeatureFlagFilters {
1650 groups: vec![],
1651 multivariate: None,
1652 payloads: HashMap::new(),
1653 aggregation_group_type_index: None,
1654 early_exit: false,
1655 },
1656 };
1657
1658 let properties = HashMap::new();
1659 let result = match_feature_flag(
1660 &flag,
1661 "user-123",
1662 &properties,
1663 &HashMap::new(),
1664 &HashMap::new(),
1665 &HashMap::new(),
1666 )
1667 .unwrap();
1668 assert_eq!(result, FlagValue::Boolean(false));
1669 }
1670
1671 #[test]
1672 fn test_hash_scale_constant() {
1673 assert_eq!(LONG_SCALE, 0xFFFFFFFFFFFFFFFu64 as f64);
1675 assert_ne!(LONG_SCALE, 0xFFFFFFFFFFFFFFFFu64 as f64);
1676 }
1677
1678 #[test]
1681 fn test_unknown_operator_returns_inconclusive_error() {
1682 let prop = Property {
1683 key: "status".to_string(),
1684 value: json!("active"),
1685 operator: "unknown_operator".to_string(),
1686 property_type: None,
1687 };
1688
1689 let mut properties = HashMap::new();
1690 properties.insert("status".to_string(), json!("active"));
1691
1692 let result = match_property(&prop, &properties);
1693 assert!(result.is_err());
1694 let err = result.unwrap_err();
1695 assert!(err.message.contains("unknown_operator"));
1696 }
1697
1698 #[test]
1699 fn test_is_date_before_with_relative_date() {
1700 let prop = Property {
1701 key: "signup_date".to_string(),
1702 value: json!("-7d"), operator: "is_date_before".to_string(),
1704 property_type: None,
1705 };
1706
1707 let mut properties = HashMap::new();
1708 let ten_days_ago = chrono::Utc::now() - chrono::Duration::days(10);
1710 properties.insert(
1711 "signup_date".to_string(),
1712 json!(ten_days_ago.format("%Y-%m-%d").to_string()),
1713 );
1714 assert!(match_property(&prop, &properties).unwrap());
1715
1716 let three_days_ago = chrono::Utc::now() - chrono::Duration::days(3);
1718 properties.insert(
1719 "signup_date".to_string(),
1720 json!(three_days_ago.format("%Y-%m-%d").to_string()),
1721 );
1722 assert!(!match_property(&prop, &properties).unwrap());
1723 }
1724
1725 #[test]
1726 fn test_is_date_after_with_relative_date() {
1727 let prop = Property {
1728 key: "last_seen".to_string(),
1729 value: json!("-30d"), operator: "is_date_after".to_string(),
1731 property_type: None,
1732 };
1733
1734 let mut properties = HashMap::new();
1735 let ten_days_ago = chrono::Utc::now() - chrono::Duration::days(10);
1737 properties.insert(
1738 "last_seen".to_string(),
1739 json!(ten_days_ago.format("%Y-%m-%d").to_string()),
1740 );
1741 assert!(match_property(&prop, &properties).unwrap());
1742
1743 let sixty_days_ago = chrono::Utc::now() - chrono::Duration::days(60);
1745 properties.insert(
1746 "last_seen".to_string(),
1747 json!(sixty_days_ago.format("%Y-%m-%d").to_string()),
1748 );
1749 assert!(!match_property(&prop, &properties).unwrap());
1750 }
1751
1752 #[test]
1753 fn test_is_date_before_with_iso_date() {
1754 let prop = Property {
1755 key: "expiry_date".to_string(),
1756 value: json!("2024-06-15"),
1757 operator: "is_date_before".to_string(),
1758 property_type: None,
1759 };
1760
1761 let mut properties = HashMap::new();
1762 properties.insert("expiry_date".to_string(), json!("2024-06-10"));
1763 assert!(match_property(&prop, &properties).unwrap());
1764
1765 properties.insert("expiry_date".to_string(), json!("2024-06-20"));
1766 assert!(!match_property(&prop, &properties).unwrap());
1767 }
1768
1769 #[test]
1770 fn test_is_date_after_with_iso_date() {
1771 let prop = Property {
1772 key: "start_date".to_string(),
1773 value: json!("2024-01-01"),
1774 operator: "is_date_after".to_string(),
1775 property_type: None,
1776 };
1777
1778 let mut properties = HashMap::new();
1779 properties.insert("start_date".to_string(), json!("2024-03-15"));
1780 assert!(match_property(&prop, &properties).unwrap());
1781
1782 properties.insert("start_date".to_string(), json!("2023-12-01"));
1783 assert!(!match_property(&prop, &properties).unwrap());
1784 }
1785
1786 #[test]
1787 fn test_is_date_with_relative_hours() {
1788 let prop = Property {
1789 key: "last_active".to_string(),
1790 value: json!("-24h"), operator: "is_date_after".to_string(),
1792 property_type: None,
1793 };
1794
1795 let mut properties = HashMap::new();
1796 let twelve_hours_ago = chrono::Utc::now() - chrono::Duration::hours(12);
1798 properties.insert(
1799 "last_active".to_string(),
1800 json!(twelve_hours_ago.to_rfc3339()),
1801 );
1802 assert!(match_property(&prop, &properties).unwrap());
1803
1804 let forty_eight_hours_ago = chrono::Utc::now() - chrono::Duration::hours(48);
1806 properties.insert(
1807 "last_active".to_string(),
1808 json!(forty_eight_hours_ago.to_rfc3339()),
1809 );
1810 assert!(!match_property(&prop, &properties).unwrap());
1811 }
1812
1813 #[test]
1814 fn test_is_date_with_relative_weeks() {
1815 let prop = Property {
1816 key: "joined".to_string(),
1817 value: json!("-2w"), operator: "is_date_before".to_string(),
1819 property_type: None,
1820 };
1821
1822 let mut properties = HashMap::new();
1823 let three_weeks_ago = chrono::Utc::now() - chrono::Duration::weeks(3);
1825 properties.insert(
1826 "joined".to_string(),
1827 json!(three_weeks_ago.format("%Y-%m-%d").to_string()),
1828 );
1829 assert!(match_property(&prop, &properties).unwrap());
1830
1831 let one_week_ago = chrono::Utc::now() - chrono::Duration::weeks(1);
1833 properties.insert(
1834 "joined".to_string(),
1835 json!(one_week_ago.format("%Y-%m-%d").to_string()),
1836 );
1837 assert!(!match_property(&prop, &properties).unwrap());
1838 }
1839
1840 #[test]
1841 fn test_is_date_with_relative_months() {
1842 let prop = Property {
1843 key: "subscription_date".to_string(),
1844 value: json!("-3m"), operator: "is_date_after".to_string(),
1846 property_type: None,
1847 };
1848
1849 let mut properties = HashMap::new();
1850 let one_month_ago = chrono::Utc::now() - chrono::Duration::days(30);
1852 properties.insert(
1853 "subscription_date".to_string(),
1854 json!(one_month_ago.format("%Y-%m-%d").to_string()),
1855 );
1856 assert!(match_property(&prop, &properties).unwrap());
1857
1858 let six_months_ago = chrono::Utc::now() - chrono::Duration::days(180);
1860 properties.insert(
1861 "subscription_date".to_string(),
1862 json!(six_months_ago.format("%Y-%m-%d").to_string()),
1863 );
1864 assert!(!match_property(&prop, &properties).unwrap());
1865 }
1866
1867 #[test]
1868 fn test_is_date_with_relative_years() {
1869 let prop = Property {
1870 key: "created_at".to_string(),
1871 value: json!("-1y"), operator: "is_date_before".to_string(),
1873 property_type: None,
1874 };
1875
1876 let mut properties = HashMap::new();
1877 let two_years_ago = chrono::Utc::now() - chrono::Duration::days(730);
1879 properties.insert(
1880 "created_at".to_string(),
1881 json!(two_years_ago.format("%Y-%m-%d").to_string()),
1882 );
1883 assert!(match_property(&prop, &properties).unwrap());
1884
1885 let six_months_ago = chrono::Utc::now() - chrono::Duration::days(180);
1887 properties.insert(
1888 "created_at".to_string(),
1889 json!(six_months_ago.format("%Y-%m-%d").to_string()),
1890 );
1891 assert!(!match_property(&prop, &properties).unwrap());
1892 }
1893
1894 #[test]
1895 fn test_is_date_with_invalid_date_format() {
1896 let prop = Property {
1897 key: "date".to_string(),
1898 value: json!("-7d"),
1899 operator: "is_date_before".to_string(),
1900 property_type: None,
1901 };
1902
1903 let mut properties = HashMap::new();
1904 properties.insert("date".to_string(), json!("not-a-date"));
1905
1906 let result = match_property(&prop, &properties);
1908 assert!(result.is_err());
1909 }
1910
1911 #[test]
1912 fn test_is_date_with_iso_datetime() {
1913 let prop = Property {
1914 key: "event_time".to_string(),
1915 value: json!("2024-06-15T10:30:00Z"),
1916 operator: "is_date_before".to_string(),
1917 property_type: None,
1918 };
1919
1920 let mut properties = HashMap::new();
1921 properties.insert("event_time".to_string(), json!("2024-06-15T08:00:00Z"));
1922 assert!(match_property(&prop, &properties).unwrap());
1923
1924 properties.insert("event_time".to_string(), json!("2024-06-15T12:00:00Z"));
1925 assert!(!match_property(&prop, &properties).unwrap());
1926 }
1927
1928 #[test]
1931 fn test_cohort_membership_in() {
1932 let mut cohorts = HashMap::new();
1934 cohorts.insert(
1935 "cohort_1".to_string(),
1936 CohortDefinition::new(
1937 "cohort_1".to_string(),
1938 vec![Property {
1939 key: "country".to_string(),
1940 value: json!("US"),
1941 operator: "exact".to_string(),
1942 property_type: None,
1943 }],
1944 ),
1945 );
1946
1947 let prop = Property {
1949 key: "$cohort".to_string(),
1950 value: json!("cohort_1"),
1951 operator: "in".to_string(),
1952 property_type: Some("cohort".to_string()),
1953 };
1954
1955 let mut properties = HashMap::new();
1957 properties.insert("country".to_string(), json!("US"));
1958
1959 let ctx = EvaluationContext {
1960 cohorts: &cohorts,
1961 flags: &HashMap::new(),
1962 distinct_id: "user-123",
1963 groups: &HashMap::new(),
1964 group_properties: &HashMap::new(),
1965 group_type_mapping: &HashMap::new(),
1966 };
1967 assert!(match_property_with_context(&prop, &properties, &ctx).unwrap());
1968
1969 properties.insert("country".to_string(), json!("UK"));
1971 assert!(!match_property_with_context(&prop, &properties, &ctx).unwrap());
1972 }
1973
1974 #[test]
1975 fn test_cohort_membership_not_in() {
1976 let mut cohorts = HashMap::new();
1977 cohorts.insert(
1978 "cohort_blocked".to_string(),
1979 CohortDefinition::new(
1980 "cohort_blocked".to_string(),
1981 vec![Property {
1982 key: "status".to_string(),
1983 value: json!("blocked"),
1984 operator: "exact".to_string(),
1985 property_type: None,
1986 }],
1987 ),
1988 );
1989
1990 let prop = Property {
1991 key: "$cohort".to_string(),
1992 value: json!("cohort_blocked"),
1993 operator: "not_in".to_string(),
1994 property_type: Some("cohort".to_string()),
1995 };
1996
1997 let mut properties = HashMap::new();
1998 properties.insert("status".to_string(), json!("active"));
1999
2000 let ctx = EvaluationContext {
2001 cohorts: &cohorts,
2002 flags: &HashMap::new(),
2003 distinct_id: "user-123",
2004 groups: &HashMap::new(),
2005 group_properties: &HashMap::new(),
2006 group_type_mapping: &HashMap::new(),
2007 };
2008 assert!(match_property_with_context(&prop, &properties, &ctx).unwrap());
2010
2011 properties.insert("status".to_string(), json!("blocked"));
2013 assert!(!match_property_with_context(&prop, &properties, &ctx).unwrap());
2014 }
2015
2016 #[test]
2017 fn test_cohort_not_found_returns_inconclusive() {
2018 let cohorts = HashMap::new(); let prop = Property {
2021 key: "$cohort".to_string(),
2022 value: json!("nonexistent_cohort"),
2023 operator: "in".to_string(),
2024 property_type: Some("cohort".to_string()),
2025 };
2026
2027 let properties = HashMap::new();
2028 let ctx = EvaluationContext {
2029 cohorts: &cohorts,
2030 flags: &HashMap::new(),
2031 distinct_id: "user-123",
2032 groups: &HashMap::new(),
2033 group_properties: &HashMap::new(),
2034 group_type_mapping: &HashMap::new(),
2035 };
2036
2037 let result = match_property_with_context(&prop, &properties, &ctx);
2038 assert!(result.is_err());
2039 assert!(result.unwrap_err().message.contains("Cohort"));
2040 }
2041
2042 #[test]
2045 fn test_flag_dependency_enabled() {
2046 let mut flags = HashMap::new();
2047 flags.insert(
2048 "prerequisite-flag".to_string(),
2049 FeatureFlag {
2050 key: "prerequisite-flag".to_string(),
2051 active: true,
2052 filters: FeatureFlagFilters {
2053 groups: vec![FeatureFlagCondition {
2054 properties: vec![],
2055 rollout_percentage: Some(100.0),
2056 variant: None,
2057 aggregation_group_type_index: None,
2058 }],
2059 multivariate: None,
2060 payloads: HashMap::new(),
2061 aggregation_group_type_index: None,
2062 early_exit: false,
2063 },
2064 },
2065 );
2066
2067 let prop = Property {
2069 key: "$feature/prerequisite-flag".to_string(),
2070 value: json!(true),
2071 operator: "exact".to_string(),
2072 property_type: None,
2073 };
2074
2075 let properties = HashMap::new();
2076 let ctx = EvaluationContext {
2077 cohorts: &HashMap::new(),
2078 flags: &flags,
2079 distinct_id: "user-123",
2080 groups: &HashMap::new(),
2081 group_properties: &HashMap::new(),
2082 group_type_mapping: &HashMap::new(),
2083 };
2084
2085 assert!(match_property_with_context(&prop, &properties, &ctx).unwrap());
2087 }
2088
2089 #[test]
2090 fn test_flag_dependency_disabled() {
2091 let mut flags = HashMap::new();
2092 flags.insert(
2093 "disabled-flag".to_string(),
2094 FeatureFlag {
2095 key: "disabled-flag".to_string(),
2096 active: false, filters: FeatureFlagFilters {
2098 groups: vec![],
2099 multivariate: None,
2100 payloads: HashMap::new(),
2101 aggregation_group_type_index: None,
2102 early_exit: false,
2103 },
2104 },
2105 );
2106
2107 let prop = Property {
2109 key: "$feature/disabled-flag".to_string(),
2110 value: json!(true),
2111 operator: "exact".to_string(),
2112 property_type: None,
2113 };
2114
2115 let properties = HashMap::new();
2116 let ctx = EvaluationContext {
2117 cohorts: &HashMap::new(),
2118 flags: &flags,
2119 distinct_id: "user-123",
2120 groups: &HashMap::new(),
2121 group_properties: &HashMap::new(),
2122 group_type_mapping: &HashMap::new(),
2123 };
2124
2125 assert!(!match_property_with_context(&prop, &properties, &ctx).unwrap());
2127 }
2128
2129 #[test]
2130 fn test_flag_dependency_variant_match() {
2131 let mut flags = HashMap::new();
2132 flags.insert(
2133 "ab-test-flag".to_string(),
2134 FeatureFlag {
2135 key: "ab-test-flag".to_string(),
2136 active: true,
2137 filters: FeatureFlagFilters {
2138 groups: vec![FeatureFlagCondition {
2139 properties: vec![],
2140 rollout_percentage: Some(100.0),
2141 variant: None,
2142 aggregation_group_type_index: None,
2143 }],
2144 multivariate: Some(MultivariateFilter {
2145 variants: vec![
2146 MultivariateVariant {
2147 key: "control".to_string(),
2148 rollout_percentage: 50.0,
2149 },
2150 MultivariateVariant {
2151 key: "test".to_string(),
2152 rollout_percentage: 50.0,
2153 },
2154 ],
2155 }),
2156 payloads: HashMap::new(),
2157 aggregation_group_type_index: None,
2158 early_exit: false,
2159 },
2160 },
2161 );
2162
2163 let prop = Property {
2165 key: "$feature/ab-test-flag".to_string(),
2166 value: json!("control"),
2167 operator: "exact".to_string(),
2168 property_type: None,
2169 };
2170
2171 let properties = HashMap::new();
2172 let ctx = EvaluationContext {
2173 cohorts: &HashMap::new(),
2174 flags: &flags,
2175 distinct_id: "user-gets-control", groups: &HashMap::new(),
2177 group_properties: &HashMap::new(),
2178 group_type_mapping: &HashMap::new(),
2179 };
2180
2181 let result = match_property_with_context(&prop, &properties, &ctx);
2183 assert!(result.is_ok());
2184 }
2185
2186 #[test]
2187 fn test_flag_dependency_not_found_returns_inconclusive() {
2188 let flags = HashMap::new(); let prop = Property {
2191 key: "$feature/nonexistent-flag".to_string(),
2192 value: json!(true),
2193 operator: "exact".to_string(),
2194 property_type: None,
2195 };
2196
2197 let properties = HashMap::new();
2198 let ctx = EvaluationContext {
2199 cohorts: &HashMap::new(),
2200 flags: &flags,
2201 distinct_id: "user-123",
2202 groups: &HashMap::new(),
2203 group_properties: &HashMap::new(),
2204 group_type_mapping: &HashMap::new(),
2205 };
2206
2207 let result = match_property_with_context(&prop, &properties, &ctx);
2208 assert!(result.is_err());
2209 assert!(result.unwrap_err().message.contains("Flag"));
2210 }
2211
2212 #[test]
2215 fn test_parse_relative_date_edge_cases() {
2216 let prop = Property {
2218 key: "date".to_string(),
2219 value: json!("placeholder"),
2220 operator: "is_date_before".to_string(),
2221 property_type: None,
2222 };
2223
2224 let mut properties = HashMap::new();
2225 properties.insert("date".to_string(), json!("2024-01-01"));
2226
2227 let empty_prop = Property {
2229 value: json!(""),
2230 ..prop.clone()
2231 };
2232 assert!(match_property(&empty_prop, &properties).is_err());
2233
2234 let dash_prop = Property {
2236 value: json!("-"),
2237 ..prop.clone()
2238 };
2239 assert!(match_property(&dash_prop, &properties).is_err());
2240
2241 let no_unit_prop = Property {
2243 value: json!("-7"),
2244 ..prop.clone()
2245 };
2246 assert!(match_property(&no_unit_prop, &properties).is_err());
2247
2248 let no_number_prop = Property {
2250 value: json!("-d"),
2251 ..prop.clone()
2252 };
2253 assert!(match_property(&no_number_prop, &properties).is_err());
2254
2255 let invalid_unit_prop = Property {
2257 value: json!("-7x"),
2258 ..prop.clone()
2259 };
2260 assert!(match_property(&invalid_unit_prop, &properties).is_err());
2261 }
2262
2263 #[test]
2264 fn test_parse_relative_date_large_values() {
2265 let prop = Property {
2267 key: "created_at".to_string(),
2268 value: json!("-1000d"), operator: "is_date_before".to_string(),
2270 property_type: None,
2271 };
2272
2273 let mut properties = HashMap::new();
2274 let five_years_ago = chrono::Utc::now() - chrono::Duration::days(1825);
2276 properties.insert(
2277 "created_at".to_string(),
2278 json!(five_years_ago.format("%Y-%m-%d").to_string()),
2279 );
2280 assert!(match_property(&prop, &properties).unwrap());
2281 }
2282
2283 #[test]
2286 fn test_regex_with_invalid_pattern_returns_false() {
2287 let prop = Property {
2289 key: "email".to_string(),
2290 value: json!("(unclosed"),
2291 operator: "regex".to_string(),
2292 property_type: None,
2293 };
2294
2295 let mut properties = HashMap::new();
2296 properties.insert("email".to_string(), json!("test@example.com"));
2297
2298 assert!(!match_property(&prop, &properties).unwrap());
2300 }
2301
2302 #[test]
2303 fn test_not_regex_with_invalid_pattern_returns_true() {
2304 let prop = Property {
2306 key: "email".to_string(),
2307 value: json!("(unclosed"),
2308 operator: "not_regex".to_string(),
2309 property_type: None,
2310 };
2311
2312 let mut properties = HashMap::new();
2313 properties.insert("email".to_string(), json!("test@example.com"));
2314
2315 assert!(match_property(&prop, &properties).unwrap());
2317 }
2318
2319 #[test]
2320 fn test_regex_with_various_invalid_patterns() {
2321 let invalid_patterns = vec![
2322 "(unclosed", "[unclosed", "*invalid", "(?P<bad", r"\", ];
2328
2329 for pattern in invalid_patterns {
2330 let prop = Property {
2331 key: "value".to_string(),
2332 value: json!(pattern),
2333 operator: "regex".to_string(),
2334 property_type: None,
2335 };
2336
2337 let mut properties = HashMap::new();
2338 properties.insert("value".to_string(), json!("test"));
2339
2340 assert!(
2342 !match_property(&prop, &properties).unwrap(),
2343 "Invalid pattern '{}' should return false for regex",
2344 pattern
2345 );
2346
2347 let not_regex_prop = Property {
2349 operator: "not_regex".to_string(),
2350 ..prop
2351 };
2352 assert!(
2353 match_property(¬_regex_prop, &properties).unwrap(),
2354 "Invalid pattern '{}' should return true for not_regex",
2355 pattern
2356 );
2357 }
2358 }
2359
2360 #[test]
2363 fn test_parse_semver_basic() {
2364 assert_eq!(parse_semver("1.2.3"), Some((1, 2, 3)));
2365 assert_eq!(parse_semver("0.0.0"), Some((0, 0, 0)));
2366 assert_eq!(parse_semver("10.20.30"), Some((10, 20, 30)));
2367 }
2368
2369 #[test]
2370 fn test_parse_semver_v_prefix() {
2371 assert_eq!(parse_semver("v1.2.3"), Some((1, 2, 3)));
2372 assert_eq!(parse_semver("V1.2.3"), Some((1, 2, 3)));
2373 }
2374
2375 #[test]
2376 fn test_parse_semver_whitespace() {
2377 assert_eq!(parse_semver(" 1.2.3 "), Some((1, 2, 3)));
2378 assert_eq!(parse_semver(" v1.2.3 "), Some((1, 2, 3)));
2379 }
2380
2381 #[test]
2382 fn test_parse_semver_prerelease_stripped() {
2383 assert_eq!(parse_semver("1.2.3-alpha"), Some((1, 2, 3)));
2384 assert_eq!(parse_semver("1.2.3-beta.1"), Some((1, 2, 3)));
2385 assert_eq!(parse_semver("1.2.3-rc.1+build.123"), Some((1, 2, 3)));
2386 assert_eq!(parse_semver("1.2.3+build.456"), Some((1, 2, 3)));
2387 }
2388
2389 #[test]
2390 fn test_parse_semver_partial_versions() {
2391 assert_eq!(parse_semver("1.2"), Some((1, 2, 0)));
2392 assert_eq!(parse_semver("1"), Some((1, 0, 0)));
2393 assert_eq!(parse_semver("v1.2"), Some((1, 2, 0)));
2394 }
2395
2396 #[test]
2397 fn test_parse_semver_extra_components_ignored() {
2398 assert_eq!(parse_semver("1.2.3.4"), Some((1, 2, 3)));
2399 assert_eq!(parse_semver("1.2.3.4.5.6"), Some((1, 2, 3)));
2400 }
2401
2402 #[test]
2403 fn test_parse_semver_leading_zeros_rejected() {
2404 assert_eq!(parse_semver("01.02.03"), None);
2406 assert_eq!(parse_semver("001.002.003"), None);
2407 assert_eq!(parse_semver("1.07.3"), None);
2408 assert_eq!(parse_semver("1.2.03"), None);
2409 assert_eq!(parse_semver("v01.2.3"), None);
2410
2411 assert_eq!(parse_semver("0.1.0"), Some((0, 1, 0)));
2413 assert_eq!(parse_semver("1.0.0"), Some((1, 0, 0)));
2414 assert_eq!(parse_semver("0.0.0"), Some((0, 0, 0)));
2415 }
2416
2417 #[test]
2418 fn test_parse_semver_invalid() {
2419 assert_eq!(parse_semver(""), None);
2420 assert_eq!(parse_semver(" "), None);
2421 assert_eq!(parse_semver("v"), None);
2422 assert_eq!(parse_semver(".1.2.3"), None);
2423 assert_eq!(parse_semver("abc"), None);
2424 assert_eq!(parse_semver("1.abc.3"), None);
2425 assert_eq!(parse_semver("1.2.abc"), None);
2426 assert_eq!(parse_semver("not-a-version"), None);
2427 }
2428
2429 #[test]
2432 fn test_semver_eq_basic() {
2433 let prop = Property {
2434 key: "version".to_string(),
2435 value: json!("1.2.3"),
2436 operator: "semver_eq".to_string(),
2437 property_type: None,
2438 };
2439
2440 let mut properties = HashMap::new();
2441
2442 properties.insert("version".to_string(), json!("1.2.3"));
2443 assert!(match_property(&prop, &properties).unwrap());
2444
2445 properties.insert("version".to_string(), json!("1.2.4"));
2446 assert!(!match_property(&prop, &properties).unwrap());
2447
2448 properties.insert("version".to_string(), json!("1.3.3"));
2449 assert!(!match_property(&prop, &properties).unwrap());
2450
2451 properties.insert("version".to_string(), json!("2.2.3"));
2452 assert!(!match_property(&prop, &properties).unwrap());
2453 }
2454
2455 #[test]
2456 fn test_semver_eq_with_v_prefix() {
2457 let prop = Property {
2458 key: "version".to_string(),
2459 value: json!("1.2.3"),
2460 operator: "semver_eq".to_string(),
2461 property_type: None,
2462 };
2463
2464 let mut properties = HashMap::new();
2465
2466 properties.insert("version".to_string(), json!("v1.2.3"));
2468 assert!(match_property(&prop, &properties).unwrap());
2469
2470 let prop_with_v = Property {
2472 value: json!("v1.2.3"),
2473 ..prop.clone()
2474 };
2475 properties.insert("version".to_string(), json!("1.2.3"));
2476 assert!(match_property(&prop_with_v, &properties).unwrap());
2477 }
2478
2479 #[test]
2480 fn test_semver_eq_prerelease_stripped() {
2481 let prop = Property {
2482 key: "version".to_string(),
2483 value: json!("1.2.3"),
2484 operator: "semver_eq".to_string(),
2485 property_type: None,
2486 };
2487
2488 let mut properties = HashMap::new();
2489
2490 properties.insert("version".to_string(), json!("1.2.3-alpha"));
2491 assert!(match_property(&prop, &properties).unwrap());
2492
2493 properties.insert("version".to_string(), json!("1.2.3-beta.1"));
2494 assert!(match_property(&prop, &properties).unwrap());
2495
2496 properties.insert("version".to_string(), json!("1.2.3+build.456"));
2497 assert!(match_property(&prop, &properties).unwrap());
2498 }
2499
2500 #[test]
2501 fn test_semver_eq_partial_versions() {
2502 let prop = Property {
2503 key: "version".to_string(),
2504 value: json!("1.2.0"),
2505 operator: "semver_eq".to_string(),
2506 property_type: None,
2507 };
2508
2509 let mut properties = HashMap::new();
2510
2511 properties.insert("version".to_string(), json!("1.2"));
2513 assert!(match_property(&prop, &properties).unwrap());
2514
2515 let partial_prop = Property {
2517 value: json!("1.2"),
2518 ..prop.clone()
2519 };
2520 properties.insert("version".to_string(), json!("1.2.0"));
2521 assert!(match_property(&partial_prop, &properties).unwrap());
2522 }
2523
2524 #[test]
2525 fn test_semver_neq() {
2526 let prop = Property {
2527 key: "version".to_string(),
2528 value: json!("1.2.3"),
2529 operator: "semver_neq".to_string(),
2530 property_type: None,
2531 };
2532
2533 let mut properties = HashMap::new();
2534
2535 properties.insert("version".to_string(), json!("1.2.3"));
2536 assert!(!match_property(&prop, &properties).unwrap());
2537
2538 properties.insert("version".to_string(), json!("1.2.4"));
2539 assert!(match_property(&prop, &properties).unwrap());
2540
2541 properties.insert("version".to_string(), json!("2.0.0"));
2542 assert!(match_property(&prop, &properties).unwrap());
2543 }
2544
2545 #[test]
2548 fn test_semver_gt() {
2549 let prop = Property {
2550 key: "version".to_string(),
2551 value: json!("1.2.3"),
2552 operator: "semver_gt".to_string(),
2553 property_type: None,
2554 };
2555
2556 let mut properties = HashMap::new();
2557
2558 properties.insert("version".to_string(), json!("1.2.4"));
2560 assert!(match_property(&prop, &properties).unwrap());
2561
2562 properties.insert("version".to_string(), json!("1.3.0"));
2563 assert!(match_property(&prop, &properties).unwrap());
2564
2565 properties.insert("version".to_string(), json!("2.0.0"));
2566 assert!(match_property(&prop, &properties).unwrap());
2567
2568 properties.insert("version".to_string(), json!("1.2.3"));
2570 assert!(!match_property(&prop, &properties).unwrap());
2571
2572 properties.insert("version".to_string(), json!("1.2.2"));
2574 assert!(!match_property(&prop, &properties).unwrap());
2575
2576 properties.insert("version".to_string(), json!("1.1.9"));
2577 assert!(!match_property(&prop, &properties).unwrap());
2578
2579 properties.insert("version".to_string(), json!("0.9.9"));
2580 assert!(!match_property(&prop, &properties).unwrap());
2581 }
2582
2583 #[test]
2584 fn test_semver_gte() {
2585 let prop = Property {
2586 key: "version".to_string(),
2587 value: json!("1.2.3"),
2588 operator: "semver_gte".to_string(),
2589 property_type: None,
2590 };
2591
2592 let mut properties = HashMap::new();
2593
2594 properties.insert("version".to_string(), json!("1.2.4"));
2596 assert!(match_property(&prop, &properties).unwrap());
2597
2598 properties.insert("version".to_string(), json!("2.0.0"));
2599 assert!(match_property(&prop, &properties).unwrap());
2600
2601 properties.insert("version".to_string(), json!("1.2.3"));
2603 assert!(match_property(&prop, &properties).unwrap());
2604
2605 properties.insert("version".to_string(), json!("1.2.2"));
2607 assert!(!match_property(&prop, &properties).unwrap());
2608
2609 properties.insert("version".to_string(), json!("0.9.9"));
2610 assert!(!match_property(&prop, &properties).unwrap());
2611 }
2612
2613 #[test]
2614 fn test_semver_lt() {
2615 let prop = Property {
2616 key: "version".to_string(),
2617 value: json!("1.2.3"),
2618 operator: "semver_lt".to_string(),
2619 property_type: None,
2620 };
2621
2622 let mut properties = HashMap::new();
2623
2624 properties.insert("version".to_string(), json!("1.2.2"));
2626 assert!(match_property(&prop, &properties).unwrap());
2627
2628 properties.insert("version".to_string(), json!("1.1.9"));
2629 assert!(match_property(&prop, &properties).unwrap());
2630
2631 properties.insert("version".to_string(), json!("0.9.9"));
2632 assert!(match_property(&prop, &properties).unwrap());
2633
2634 properties.insert("version".to_string(), json!("1.2.3"));
2636 assert!(!match_property(&prop, &properties).unwrap());
2637
2638 properties.insert("version".to_string(), json!("1.2.4"));
2640 assert!(!match_property(&prop, &properties).unwrap());
2641
2642 properties.insert("version".to_string(), json!("2.0.0"));
2643 assert!(!match_property(&prop, &properties).unwrap());
2644 }
2645
2646 #[test]
2647 fn test_semver_lte() {
2648 let prop = Property {
2649 key: "version".to_string(),
2650 value: json!("1.2.3"),
2651 operator: "semver_lte".to_string(),
2652 property_type: None,
2653 };
2654
2655 let mut properties = HashMap::new();
2656
2657 properties.insert("version".to_string(), json!("1.2.2"));
2659 assert!(match_property(&prop, &properties).unwrap());
2660
2661 properties.insert("version".to_string(), json!("0.9.9"));
2662 assert!(match_property(&prop, &properties).unwrap());
2663
2664 properties.insert("version".to_string(), json!("1.2.3"));
2666 assert!(match_property(&prop, &properties).unwrap());
2667
2668 properties.insert("version".to_string(), json!("1.2.4"));
2670 assert!(!match_property(&prop, &properties).unwrap());
2671
2672 properties.insert("version".to_string(), json!("2.0.0"));
2673 assert!(!match_property(&prop, &properties).unwrap());
2674 }
2675
2676 #[test]
2679 fn test_semver_tilde_basic() {
2680 let prop = Property {
2682 key: "version".to_string(),
2683 value: json!("1.2.3"),
2684 operator: "semver_tilde".to_string(),
2685 property_type: None,
2686 };
2687
2688 let mut properties = HashMap::new();
2689
2690 properties.insert("version".to_string(), json!("1.2.3"));
2692 assert!(match_property(&prop, &properties).unwrap());
2693
2694 properties.insert("version".to_string(), json!("1.2.4"));
2696 assert!(match_property(&prop, &properties).unwrap());
2697
2698 properties.insert("version".to_string(), json!("1.2.99"));
2699 assert!(match_property(&prop, &properties).unwrap());
2700
2701 properties.insert("version".to_string(), json!("1.3.0"));
2703 assert!(!match_property(&prop, &properties).unwrap());
2704
2705 properties.insert("version".to_string(), json!("1.3.1"));
2707 assert!(!match_property(&prop, &properties).unwrap());
2708
2709 properties.insert("version".to_string(), json!("2.0.0"));
2710 assert!(!match_property(&prop, &properties).unwrap());
2711
2712 properties.insert("version".to_string(), json!("1.2.2"));
2714 assert!(!match_property(&prop, &properties).unwrap());
2715
2716 properties.insert("version".to_string(), json!("1.1.9"));
2717 assert!(!match_property(&prop, &properties).unwrap());
2718 }
2719
2720 #[test]
2721 fn test_semver_tilde_zero_versions() {
2722 let prop = Property {
2724 key: "version".to_string(),
2725 value: json!("0.2.3"),
2726 operator: "semver_tilde".to_string(),
2727 property_type: None,
2728 };
2729
2730 let mut properties = HashMap::new();
2731
2732 properties.insert("version".to_string(), json!("0.2.3"));
2733 assert!(match_property(&prop, &properties).unwrap());
2734
2735 properties.insert("version".to_string(), json!("0.2.9"));
2736 assert!(match_property(&prop, &properties).unwrap());
2737
2738 properties.insert("version".to_string(), json!("0.3.0"));
2739 assert!(!match_property(&prop, &properties).unwrap());
2740
2741 properties.insert("version".to_string(), json!("0.2.2"));
2742 assert!(!match_property(&prop, &properties).unwrap());
2743 }
2744
2745 #[test]
2748 fn test_semver_caret_major_nonzero() {
2749 let prop = Property {
2751 key: "version".to_string(),
2752 value: json!("1.2.3"),
2753 operator: "semver_caret".to_string(),
2754 property_type: None,
2755 };
2756
2757 let mut properties = HashMap::new();
2758
2759 properties.insert("version".to_string(), json!("1.2.3"));
2761 assert!(match_property(&prop, &properties).unwrap());
2762
2763 properties.insert("version".to_string(), json!("1.2.4"));
2765 assert!(match_property(&prop, &properties).unwrap());
2766
2767 properties.insert("version".to_string(), json!("1.3.0"));
2768 assert!(match_property(&prop, &properties).unwrap());
2769
2770 properties.insert("version".to_string(), json!("1.99.99"));
2771 assert!(match_property(&prop, &properties).unwrap());
2772
2773 properties.insert("version".to_string(), json!("2.0.0"));
2775 assert!(!match_property(&prop, &properties).unwrap());
2776
2777 properties.insert("version".to_string(), json!("2.0.1"));
2779 assert!(!match_property(&prop, &properties).unwrap());
2780
2781 properties.insert("version".to_string(), json!("1.2.2"));
2783 assert!(!match_property(&prop, &properties).unwrap());
2784
2785 properties.insert("version".to_string(), json!("0.9.9"));
2786 assert!(!match_property(&prop, &properties).unwrap());
2787 }
2788
2789 #[test]
2790 fn test_semver_caret_major_zero_minor_nonzero() {
2791 let prop = Property {
2793 key: "version".to_string(),
2794 value: json!("0.2.3"),
2795 operator: "semver_caret".to_string(),
2796 property_type: None,
2797 };
2798
2799 let mut properties = HashMap::new();
2800
2801 properties.insert("version".to_string(), json!("0.2.3"));
2803 assert!(match_property(&prop, &properties).unwrap());
2804
2805 properties.insert("version".to_string(), json!("0.2.4"));
2807 assert!(match_property(&prop, &properties).unwrap());
2808
2809 properties.insert("version".to_string(), json!("0.2.99"));
2810 assert!(match_property(&prop, &properties).unwrap());
2811
2812 properties.insert("version".to_string(), json!("0.3.0"));
2814 assert!(!match_property(&prop, &properties).unwrap());
2815
2816 properties.insert("version".to_string(), json!("0.3.1"));
2818 assert!(!match_property(&prop, &properties).unwrap());
2819
2820 properties.insert("version".to_string(), json!("1.0.0"));
2821 assert!(!match_property(&prop, &properties).unwrap());
2822
2823 properties.insert("version".to_string(), json!("0.2.2"));
2825 assert!(!match_property(&prop, &properties).unwrap());
2826
2827 properties.insert("version".to_string(), json!("0.1.9"));
2828 assert!(!match_property(&prop, &properties).unwrap());
2829 }
2830
2831 #[test]
2832 fn test_semver_caret_major_zero_minor_zero() {
2833 let prop = Property {
2835 key: "version".to_string(),
2836 value: json!("0.0.3"),
2837 operator: "semver_caret".to_string(),
2838 property_type: None,
2839 };
2840
2841 let mut properties = HashMap::new();
2842
2843 properties.insert("version".to_string(), json!("0.0.3"));
2845 assert!(match_property(&prop, &properties).unwrap());
2846
2847 properties.insert("version".to_string(), json!("0.0.4"));
2849 assert!(!match_property(&prop, &properties).unwrap());
2850
2851 properties.insert("version".to_string(), json!("0.0.5"));
2853 assert!(!match_property(&prop, &properties).unwrap());
2854
2855 properties.insert("version".to_string(), json!("0.1.0"));
2856 assert!(!match_property(&prop, &properties).unwrap());
2857
2858 properties.insert("version".to_string(), json!("0.0.2"));
2860 assert!(!match_property(&prop, &properties).unwrap());
2861 }
2862
2863 #[test]
2866 fn test_semver_wildcard_major() {
2867 let prop = Property {
2869 key: "version".to_string(),
2870 value: json!("1.*"),
2871 operator: "semver_wildcard".to_string(),
2872 property_type: None,
2873 };
2874
2875 let mut properties = HashMap::new();
2876
2877 properties.insert("version".to_string(), json!("1.0.0"));
2879 assert!(match_property(&prop, &properties).unwrap());
2880
2881 properties.insert("version".to_string(), json!("1.2.3"));
2883 assert!(match_property(&prop, &properties).unwrap());
2884
2885 properties.insert("version".to_string(), json!("1.99.99"));
2886 assert!(match_property(&prop, &properties).unwrap());
2887
2888 properties.insert("version".to_string(), json!("2.0.0"));
2890 assert!(!match_property(&prop, &properties).unwrap());
2891
2892 properties.insert("version".to_string(), json!("2.0.1"));
2894 assert!(!match_property(&prop, &properties).unwrap());
2895
2896 properties.insert("version".to_string(), json!("0.9.9"));
2898 assert!(!match_property(&prop, &properties).unwrap());
2899 }
2900
2901 #[test]
2902 fn test_semver_wildcard_minor() {
2903 let prop = Property {
2905 key: "version".to_string(),
2906 value: json!("1.2.*"),
2907 operator: "semver_wildcard".to_string(),
2908 property_type: None,
2909 };
2910
2911 let mut properties = HashMap::new();
2912
2913 properties.insert("version".to_string(), json!("1.2.0"));
2915 assert!(match_property(&prop, &properties).unwrap());
2916
2917 properties.insert("version".to_string(), json!("1.2.3"));
2919 assert!(match_property(&prop, &properties).unwrap());
2920
2921 properties.insert("version".to_string(), json!("1.2.99"));
2922 assert!(match_property(&prop, &properties).unwrap());
2923
2924 properties.insert("version".to_string(), json!("1.3.0"));
2926 assert!(!match_property(&prop, &properties).unwrap());
2927
2928 properties.insert("version".to_string(), json!("1.3.1"));
2930 assert!(!match_property(&prop, &properties).unwrap());
2931
2932 properties.insert("version".to_string(), json!("2.0.0"));
2933 assert!(!match_property(&prop, &properties).unwrap());
2934
2935 properties.insert("version".to_string(), json!("1.1.9"));
2937 assert!(!match_property(&prop, &properties).unwrap());
2938 }
2939
2940 #[test]
2941 fn test_semver_wildcard_zero() {
2942 let prop = Property {
2944 key: "version".to_string(),
2945 value: json!("0.*"),
2946 operator: "semver_wildcard".to_string(),
2947 property_type: None,
2948 };
2949
2950 let mut properties = HashMap::new();
2951
2952 properties.insert("version".to_string(), json!("0.0.0"));
2953 assert!(match_property(&prop, &properties).unwrap());
2954
2955 properties.insert("version".to_string(), json!("0.99.99"));
2956 assert!(match_property(&prop, &properties).unwrap());
2957
2958 properties.insert("version".to_string(), json!("1.0.0"));
2959 assert!(!match_property(&prop, &properties).unwrap());
2960 }
2961
2962 #[test]
2965 fn test_semver_invalid_property_value() {
2966 let prop = Property {
2967 key: "version".to_string(),
2968 value: json!("1.2.3"),
2969 operator: "semver_eq".to_string(),
2970 property_type: None,
2971 };
2972
2973 let mut properties = HashMap::new();
2974
2975 properties.insert("version".to_string(), json!("not-a-version"));
2977 assert!(match_property(&prop, &properties).is_err());
2978
2979 properties.insert("version".to_string(), json!(""));
2980 assert!(match_property(&prop, &properties).is_err());
2981
2982 properties.insert("version".to_string(), json!(".1.2.3"));
2983 assert!(match_property(&prop, &properties).is_err());
2984
2985 properties.insert("version".to_string(), json!("abc.def.ghi"));
2986 assert!(match_property(&prop, &properties).is_err());
2987 }
2988
2989 #[test]
2990 fn test_semver_invalid_target_value() {
2991 let mut properties = HashMap::new();
2992 properties.insert("version".to_string(), json!("1.2.3"));
2993
2994 let prop = Property {
2996 key: "version".to_string(),
2997 value: json!("not-valid"),
2998 operator: "semver_eq".to_string(),
2999 property_type: None,
3000 };
3001 assert!(match_property(&prop, &properties).is_err());
3002
3003 let prop = Property {
3004 key: "version".to_string(),
3005 value: json!(""),
3006 operator: "semver_gt".to_string(),
3007 property_type: None,
3008 };
3009 assert!(match_property(&prop, &properties).is_err());
3010 }
3011
3012 #[test]
3013 fn test_semver_invalid_wildcard_pattern() {
3014 let mut properties = HashMap::new();
3015 properties.insert("version".to_string(), json!("1.2.3"));
3016
3017 let invalid_patterns = vec![
3019 "*", "*.2.3", "1.*.3", "1.2.3.*", "abc.*", ];
3025
3026 for pattern in invalid_patterns {
3027 let prop = Property {
3028 key: "version".to_string(),
3029 value: json!(pattern),
3030 operator: "semver_wildcard".to_string(),
3031 property_type: None,
3032 };
3033 assert!(
3034 match_property(&prop, &properties).is_err(),
3035 "Pattern '{}' should be invalid",
3036 pattern
3037 );
3038 }
3039 }
3040
3041 #[test]
3042 fn test_semver_missing_property() {
3043 let prop = Property {
3044 key: "version".to_string(),
3045 value: json!("1.2.3"),
3046 operator: "semver_eq".to_string(),
3047 property_type: None,
3048 };
3049
3050 let properties = HashMap::new(); assert!(match_property(&prop, &properties).is_err());
3052 }
3053
3054 #[test]
3055 fn test_semver_null_property_value() {
3056 let prop = Property {
3057 key: "version".to_string(),
3058 value: json!("1.2.3"),
3059 operator: "semver_eq".to_string(),
3060 property_type: None,
3061 };
3062
3063 let mut properties = HashMap::new();
3064 properties.insert("version".to_string(), json!(null));
3065
3066 assert!(match_property(&prop, &properties).is_err());
3068 }
3069
3070 #[test]
3071 fn test_semver_numeric_property_value() {
3072 let prop = Property {
3074 key: "version".to_string(),
3075 value: json!("1.0.0"),
3076 operator: "semver_eq".to_string(),
3077 property_type: None,
3078 };
3079
3080 let mut properties = HashMap::new();
3081 properties.insert("version".to_string(), json!(1));
3083 assert!(match_property(&prop, &properties).unwrap());
3084 }
3085
3086 #[test]
3089 fn test_semver_four_part_versions() {
3090 let prop = Property {
3091 key: "version".to_string(),
3092 value: json!("1.2.3.4"),
3093 operator: "semver_eq".to_string(),
3094 property_type: None,
3095 };
3096
3097 let mut properties = HashMap::new();
3098
3099 properties.insert("version".to_string(), json!("1.2.3"));
3101 assert!(match_property(&prop, &properties).unwrap());
3102
3103 properties.insert("version".to_string(), json!("1.2.3.4"));
3104 assert!(match_property(&prop, &properties).unwrap());
3105
3106 properties.insert("version".to_string(), json!("1.2.3.999"));
3107 assert!(match_property(&prop, &properties).unwrap());
3108 }
3109
3110 #[test]
3111 fn test_semver_large_version_numbers() {
3112 let prop = Property {
3113 key: "version".to_string(),
3114 value: json!("1000.2000.3000"),
3115 operator: "semver_eq".to_string(),
3116 property_type: None,
3117 };
3118
3119 let mut properties = HashMap::new();
3120 properties.insert("version".to_string(), json!("1000.2000.3000"));
3121 assert!(match_property(&prop, &properties).unwrap());
3122 }
3123
3124 #[test]
3125 fn test_semver_comparison_ordering() {
3126 let cases = vec![
3128 ("0.0.1", "0.0.2", "semver_lt", true),
3129 ("0.1.0", "0.0.99", "semver_gt", true),
3130 ("1.0.0", "0.99.99", "semver_gt", true),
3131 ("1.0.0", "1.0.0", "semver_eq", true),
3132 ("2.0.0", "10.0.0", "semver_lt", true), ("9.0.0", "10.0.0", "semver_lt", true), ("1.9.0", "1.10.0", "semver_lt", true), ("1.2.9", "1.2.10", "semver_lt", true), ];
3137
3138 for (prop_val, target_val, op, expected) in cases {
3139 let prop = Property {
3140 key: "version".to_string(),
3141 value: json!(target_val),
3142 operator: op.to_string(),
3143 property_type: None,
3144 };
3145
3146 let mut properties = HashMap::new();
3147 properties.insert("version".to_string(), json!(prop_val));
3148
3149 assert_eq!(
3150 match_property(&prop, &properties).unwrap(),
3151 expected,
3152 "{} {} {} should be {}",
3153 prop_val,
3154 op,
3155 target_val,
3156 expected
3157 );
3158 }
3159 }
3160
3161 #[test]
3162 fn test_match_property_semver_rejects_leading_zeros() {
3163 let bad_versions = ["1.07.3", "01.02.03", "1.2.03", "v01.2.3", "001.0.0"];
3168
3169 for bad in bad_versions {
3171 let prop = Property {
3172 key: "version".to_string(),
3173 value: json!("1.2.3"),
3174 operator: "semver_eq".to_string(),
3175 property_type: None,
3176 };
3177 let mut properties = HashMap::new();
3178 properties.insert("version".to_string(), json!(bad));
3179 assert!(
3180 match_property(&prop, &properties).is_err(),
3181 "override '{}' should be rejected",
3182 bad
3183 );
3184 }
3185
3186 for good in ["0.1.0", "1.0.0", "0.0.0"] {
3188 let prop = Property {
3189 key: "version".to_string(),
3190 value: json!(good),
3191 operator: "semver_eq".to_string(),
3192 property_type: None,
3193 };
3194 let mut properties = HashMap::new();
3195 properties.insert("version".to_string(), json!(good));
3196 assert!(
3197 match_property(&prop, &properties).unwrap(),
3198 "'{}' should parse and match itself",
3199 good
3200 );
3201 }
3202
3203 let mut properties = HashMap::new();
3205 properties.insert("version".to_string(), json!("1.2.3"));
3206
3207 for op in ["semver_gt", "semver_caret", "semver_tilde"] {
3208 for bad in bad_versions {
3209 let prop = Property {
3210 key: "version".to_string(),
3211 value: json!(bad),
3212 operator: op.to_string(),
3213 property_type: None,
3214 };
3215 assert!(
3216 match_property(&prop, &properties).is_err(),
3217 "target '{}' for {} should be rejected",
3218 bad,
3219 op
3220 );
3221 }
3222 }
3223
3224 for bad_pattern in ["01.*", "1.07.*", "v01.2.*"] {
3226 let prop = Property {
3227 key: "version".to_string(),
3228 value: json!(bad_pattern),
3229 operator: "semver_wildcard".to_string(),
3230 property_type: None,
3231 };
3232 assert!(
3233 match_property(&prop, &properties).is_err(),
3234 "wildcard target '{}' should be rejected",
3235 bad_pattern
3236 );
3237 }
3238 }
3239
3240 fn early_exit_flag(early_exit: bool) -> FeatureFlag {
3247 FeatureFlag {
3248 key: "early-exit-flag".to_string(),
3249 active: true,
3250 filters: FeatureFlagFilters {
3251 groups: vec![
3252 FeatureFlagCondition {
3255 properties: vec![],
3256 rollout_percentage: Some(0.0),
3257 variant: None,
3258 aggregation_group_type_index: None,
3259 },
3260 FeatureFlagCondition {
3262 properties: vec![],
3263 rollout_percentage: Some(100.0),
3264 variant: None,
3265 aggregation_group_type_index: None,
3266 },
3267 ],
3268 multivariate: None,
3269 payloads: HashMap::new(),
3270 aggregation_group_type_index: None,
3271 early_exit,
3272 },
3273 }
3274 }
3275
3276 macro_rules! test_early_exit {
3277 ($name:ident, $early_exit:expr, $expected:expr) => {
3278 #[test]
3279 fn $name() {
3280 let flag = early_exit_flag($early_exit);
3281 let result = match_feature_flag(
3282 &flag,
3283 "user-123",
3284 &HashMap::new(),
3285 &HashMap::new(),
3286 &HashMap::new(),
3287 &HashMap::new(),
3288 )
3289 .unwrap();
3290 assert_eq!(result, $expected);
3291 }
3292 };
3293 }
3294
3295 test_early_exit!(
3296 test_early_exit_enabled_returns_false_without_evaluating_later_group,
3297 true,
3298 FlagValue::Boolean(false)
3299 );
3300 test_early_exit!(
3301 test_early_exit_unset_falls_through_to_matching_group,
3302 false,
3303 FlagValue::Boolean(true)
3304 );
3305
3306 #[test]
3307 fn test_early_exit_default_is_false_from_json() {
3308 let flag: FeatureFlag = serde_json::from_value(json!({
3311 "key": "early-exit-flag",
3312 "active": true,
3313 "filters": {
3314 "groups": [
3315 { "properties": [], "rollout_percentage": 0.0, "variant": null },
3316 { "properties": [], "rollout_percentage": 100.0, "variant": null }
3317 ]
3318 }
3319 }))
3320 .unwrap();
3321 assert!(!flag.filters.early_exit);
3322 let result = match_feature_flag(
3323 &flag,
3324 "user-123",
3325 &HashMap::new(),
3326 &HashMap::new(),
3327 &HashMap::new(),
3328 &HashMap::new(),
3329 )
3330 .unwrap();
3331 assert_eq!(result, FlagValue::Boolean(true));
3332 }
3333
3334 #[test]
3335 fn test_early_exit_explicit_false_falls_through() {
3336 let flag: FeatureFlag = serde_json::from_value(json!({
3337 "key": "early-exit-flag",
3338 "active": true,
3339 "filters": {
3340 "early_exit": false,
3341 "groups": [
3342 { "properties": [], "rollout_percentage": 0.0, "variant": null },
3343 { "properties": [], "rollout_percentage": 100.0, "variant": null }
3344 ]
3345 }
3346 }))
3347 .unwrap();
3348 assert!(!flag.filters.early_exit);
3349 let result = match_feature_flag(
3350 &flag,
3351 "user-123",
3352 &HashMap::new(),
3353 &HashMap::new(),
3354 &HashMap::new(),
3355 &HashMap::new(),
3356 )
3357 .unwrap();
3358 assert_eq!(result, FlagValue::Boolean(true));
3359 }
3360
3361 #[test]
3362 fn test_early_exit_property_mismatch_does_not_short_circuit() {
3363 let flag = FeatureFlag {
3367 key: "early-exit-flag".to_string(),
3368 active: true,
3369 filters: FeatureFlagFilters {
3370 groups: vec![
3371 FeatureFlagCondition {
3372 properties: vec![Property {
3373 key: "country".to_string(),
3374 value: json!("US"),
3375 operator: "exact".to_string(),
3376 property_type: None,
3377 }],
3378 rollout_percentage: Some(100.0),
3379 variant: None,
3380 aggregation_group_type_index: None,
3381 },
3382 FeatureFlagCondition {
3383 properties: vec![],
3384 rollout_percentage: Some(100.0),
3385 variant: None,
3386 aggregation_group_type_index: None,
3387 },
3388 ],
3389 multivariate: None,
3390 payloads: HashMap::new(),
3391 aggregation_group_type_index: None,
3392 early_exit: true,
3393 },
3394 };
3395
3396 let mut properties = HashMap::new();
3397 properties.insert("country".to_string(), json!("UK")); let result = match_feature_flag(
3400 &flag,
3401 "user-123",
3402 &properties,
3403 &HashMap::new(),
3404 &HashMap::new(),
3405 &HashMap::new(),
3406 )
3407 .unwrap();
3408 assert_eq!(result, FlagValue::Boolean(true));
3410 }
3411
3412 macro_rules! test_early_exit_with_context {
3413 ($name:ident, $early_exit:expr, $expected:expr) => {
3414 #[test]
3415 fn $name() {
3416 let flag = early_exit_flag($early_exit);
3417 let ctx = EvaluationContext {
3418 cohorts: &HashMap::new(),
3419 flags: &HashMap::new(),
3420 distinct_id: "user-123",
3421 groups: &HashMap::new(),
3422 group_properties: &HashMap::new(),
3423 group_type_mapping: &HashMap::new(),
3424 };
3425 let result = match_feature_flag_with_context(&flag, &HashMap::new(), &ctx).unwrap();
3426 assert_eq!(result, $expected);
3427 }
3428 };
3429 }
3430
3431 test_early_exit_with_context!(
3432 test_early_exit_enabled_short_circuits_with_context,
3433 true,
3434 FlagValue::Boolean(false)
3435 );
3436 test_early_exit_with_context!(
3437 test_early_exit_unset_falls_through_with_context,
3438 false,
3439 FlagValue::Boolean(true)
3440 );
3441
3442 #[test]
3443 fn test_early_exit_does_not_short_circuit_when_prior_group_inconclusive() {
3444 let flag = FeatureFlag {
3452 key: "early-exit-flag".to_string(),
3453 active: true,
3454 filters: FeatureFlagFilters {
3455 groups: vec![
3456 FeatureFlagCondition {
3457 properties: vec![],
3458 rollout_percentage: Some(100.0),
3459 variant: None,
3460 aggregation_group_type_index: Some(0),
3461 },
3462 FeatureFlagCondition {
3463 properties: vec![],
3464 rollout_percentage: Some(0.0),
3465 variant: None,
3466 aggregation_group_type_index: None,
3467 },
3468 ],
3469 multivariate: None,
3470 payloads: HashMap::new(),
3471 aggregation_group_type_index: None,
3472 early_exit: true,
3473 },
3474 };
3475
3476 let mut group_type_mapping = HashMap::new();
3477 group_type_mapping.insert("0".to_string(), "company".to_string());
3478
3479 let mut groups = HashMap::new();
3480 groups.insert("company".to_string(), "acme".to_string());
3481
3482 let result = match_feature_flag(
3484 &flag,
3485 "user-123",
3486 &HashMap::new(),
3487 &groups,
3488 &HashMap::new(), &group_type_mapping,
3490 );
3491 assert!(
3492 result.is_err(),
3493 "expected InconclusiveMatchError, got {:?}",
3494 result
3495 );
3496 }
3497}