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 {
71 Self {
72 message: message.to_string(),
73 }
74 }
75}
76
77impl fmt::Display for InconclusiveMatchError {
78 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
79 write!(f, "{}", self.message)
80 }
81}
82
83impl std::error::Error for InconclusiveMatchError {}
84
85impl Default for FlagValue {
86 fn default() -> Self {
87 FlagValue::Boolean(false)
88 }
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct FeatureFlag {
97 pub key: String,
99 pub active: bool,
101 #[serde(default)]
103 pub filters: FeatureFlagFilters,
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize, Default)]
108pub struct FeatureFlagFilters {
109 #[serde(default)]
111 pub groups: Vec<FeatureFlagCondition>,
112 #[serde(default)]
114 pub multivariate: Option<MultivariateFilter>,
115 #[serde(default)]
117 pub payloads: HashMap<String, serde_json::Value>,
118 #[serde(default)]
121 pub aggregation_group_type_index: Option<i32>,
122}
123
124#[derive(Debug, Clone, Serialize, Deserialize)]
129pub struct FeatureFlagCondition {
130 #[serde(default)]
132 pub properties: Vec<Property>,
133 pub rollout_percentage: Option<f64>,
135 pub variant: Option<String>,
137 #[serde(default)]
141 pub aggregation_group_type_index: Option<i32>,
142}
143
144#[derive(Debug, Clone, Serialize, Deserialize)]
148pub struct Property {
149 pub key: String,
151 pub value: serde_json::Value,
153 #[serde(default = "default_operator")]
157 pub operator: String,
158 #[serde(rename = "type")]
160 pub property_type: Option<String>,
161}
162
163fn default_operator() -> String {
164 "exact".to_string()
165}
166
167#[derive(Debug, Clone, Serialize, Deserialize)]
169pub struct CohortDefinition {
170 pub id: String,
171 #[serde(default)]
175 pub properties: serde_json::Value,
176}
177
178impl CohortDefinition {
179 pub fn new(id: String, properties: Vec<Property>) -> Self {
181 Self {
182 id,
183 properties: serde_json::to_value(properties).unwrap_or_default(),
184 }
185 }
186
187 pub fn parse_properties(&self) -> Vec<Property> {
191 if let Some(arr) = self.properties.as_array() {
193 return arr
194 .iter()
195 .filter_map(|v| serde_json::from_value::<Property>(v.clone()).ok())
196 .collect();
197 }
198
199 if let Some(obj) = self.properties.as_object() {
201 if let Some(values) = obj.get("values") {
202 if let Some(values_arr) = values.as_array() {
203 return values_arr
204 .iter()
205 .filter_map(|v| {
206 if v.get("type").and_then(|t| t.as_str()) == Some("property") {
208 serde_json::from_value::<Property>(v.clone()).ok()
209 } else if let Some(inner_values) = v.get("values") {
210 inner_values.as_array().and_then(|arr| {
212 arr.iter()
213 .filter_map(|inner| {
214 serde_json::from_value::<Property>(inner.clone()).ok()
215 })
216 .next()
217 })
218 } else {
219 None
220 }
221 })
222 .collect();
223 }
224 }
225 }
226
227 Vec::new()
228 }
229}
230
231pub struct EvaluationContext<'a> {
239 pub cohorts: &'a HashMap<String, CohortDefinition>,
240 pub flags: &'a HashMap<String, FeatureFlag>,
241 pub distinct_id: &'a str,
242 pub groups: &'a HashMap<String, String>,
243 pub group_properties: &'a HashMap<String, HashMap<String, serde_json::Value>>,
244 pub group_type_mapping: &'a HashMap<String, String>,
245}
246
247#[derive(Debug, Clone, Serialize, Deserialize, Default)]
249pub struct MultivariateFilter {
250 pub variants: Vec<MultivariateVariant>,
252}
253
254#[derive(Debug, Clone, Serialize, Deserialize)]
256pub struct MultivariateVariant {
257 pub key: String,
259 pub rollout_percentage: f64,
261}
262
263#[derive(Debug, Clone, Serialize, Deserialize)]
268#[serde(untagged)]
269pub enum FeatureFlagsResponse {
270 V2 {
272 flags: HashMap<String, FlagDetail>,
274 #[serde(rename = "errorsWhileComputingFlags")]
276 #[serde(default)]
277 errors_while_computing_flags: bool,
278 #[serde(rename = "quotaLimited")]
281 #[serde(default)]
282 quota_limited: bool,
283 #[serde(rename = "requestId")]
287 #[serde(default)]
288 request_id: Option<String>,
289 },
290 Legacy {
292 #[serde(rename = "featureFlags")]
294 feature_flags: HashMap<String, FlagValue>,
295 #[serde(rename = "featureFlagPayloads")]
297 #[serde(default)]
298 feature_flag_payloads: HashMap<String, serde_json::Value>,
299 #[serde(default)]
301 errors: Option<Vec<String>>,
302 },
303}
304
305impl FeatureFlagsResponse {
306 pub fn normalize(
308 self,
309 ) -> (
310 HashMap<String, FlagValue>,
311 HashMap<String, serde_json::Value>,
312 ) {
313 match self {
314 FeatureFlagsResponse::V2 { flags, .. } => {
315 let mut feature_flags = HashMap::new();
316 let mut payloads = HashMap::new();
317
318 for (key, detail) in flags {
319 if detail.enabled {
320 if let Some(variant) = detail.variant {
321 feature_flags.insert(key.clone(), FlagValue::String(variant));
322 } else {
323 feature_flags.insert(key.clone(), FlagValue::Boolean(true));
324 }
325 } else {
326 feature_flags.insert(key.clone(), FlagValue::Boolean(false));
327 }
328
329 if let Some(metadata) = detail.metadata {
330 if let Some(payload) = metadata.payload {
331 payloads.insert(key, payload);
332 }
333 }
334 }
335
336 (feature_flags, payloads)
337 }
338 FeatureFlagsResponse::Legacy {
339 feature_flags,
340 feature_flag_payloads,
341 ..
342 } => (feature_flags, feature_flag_payloads),
343 }
344 }
345}
346
347#[derive(Debug, Clone, Serialize, Deserialize)]
352pub struct FlagDetail {
353 pub key: String,
355 pub enabled: bool,
357 pub variant: Option<String>,
359 #[serde(default)]
361 pub reason: Option<FlagReason>,
362 #[serde(default)]
364 pub metadata: Option<FlagMetadata>,
365}
366
367#[derive(Debug, Clone, Serialize, Deserialize)]
369pub struct FlagReason {
370 pub code: String,
372 #[serde(default)]
374 pub condition_index: Option<usize>,
375 #[serde(default)]
377 pub description: Option<String>,
378}
379
380#[derive(Debug, Clone, Serialize, Deserialize)]
382pub struct FlagMetadata {
383 pub id: u64,
385 pub version: u32,
387 pub description: Option<String>,
389 pub payload: Option<serde_json::Value>,
391}
392
393const LONG_SCALE: f64 = 0xFFFFFFFFFFFFFFFu64 as f64; pub fn hash_key(key: &str, distinct_id: &str, salt: &str) -> f64 {
401 let hash_key = format!("{key}.{distinct_id}{salt}");
402 let mut hasher = Sha1::new();
403 hasher.update(hash_key.as_bytes());
404 let result = hasher.finalize();
405 let hex_str = format!("{result:x}");
406 let hash_val = u64::from_str_radix(&hex_str[..15], 16).unwrap_or(0);
407 hash_val as f64 / LONG_SCALE
408}
409
410pub fn get_matching_variant(flag: &FeatureFlag, distinct_id: &str) -> Option<String> {
416 let hash_value = hash_key(&flag.key, distinct_id, VARIANT_HASH_SALT);
417 let variants = flag.filters.multivariate.as_ref()?.variants.as_slice();
418
419 let mut value_min = 0.0;
420 for variant in variants {
421 let value_max = value_min + variant.rollout_percentage / 100.0;
422 if hash_value >= value_min && hash_value < value_max {
423 return Some(variant.key.clone());
424 }
425 value_min = value_max;
426 }
427 None
428}
429
430enum ConditionTarget<'a> {
432 Use {
434 bucketing: String,
435 properties: &'a HashMap<String, serde_json::Value>,
436 },
437 Skip,
439 Inconclusive,
442}
443
444fn resolve_condition_target<'a>(
450 condition: &FeatureFlagCondition,
451 flag_aggregation: Option<i32>,
452 distinct_id: &str,
453 person_properties: &'a HashMap<String, serde_json::Value>,
454 groups: &HashMap<String, String>,
455 group_properties: &'a HashMap<String, HashMap<String, serde_json::Value>>,
456 group_type_mapping: &HashMap<String, String>,
457) -> ConditionTarget<'a> {
458 let effective_aggregation = condition.aggregation_group_type_index.or(flag_aggregation);
461
462 match effective_aggregation {
463 None => ConditionTarget::Use {
464 bucketing: distinct_id.to_string(),
465 properties: person_properties,
466 },
467 Some(idx) => {
468 let key = idx.to_string();
469 let Some(group_type) = group_type_mapping.get(&key) else {
470 return ConditionTarget::Skip;
471 };
472 let Some(group_key) = groups.get(group_type) else {
473 return ConditionTarget::Skip;
474 };
475 let Some(props) = group_properties.get(group_type) else {
476 return ConditionTarget::Inconclusive;
477 };
478 ConditionTarget::Use {
479 bucketing: group_key.clone(),
480 properties: props,
481 }
482 }
483 }
484}
485
486#[must_use = "feature flag evaluation result should be used"]
487#[allow(clippy::too_many_arguments)]
488pub fn match_feature_flag(
489 flag: &FeatureFlag,
490 distinct_id: &str,
491 person_properties: &HashMap<String, serde_json::Value>,
492 groups: &HashMap<String, String>,
493 group_properties: &HashMap<String, HashMap<String, serde_json::Value>>,
494 group_type_mapping: &HashMap<String, String>,
495) -> Result<FlagValue, InconclusiveMatchError> {
496 if !flag.active {
497 return Ok(FlagValue::Boolean(false));
498 }
499
500 let conditions = &flag.filters.groups;
501 let flag_aggregation = flag.filters.aggregation_group_type_index;
502
503 let mut sorted_conditions = conditions.clone();
505 sorted_conditions.sort_by_key(|c| if c.variant.is_some() { 0 } else { 1 });
506
507 let mut is_inconclusive = false;
508
509 for condition in sorted_conditions {
510 let (effective_bucketing, effective_properties) = match resolve_condition_target(
511 &condition,
512 flag_aggregation,
513 distinct_id,
514 person_properties,
515 groups,
516 group_properties,
517 group_type_mapping,
518 ) {
519 ConditionTarget::Use {
520 bucketing,
521 properties,
522 } => (bucketing, properties),
523 ConditionTarget::Skip => continue,
524 ConditionTarget::Inconclusive => {
525 is_inconclusive = true;
526 continue;
527 }
528 };
529
530 match is_condition_match(flag, &effective_bucketing, &condition, effective_properties) {
531 Ok(true) => {
532 if let Some(variant_override) = &condition.variant {
533 if let Some(ref multivariate) = flag.filters.multivariate {
535 let valid_variants: Vec<String> = multivariate
536 .variants
537 .iter()
538 .map(|v| v.key.clone())
539 .collect();
540
541 if valid_variants.contains(variant_override) {
542 return Ok(FlagValue::String(variant_override.clone()));
543 }
544 }
545 }
546
547 if let Some(variant) = get_matching_variant(flag, &effective_bucketing) {
549 return Ok(FlagValue::String(variant));
550 }
551 return Ok(FlagValue::Boolean(true));
552 }
553 Ok(false) => continue,
554 Err(_) => {
555 is_inconclusive = true;
556 }
557 }
558 }
559
560 if is_inconclusive {
561 return Err(InconclusiveMatchError::new(
562 "Can't determine if feature flag is enabled or not with given properties",
563 ));
564 }
565
566 Ok(FlagValue::Boolean(false))
567}
568
569fn is_condition_match(
570 flag: &FeatureFlag,
571 bucketing_id: &str,
572 condition: &FeatureFlagCondition,
573 properties: &HashMap<String, serde_json::Value>,
574) -> Result<bool, InconclusiveMatchError> {
575 for prop in &condition.properties {
577 if !match_property(prop, properties)? {
578 return Ok(false);
579 }
580 }
581
582 if let Some(rollout_percentage) = condition.rollout_percentage {
584 let hash_value = hash_key(&flag.key, bucketing_id, ROLLOUT_HASH_SALT);
585 if hash_value > (rollout_percentage / 100.0) {
586 return Ok(false);
587 }
588 }
589
590 Ok(true)
591}
592
593#[must_use = "feature flag evaluation result should be used"]
600pub fn match_feature_flag_with_context(
601 flag: &FeatureFlag,
602 person_properties: &HashMap<String, serde_json::Value>,
603 ctx: &EvaluationContext,
604) -> Result<FlagValue, InconclusiveMatchError> {
605 if !flag.active {
606 return Ok(FlagValue::Boolean(false));
607 }
608
609 let conditions = &flag.filters.groups;
610 let flag_aggregation = flag.filters.aggregation_group_type_index;
611
612 let mut sorted_conditions = conditions.clone();
614 sorted_conditions.sort_by_key(|c| if c.variant.is_some() { 0 } else { 1 });
615
616 let mut is_inconclusive = false;
617
618 for condition in sorted_conditions {
619 let (effective_bucketing, effective_properties) = match resolve_condition_target(
620 &condition,
621 flag_aggregation,
622 ctx.distinct_id,
623 person_properties,
624 ctx.groups,
625 ctx.group_properties,
626 ctx.group_type_mapping,
627 ) {
628 ConditionTarget::Use {
629 bucketing,
630 properties,
631 } => (bucketing, properties),
632 ConditionTarget::Skip => continue,
633 ConditionTarget::Inconclusive => {
634 is_inconclusive = true;
635 continue;
636 }
637 };
638
639 match is_condition_match_with_context(
640 flag,
641 &effective_bucketing,
642 &condition,
643 effective_properties,
644 ctx,
645 ) {
646 Ok(true) => {
647 if let Some(variant_override) = &condition.variant {
648 if let Some(ref multivariate) = flag.filters.multivariate {
650 let valid_variants: Vec<String> = multivariate
651 .variants
652 .iter()
653 .map(|v| v.key.clone())
654 .collect();
655
656 if valid_variants.contains(variant_override) {
657 return Ok(FlagValue::String(variant_override.clone()));
658 }
659 }
660 }
661
662 if let Some(variant) = get_matching_variant(flag, &effective_bucketing) {
664 return Ok(FlagValue::String(variant));
665 }
666 return Ok(FlagValue::Boolean(true));
667 }
668 Ok(false) => continue,
669 Err(_) => {
670 is_inconclusive = true;
671 }
672 }
673 }
674
675 if is_inconclusive {
676 return Err(InconclusiveMatchError::new(
677 "Can't determine if feature flag is enabled or not with given properties",
678 ));
679 }
680
681 Ok(FlagValue::Boolean(false))
682}
683
684fn is_condition_match_with_context(
685 flag: &FeatureFlag,
686 bucketing_id: &str,
687 condition: &FeatureFlagCondition,
688 properties: &HashMap<String, serde_json::Value>,
689 ctx: &EvaluationContext,
690) -> Result<bool, InconclusiveMatchError> {
691 for prop in &condition.properties {
693 if !match_property_with_context(prop, properties, ctx)? {
694 return Ok(false);
695 }
696 }
697
698 if let Some(rollout_percentage) = condition.rollout_percentage {
700 let hash_value = hash_key(&flag.key, bucketing_id, ROLLOUT_HASH_SALT);
701 if hash_value > (rollout_percentage / 100.0) {
702 return Ok(false);
703 }
704 }
705
706 Ok(true)
707}
708
709pub fn match_property_with_context(
711 property: &Property,
712 properties: &HashMap<String, serde_json::Value>,
713 ctx: &EvaluationContext,
714) -> Result<bool, InconclusiveMatchError> {
715 if property.property_type.as_deref() == Some("cohort") {
717 return match_cohort_property(property, properties, ctx);
718 }
719
720 if property.key.starts_with("$feature/") {
722 return match_flag_dependency_property(property, ctx);
723 }
724
725 match_property(property, properties)
727}
728
729fn match_cohort_property(
731 property: &Property,
732 properties: &HashMap<String, serde_json::Value>,
733 ctx: &EvaluationContext,
734) -> Result<bool, InconclusiveMatchError> {
735 let cohort_id = property
736 .value
737 .as_str()
738 .ok_or_else(|| InconclusiveMatchError::new("Cohort ID must be a string"))?;
739
740 let cohort = ctx.cohorts.get(cohort_id).ok_or_else(|| {
741 InconclusiveMatchError::new(&format!("Cohort '{}' not found in local cache", cohort_id))
742 })?;
743
744 let cohort_properties = cohort.parse_properties();
746 let mut is_in_cohort = true;
747 for cohort_prop in &cohort_properties {
748 match match_property(cohort_prop, properties) {
749 Ok(true) => continue,
750 Ok(false) => {
751 is_in_cohort = false;
752 break;
753 }
754 Err(e) => {
755 return Err(InconclusiveMatchError::new(&format!(
757 "Cannot evaluate cohort '{}' property '{}': {}",
758 cohort_id, cohort_prop.key, e.message
759 )));
760 }
761 }
762 }
763
764 Ok(match property.operator.as_str() {
766 "in" => is_in_cohort,
767 "not_in" => !is_in_cohort,
768 op => {
769 return Err(InconclusiveMatchError::new(&format!(
770 "Unknown cohort operator: {}",
771 op
772 )));
773 }
774 })
775}
776
777fn match_flag_dependency_property(
779 property: &Property,
780 ctx: &EvaluationContext,
781) -> Result<bool, InconclusiveMatchError> {
782 let flag_key = property
784 .key
785 .strip_prefix("$feature/")
786 .ok_or_else(|| InconclusiveMatchError::new("Invalid flag dependency format"))?;
787
788 let flag = ctx.flags.get(flag_key).ok_or_else(|| {
789 InconclusiveMatchError::new(&format!("Flag '{}' not found in local cache", flag_key))
790 })?;
791
792 let empty_props = HashMap::new();
795 let flag_value = match_feature_flag(
796 flag,
797 ctx.distinct_id,
798 &empty_props,
799 ctx.groups,
800 ctx.group_properties,
801 ctx.group_type_mapping,
802 )?;
803
804 let expected = &property.value;
806
807 let matches = match (&flag_value, expected) {
808 (FlagValue::Boolean(b), serde_json::Value::Bool(expected_b)) => b == expected_b,
809 (FlagValue::String(s), serde_json::Value::String(expected_s)) => {
810 s.eq_ignore_ascii_case(expected_s)
811 }
812 (FlagValue::Boolean(true), serde_json::Value::String(s)) => {
813 s.is_empty() || s == "true"
816 }
817 (FlagValue::Boolean(false), serde_json::Value::String(s)) => s.is_empty() || s == "false",
818 (FlagValue::String(s), serde_json::Value::Bool(true)) => {
819 !s.is_empty()
821 }
822 (FlagValue::String(_), serde_json::Value::Bool(false)) => false,
823 _ => false,
824 };
825
826 Ok(match property.operator.as_str() {
828 "exact" => matches,
829 "is_not" => !matches,
830 op => {
831 return Err(InconclusiveMatchError::new(&format!(
832 "Unknown flag dependency operator: {}",
833 op
834 )));
835 }
836 })
837}
838
839fn parse_relative_date(value: &str) -> Option<DateTime<Utc>> {
842 let value = value.trim();
843 if value.len() < 3 || !value.starts_with('-') {
845 return None;
846 }
847
848 let (num_str, unit) = value[1..].split_at(value.len() - 2);
849 let num: i64 = num_str.parse().ok()?;
850
851 let duration = match unit {
852 "h" => chrono::Duration::hours(num),
853 "d" => chrono::Duration::days(num),
854 "w" => chrono::Duration::weeks(num),
855 "m" => chrono::Duration::days(num * 30), "y" => chrono::Duration::days(num * 365), _ => return None,
858 };
859
860 Some(Utc::now() - duration)
861}
862
863fn parse_date_value(value: &serde_json::Value) -> Option<DateTime<Utc>> {
865 let date_str = value.as_str()?;
866
867 if date_str.starts_with('-') && date_str.len() > 1 {
869 if let Some(dt) = parse_relative_date(date_str) {
870 return Some(dt);
871 }
872 }
873
874 if let Ok(dt) = DateTime::parse_from_rfc3339(date_str) {
876 return Some(dt.with_timezone(&Utc));
877 }
878
879 if let Ok(date) = NaiveDate::parse_from_str(date_str, "%Y-%m-%d") {
881 return Some(
882 date.and_hms_opt(0, 0, 0)
883 .expect("midnight is always valid")
884 .and_utc(),
885 );
886 }
887
888 None
889}
890
891type SemverTuple = (u64, u64, u64);
893
894fn parse_semver(value: &str) -> Option<SemverTuple> {
906 let value = value.trim();
907 if value.is_empty() {
908 return None;
909 }
910
911 let value = value
913 .strip_prefix('v')
914 .or_else(|| value.strip_prefix('V'))
915 .unwrap_or(value);
916 if value.is_empty() {
917 return None;
918 }
919
920 let value = value.split(['-', '+']).next().unwrap_or(value);
922 if value.is_empty() {
923 return None;
924 }
925
926 if value.starts_with('.') {
928 return None;
929 }
930
931 let parts: Vec<&str> = value.split('.').collect();
933 if parts.is_empty() {
934 return None;
935 }
936
937 let major = parse_semver_numeric(parts.first()?)?;
938 let minor = parts.get(1).map_or(Some(0), |s| parse_semver_numeric(s))?;
939 let patch = parts.get(2).map_or(Some(0), |s| parse_semver_numeric(s))?;
940
941 Some((major, minor, patch))
942}
943
944fn parse_semver_numeric(part: &str) -> Option<u64> {
949 if part.is_empty() || !part.bytes().all(|b| b.is_ascii_digit()) {
950 return None;
951 }
952 if part.len() > 1 && part.starts_with('0') {
953 return None;
954 }
955 part.parse().ok()
956}
957
958fn parse_semver_wildcard(pattern: &str) -> Option<(SemverTuple, SemverTuple)> {
961 let pattern = pattern.trim();
962 if pattern.is_empty() {
963 return None;
964 }
965
966 let pattern = pattern
968 .strip_prefix('v')
969 .or_else(|| pattern.strip_prefix('V'))
970 .unwrap_or(pattern);
971 if pattern.is_empty() {
972 return None;
973 }
974
975 let parts: Vec<&str> = pattern.split('.').collect();
976
977 match parts.as_slice() {
978 [major_str, "*"] => {
980 let major = parse_semver_numeric(major_str)?;
981 Some(((major, 0, 0), (major + 1, 0, 0)))
982 }
983 [major_str, minor_str, "*"] => {
985 let major = parse_semver_numeric(major_str)?;
986 let minor = parse_semver_numeric(minor_str)?;
987 Some(((major, minor, 0), (major, minor + 1, 0)))
988 }
989 _ => None,
990 }
991}
992
993fn compute_tilde_bounds(version: SemverTuple) -> (SemverTuple, SemverTuple) {
995 let (major, minor, patch) = version;
996 ((major, minor, patch), (major, minor + 1, 0))
997}
998
999fn compute_caret_bounds(version: SemverTuple) -> (SemverTuple, SemverTuple) {
1004 let (major, minor, patch) = version;
1005 if major > 0 {
1006 ((major, minor, patch), (major + 1, 0, 0))
1007 } else if minor > 0 {
1008 ((0, minor, patch), (0, minor + 1, 0))
1009 } else {
1010 ((0, 0, patch), (0, 0, patch + 1))
1011 }
1012}
1013
1014fn match_property(
1015 property: &Property,
1016 properties: &HashMap<String, serde_json::Value>,
1017) -> Result<bool, InconclusiveMatchError> {
1018 let value = match properties.get(&property.key) {
1019 Some(v) => v,
1020 None => {
1021 if property.operator == "is_not_set" {
1023 return Ok(true);
1024 }
1025 if property.operator == "is_set" {
1027 return Ok(false);
1028 }
1029 return Err(InconclusiveMatchError::new(&format!(
1031 "Property '{}' not found in provided properties",
1032 property.key
1033 )));
1034 }
1035 };
1036
1037 Ok(match property.operator.as_str() {
1038 "exact" => {
1039 if property.value.is_array() {
1040 if let Some(arr) = property.value.as_array() {
1041 for val in arr {
1042 if compare_values(val, value) {
1043 return Ok(true);
1044 }
1045 }
1046 return Ok(false);
1047 }
1048 }
1049 compare_values(&property.value, value)
1050 }
1051 "is_not" => {
1052 if property.value.is_array() {
1053 if let Some(arr) = property.value.as_array() {
1054 for val in arr {
1055 if compare_values(val, value) {
1056 return Ok(false);
1057 }
1058 }
1059 return Ok(true);
1060 }
1061 }
1062 !compare_values(&property.value, value)
1063 }
1064 "is_set" => true, "is_not_set" => false, "icontains" => {
1067 let prop_str = value_to_string(value);
1068 let search_str = value_to_string(&property.value);
1069 prop_str.to_lowercase().contains(&search_str.to_lowercase())
1070 }
1071 "not_icontains" => {
1072 let prop_str = value_to_string(value);
1073 let search_str = value_to_string(&property.value);
1074 !prop_str.to_lowercase().contains(&search_str.to_lowercase())
1075 }
1076 "regex" => {
1077 let prop_str = value_to_string(value);
1078 let regex_str = value_to_string(&property.value);
1079 get_cached_regex(®ex_str)
1080 .map(|re| re.is_match(&prop_str))
1081 .unwrap_or(false)
1082 }
1083 "not_regex" => {
1084 let prop_str = value_to_string(value);
1085 let regex_str = value_to_string(&property.value);
1086 get_cached_regex(®ex_str)
1087 .map(|re| !re.is_match(&prop_str))
1088 .unwrap_or(true)
1089 }
1090 "gt" | "gte" | "lt" | "lte" => compare_numeric(&property.operator, &property.value, value),
1091 "is_date_before" | "is_date_after" => {
1092 let target_date = parse_date_value(&property.value).ok_or_else(|| {
1093 InconclusiveMatchError::new(&format!(
1094 "Unable to parse target date value: {:?}",
1095 property.value
1096 ))
1097 })?;
1098
1099 let prop_date = parse_date_value(value).ok_or_else(|| {
1100 InconclusiveMatchError::new(&format!(
1101 "Unable to parse property date value for '{}': {:?}",
1102 property.key, value
1103 ))
1104 })?;
1105
1106 if property.operator == "is_date_before" {
1107 prop_date < target_date
1108 } else {
1109 prop_date > target_date
1110 }
1111 }
1112 "semver_eq" | "semver_neq" | "semver_gt" | "semver_gte" | "semver_lt" | "semver_lte" => {
1114 let prop_str = value_to_string(value);
1115 let target_str = value_to_string(&property.value);
1116
1117 let prop_version = parse_semver(&prop_str).ok_or_else(|| {
1118 InconclusiveMatchError::new(&format!(
1119 "Unable to parse property semver value for '{}': {:?}",
1120 property.key, value
1121 ))
1122 })?;
1123
1124 let target_version = parse_semver(&target_str).ok_or_else(|| {
1125 InconclusiveMatchError::new(&format!(
1126 "Unable to parse target semver value: {:?}",
1127 property.value
1128 ))
1129 })?;
1130
1131 match property.operator.as_str() {
1132 "semver_eq" => prop_version == target_version,
1133 "semver_neq" => prop_version != target_version,
1134 "semver_gt" => prop_version > target_version,
1135 "semver_gte" => prop_version >= target_version,
1136 "semver_lt" => prop_version < target_version,
1137 "semver_lte" => prop_version <= target_version,
1138 _ => unreachable!(),
1139 }
1140 }
1141 "semver_tilde" => {
1142 let prop_str = value_to_string(value);
1143 let target_str = value_to_string(&property.value);
1144
1145 let prop_version = parse_semver(&prop_str).ok_or_else(|| {
1146 InconclusiveMatchError::new(&format!(
1147 "Unable to parse property semver value for '{}': {:?}",
1148 property.key, value
1149 ))
1150 })?;
1151
1152 let target_version = parse_semver(&target_str).ok_or_else(|| {
1153 InconclusiveMatchError::new(&format!(
1154 "Unable to parse target semver value: {:?}",
1155 property.value
1156 ))
1157 })?;
1158
1159 let (lower, upper) = compute_tilde_bounds(target_version);
1160 prop_version >= lower && prop_version < upper
1161 }
1162 "semver_caret" => {
1163 let prop_str = value_to_string(value);
1164 let target_str = value_to_string(&property.value);
1165
1166 let prop_version = parse_semver(&prop_str).ok_or_else(|| {
1167 InconclusiveMatchError::new(&format!(
1168 "Unable to parse property semver value for '{}': {:?}",
1169 property.key, value
1170 ))
1171 })?;
1172
1173 let target_version = parse_semver(&target_str).ok_or_else(|| {
1174 InconclusiveMatchError::new(&format!(
1175 "Unable to parse target semver value: {:?}",
1176 property.value
1177 ))
1178 })?;
1179
1180 let (lower, upper) = compute_caret_bounds(target_version);
1181 prop_version >= lower && prop_version < upper
1182 }
1183 "semver_wildcard" => {
1184 let prop_str = value_to_string(value);
1185 let target_str = value_to_string(&property.value);
1186
1187 let prop_version = parse_semver(&prop_str).ok_or_else(|| {
1188 InconclusiveMatchError::new(&format!(
1189 "Unable to parse property semver value for '{}': {:?}",
1190 property.key, value
1191 ))
1192 })?;
1193
1194 let (lower, upper) = parse_semver_wildcard(&target_str).ok_or_else(|| {
1195 InconclusiveMatchError::new(&format!(
1196 "Unable to parse target semver wildcard pattern: {:?}",
1197 property.value
1198 ))
1199 })?;
1200
1201 prop_version >= lower && prop_version < upper
1202 }
1203 unknown => {
1204 return Err(InconclusiveMatchError::new(&format!(
1205 "Unknown operator: {}",
1206 unknown
1207 )));
1208 }
1209 })
1210}
1211
1212fn compare_values(a: &serde_json::Value, b: &serde_json::Value) -> bool {
1213 if let (Some(a_str), Some(b_str)) = (a.as_str(), b.as_str()) {
1215 return a_str.eq_ignore_ascii_case(b_str);
1216 }
1217
1218 a == b
1220}
1221
1222fn value_to_string(value: &serde_json::Value) -> String {
1223 match value {
1224 serde_json::Value::String(s) => s.clone(),
1225 serde_json::Value::Number(n) => n.to_string(),
1226 serde_json::Value::Bool(b) => b.to_string(),
1227 _ => value.to_string(),
1228 }
1229}
1230
1231fn compare_numeric(
1232 operator: &str,
1233 property_value: &serde_json::Value,
1234 value: &serde_json::Value,
1235) -> bool {
1236 let prop_num = match property_value {
1237 serde_json::Value::Number(n) => n.as_f64(),
1238 serde_json::Value::String(s) => s.parse::<f64>().ok(),
1239 _ => None,
1240 };
1241
1242 let val_num = match value {
1243 serde_json::Value::Number(n) => n.as_f64(),
1244 serde_json::Value::String(s) => s.parse::<f64>().ok(),
1245 _ => None,
1246 };
1247
1248 if let (Some(prop), Some(val)) = (prop_num, val_num) {
1249 match operator {
1250 "gt" => val > prop,
1251 "gte" => val >= prop,
1252 "lt" => val < prop,
1253 "lte" => val <= prop,
1254 _ => false,
1255 }
1256 } else {
1257 let prop_str = value_to_string(property_value);
1259 let val_str = value_to_string(value);
1260 match operator {
1261 "gt" => val_str > prop_str,
1262 "gte" => val_str >= prop_str,
1263 "lt" => val_str < prop_str,
1264 "lte" => val_str <= prop_str,
1265 _ => false,
1266 }
1267 }
1268}
1269
1270#[cfg(test)]
1271mod tests {
1272 use super::*;
1273 use serde_json::json;
1274
1275 const TEST_SALT: &str = "test-salt";
1277
1278 #[test]
1279 fn test_hash_key() {
1280 let hash = hash_key("test-flag", "user-123", TEST_SALT);
1281 assert!((0.0..=1.0).contains(&hash));
1282
1283 let hash2 = hash_key("test-flag", "user-123", TEST_SALT);
1285 assert_eq!(hash, hash2);
1286
1287 let hash3 = hash_key("test-flag", "user-456", TEST_SALT);
1289 assert_ne!(hash, hash3);
1290 }
1291
1292 #[test]
1293 fn test_simple_flag_match() {
1294 let flag = FeatureFlag {
1295 key: "test-flag".to_string(),
1296 active: true,
1297 filters: FeatureFlagFilters {
1298 groups: vec![FeatureFlagCondition {
1299 properties: vec![],
1300 rollout_percentage: Some(100.0),
1301 variant: None,
1302 aggregation_group_type_index: None,
1303 }],
1304 multivariate: None,
1305 payloads: HashMap::new(),
1306 aggregation_group_type_index: None,
1307 },
1308 };
1309
1310 let properties = HashMap::new();
1311 let result = match_feature_flag(
1312 &flag,
1313 "user-123",
1314 &properties,
1315 &HashMap::new(),
1316 &HashMap::new(),
1317 &HashMap::new(),
1318 )
1319 .unwrap();
1320 assert_eq!(result, FlagValue::Boolean(true));
1321 }
1322
1323 #[test]
1324 fn test_property_matching() {
1325 let prop = Property {
1326 key: "country".to_string(),
1327 value: json!("US"),
1328 operator: "exact".to_string(),
1329 property_type: None,
1330 };
1331
1332 let mut properties = HashMap::new();
1333 properties.insert("country".to_string(), json!("US"));
1334
1335 assert!(match_property(&prop, &properties).unwrap());
1336
1337 properties.insert("country".to_string(), json!("UK"));
1338 assert!(!match_property(&prop, &properties).unwrap());
1339 }
1340
1341 #[test]
1342 fn test_multivariate_variants() {
1343 let flag = FeatureFlag {
1344 key: "test-flag".to_string(),
1345 active: true,
1346 filters: FeatureFlagFilters {
1347 groups: vec![FeatureFlagCondition {
1348 properties: vec![],
1349 rollout_percentage: Some(100.0),
1350 variant: None,
1351 aggregation_group_type_index: None,
1352 }],
1353 multivariate: Some(MultivariateFilter {
1354 variants: vec![
1355 MultivariateVariant {
1356 key: "control".to_string(),
1357 rollout_percentage: 50.0,
1358 },
1359 MultivariateVariant {
1360 key: "test".to_string(),
1361 rollout_percentage: 50.0,
1362 },
1363 ],
1364 }),
1365 payloads: HashMap::new(),
1366 aggregation_group_type_index: None,
1367 },
1368 };
1369
1370 let properties = HashMap::new();
1371 let result = match_feature_flag(
1372 &flag,
1373 "user-123",
1374 &properties,
1375 &HashMap::new(),
1376 &HashMap::new(),
1377 &HashMap::new(),
1378 )
1379 .unwrap();
1380
1381 match result {
1382 FlagValue::String(variant) => {
1383 assert!(variant == "control" || variant == "test");
1384 }
1385 _ => panic!("Expected string variant"),
1386 }
1387 }
1388
1389 #[test]
1390 fn test_inactive_flag() {
1391 let flag = FeatureFlag {
1392 key: "inactive-flag".to_string(),
1393 active: false,
1394 filters: FeatureFlagFilters {
1395 groups: vec![FeatureFlagCondition {
1396 properties: vec![],
1397 rollout_percentage: Some(100.0),
1398 variant: None,
1399 aggregation_group_type_index: None,
1400 }],
1401 multivariate: None,
1402 payloads: HashMap::new(),
1403 aggregation_group_type_index: None,
1404 },
1405 };
1406
1407 let properties = HashMap::new();
1408 let result = match_feature_flag(
1409 &flag,
1410 "user-123",
1411 &properties,
1412 &HashMap::new(),
1413 &HashMap::new(),
1414 &HashMap::new(),
1415 )
1416 .unwrap();
1417 assert_eq!(result, FlagValue::Boolean(false));
1418 }
1419
1420 #[test]
1421 fn test_rollout_percentage() {
1422 let flag = FeatureFlag {
1423 key: "rollout-flag".to_string(),
1424 active: true,
1425 filters: FeatureFlagFilters {
1426 groups: vec![FeatureFlagCondition {
1427 properties: vec![],
1428 rollout_percentage: Some(30.0), variant: None,
1430 aggregation_group_type_index: None,
1431 }],
1432 multivariate: None,
1433 payloads: HashMap::new(),
1434 aggregation_group_type_index: None,
1435 },
1436 };
1437
1438 let properties = HashMap::new();
1439
1440 let mut enabled_count = 0;
1442 for i in 0..1000 {
1443 let result = match_feature_flag(
1444 &flag,
1445 &format!("user-{}", i),
1446 &properties,
1447 &HashMap::new(),
1448 &HashMap::new(),
1449 &HashMap::new(),
1450 )
1451 .unwrap();
1452 if result == FlagValue::Boolean(true) {
1453 enabled_count += 1;
1454 }
1455 }
1456
1457 assert!(enabled_count > 250 && enabled_count < 350);
1459 }
1460
1461 #[test]
1462 fn test_regex_operator() {
1463 let prop = Property {
1464 key: "email".to_string(),
1465 value: json!(".*@company\\.com$"),
1466 operator: "regex".to_string(),
1467 property_type: None,
1468 };
1469
1470 let mut properties = HashMap::new();
1471 properties.insert("email".to_string(), json!("user@company.com"));
1472 assert!(match_property(&prop, &properties).unwrap());
1473
1474 properties.insert("email".to_string(), json!("user@example.com"));
1475 assert!(!match_property(&prop, &properties).unwrap());
1476 }
1477
1478 #[test]
1479 fn test_icontains_operator() {
1480 let prop = Property {
1481 key: "name".to_string(),
1482 value: json!("ADMIN"),
1483 operator: "icontains".to_string(),
1484 property_type: None,
1485 };
1486
1487 let mut properties = HashMap::new();
1488 properties.insert("name".to_string(), json!("admin_user"));
1489 assert!(match_property(&prop, &properties).unwrap());
1490
1491 properties.insert("name".to_string(), json!("regular_user"));
1492 assert!(!match_property(&prop, &properties).unwrap());
1493 }
1494
1495 #[test]
1496 fn test_numeric_operators() {
1497 let prop_gt = Property {
1499 key: "age".to_string(),
1500 value: json!(18),
1501 operator: "gt".to_string(),
1502 property_type: None,
1503 };
1504
1505 let mut properties = HashMap::new();
1506 properties.insert("age".to_string(), json!(25));
1507 assert!(match_property(&prop_gt, &properties).unwrap());
1508
1509 properties.insert("age".to_string(), json!(15));
1510 assert!(!match_property(&prop_gt, &properties).unwrap());
1511
1512 let prop_lte = Property {
1514 key: "score".to_string(),
1515 value: json!(100),
1516 operator: "lte".to_string(),
1517 property_type: None,
1518 };
1519
1520 properties.insert("score".to_string(), json!(100));
1521 assert!(match_property(&prop_lte, &properties).unwrap());
1522
1523 properties.insert("score".to_string(), json!(101));
1524 assert!(!match_property(&prop_lte, &properties).unwrap());
1525 }
1526
1527 #[test]
1528 fn test_is_set_operator() {
1529 let prop = Property {
1530 key: "email".to_string(),
1531 value: json!(true),
1532 operator: "is_set".to_string(),
1533 property_type: None,
1534 };
1535
1536 let mut properties = HashMap::new();
1537 properties.insert("email".to_string(), json!("test@example.com"));
1538 assert!(match_property(&prop, &properties).unwrap());
1539
1540 properties.remove("email");
1541 assert!(!match_property(&prop, &properties).unwrap());
1542 }
1543
1544 #[test]
1545 fn test_is_not_set_operator() {
1546 let prop = Property {
1547 key: "phone".to_string(),
1548 value: json!(true),
1549 operator: "is_not_set".to_string(),
1550 property_type: None,
1551 };
1552
1553 let mut properties = HashMap::new();
1554 assert!(match_property(&prop, &properties).unwrap());
1555
1556 properties.insert("phone".to_string(), json!("+1234567890"));
1557 assert!(!match_property(&prop, &properties).unwrap());
1558 }
1559
1560 #[test]
1561 fn test_empty_groups() {
1562 let flag = FeatureFlag {
1563 key: "empty-groups".to_string(),
1564 active: true,
1565 filters: FeatureFlagFilters {
1566 groups: vec![],
1567 multivariate: None,
1568 payloads: HashMap::new(),
1569 aggregation_group_type_index: None,
1570 },
1571 };
1572
1573 let properties = HashMap::new();
1574 let result = match_feature_flag(
1575 &flag,
1576 "user-123",
1577 &properties,
1578 &HashMap::new(),
1579 &HashMap::new(),
1580 &HashMap::new(),
1581 )
1582 .unwrap();
1583 assert_eq!(result, FlagValue::Boolean(false));
1584 }
1585
1586 #[test]
1587 fn test_hash_scale_constant() {
1588 assert_eq!(LONG_SCALE, 0xFFFFFFFFFFFFFFFu64 as f64);
1590 assert_ne!(LONG_SCALE, 0xFFFFFFFFFFFFFFFFu64 as f64);
1591 }
1592
1593 #[test]
1596 fn test_unknown_operator_returns_inconclusive_error() {
1597 let prop = Property {
1598 key: "status".to_string(),
1599 value: json!("active"),
1600 operator: "unknown_operator".to_string(),
1601 property_type: None,
1602 };
1603
1604 let mut properties = HashMap::new();
1605 properties.insert("status".to_string(), json!("active"));
1606
1607 let result = match_property(&prop, &properties);
1608 assert!(result.is_err());
1609 let err = result.unwrap_err();
1610 assert!(err.message.contains("unknown_operator"));
1611 }
1612
1613 #[test]
1614 fn test_is_date_before_with_relative_date() {
1615 let prop = Property {
1616 key: "signup_date".to_string(),
1617 value: json!("-7d"), operator: "is_date_before".to_string(),
1619 property_type: None,
1620 };
1621
1622 let mut properties = HashMap::new();
1623 let ten_days_ago = chrono::Utc::now() - chrono::Duration::days(10);
1625 properties.insert(
1626 "signup_date".to_string(),
1627 json!(ten_days_ago.format("%Y-%m-%d").to_string()),
1628 );
1629 assert!(match_property(&prop, &properties).unwrap());
1630
1631 let three_days_ago = chrono::Utc::now() - chrono::Duration::days(3);
1633 properties.insert(
1634 "signup_date".to_string(),
1635 json!(three_days_ago.format("%Y-%m-%d").to_string()),
1636 );
1637 assert!(!match_property(&prop, &properties).unwrap());
1638 }
1639
1640 #[test]
1641 fn test_is_date_after_with_relative_date() {
1642 let prop = Property {
1643 key: "last_seen".to_string(),
1644 value: json!("-30d"), operator: "is_date_after".to_string(),
1646 property_type: None,
1647 };
1648
1649 let mut properties = HashMap::new();
1650 let ten_days_ago = chrono::Utc::now() - chrono::Duration::days(10);
1652 properties.insert(
1653 "last_seen".to_string(),
1654 json!(ten_days_ago.format("%Y-%m-%d").to_string()),
1655 );
1656 assert!(match_property(&prop, &properties).unwrap());
1657
1658 let sixty_days_ago = chrono::Utc::now() - chrono::Duration::days(60);
1660 properties.insert(
1661 "last_seen".to_string(),
1662 json!(sixty_days_ago.format("%Y-%m-%d").to_string()),
1663 );
1664 assert!(!match_property(&prop, &properties).unwrap());
1665 }
1666
1667 #[test]
1668 fn test_is_date_before_with_iso_date() {
1669 let prop = Property {
1670 key: "expiry_date".to_string(),
1671 value: json!("2024-06-15"),
1672 operator: "is_date_before".to_string(),
1673 property_type: None,
1674 };
1675
1676 let mut properties = HashMap::new();
1677 properties.insert("expiry_date".to_string(), json!("2024-06-10"));
1678 assert!(match_property(&prop, &properties).unwrap());
1679
1680 properties.insert("expiry_date".to_string(), json!("2024-06-20"));
1681 assert!(!match_property(&prop, &properties).unwrap());
1682 }
1683
1684 #[test]
1685 fn test_is_date_after_with_iso_date() {
1686 let prop = Property {
1687 key: "start_date".to_string(),
1688 value: json!("2024-01-01"),
1689 operator: "is_date_after".to_string(),
1690 property_type: None,
1691 };
1692
1693 let mut properties = HashMap::new();
1694 properties.insert("start_date".to_string(), json!("2024-03-15"));
1695 assert!(match_property(&prop, &properties).unwrap());
1696
1697 properties.insert("start_date".to_string(), json!("2023-12-01"));
1698 assert!(!match_property(&prop, &properties).unwrap());
1699 }
1700
1701 #[test]
1702 fn test_is_date_with_relative_hours() {
1703 let prop = Property {
1704 key: "last_active".to_string(),
1705 value: json!("-24h"), operator: "is_date_after".to_string(),
1707 property_type: None,
1708 };
1709
1710 let mut properties = HashMap::new();
1711 let twelve_hours_ago = chrono::Utc::now() - chrono::Duration::hours(12);
1713 properties.insert(
1714 "last_active".to_string(),
1715 json!(twelve_hours_ago.to_rfc3339()),
1716 );
1717 assert!(match_property(&prop, &properties).unwrap());
1718
1719 let forty_eight_hours_ago = chrono::Utc::now() - chrono::Duration::hours(48);
1721 properties.insert(
1722 "last_active".to_string(),
1723 json!(forty_eight_hours_ago.to_rfc3339()),
1724 );
1725 assert!(!match_property(&prop, &properties).unwrap());
1726 }
1727
1728 #[test]
1729 fn test_is_date_with_relative_weeks() {
1730 let prop = Property {
1731 key: "joined".to_string(),
1732 value: json!("-2w"), operator: "is_date_before".to_string(),
1734 property_type: None,
1735 };
1736
1737 let mut properties = HashMap::new();
1738 let three_weeks_ago = chrono::Utc::now() - chrono::Duration::weeks(3);
1740 properties.insert(
1741 "joined".to_string(),
1742 json!(three_weeks_ago.format("%Y-%m-%d").to_string()),
1743 );
1744 assert!(match_property(&prop, &properties).unwrap());
1745
1746 let one_week_ago = chrono::Utc::now() - chrono::Duration::weeks(1);
1748 properties.insert(
1749 "joined".to_string(),
1750 json!(one_week_ago.format("%Y-%m-%d").to_string()),
1751 );
1752 assert!(!match_property(&prop, &properties).unwrap());
1753 }
1754
1755 #[test]
1756 fn test_is_date_with_relative_months() {
1757 let prop = Property {
1758 key: "subscription_date".to_string(),
1759 value: json!("-3m"), operator: "is_date_after".to_string(),
1761 property_type: None,
1762 };
1763
1764 let mut properties = HashMap::new();
1765 let one_month_ago = chrono::Utc::now() - chrono::Duration::days(30);
1767 properties.insert(
1768 "subscription_date".to_string(),
1769 json!(one_month_ago.format("%Y-%m-%d").to_string()),
1770 );
1771 assert!(match_property(&prop, &properties).unwrap());
1772
1773 let six_months_ago = chrono::Utc::now() - chrono::Duration::days(180);
1775 properties.insert(
1776 "subscription_date".to_string(),
1777 json!(six_months_ago.format("%Y-%m-%d").to_string()),
1778 );
1779 assert!(!match_property(&prop, &properties).unwrap());
1780 }
1781
1782 #[test]
1783 fn test_is_date_with_relative_years() {
1784 let prop = Property {
1785 key: "created_at".to_string(),
1786 value: json!("-1y"), operator: "is_date_before".to_string(),
1788 property_type: None,
1789 };
1790
1791 let mut properties = HashMap::new();
1792 let two_years_ago = chrono::Utc::now() - chrono::Duration::days(730);
1794 properties.insert(
1795 "created_at".to_string(),
1796 json!(two_years_ago.format("%Y-%m-%d").to_string()),
1797 );
1798 assert!(match_property(&prop, &properties).unwrap());
1799
1800 let six_months_ago = chrono::Utc::now() - chrono::Duration::days(180);
1802 properties.insert(
1803 "created_at".to_string(),
1804 json!(six_months_ago.format("%Y-%m-%d").to_string()),
1805 );
1806 assert!(!match_property(&prop, &properties).unwrap());
1807 }
1808
1809 #[test]
1810 fn test_is_date_with_invalid_date_format() {
1811 let prop = Property {
1812 key: "date".to_string(),
1813 value: json!("-7d"),
1814 operator: "is_date_before".to_string(),
1815 property_type: None,
1816 };
1817
1818 let mut properties = HashMap::new();
1819 properties.insert("date".to_string(), json!("not-a-date"));
1820
1821 let result = match_property(&prop, &properties);
1823 assert!(result.is_err());
1824 }
1825
1826 #[test]
1827 fn test_is_date_with_iso_datetime() {
1828 let prop = Property {
1829 key: "event_time".to_string(),
1830 value: json!("2024-06-15T10:30:00Z"),
1831 operator: "is_date_before".to_string(),
1832 property_type: None,
1833 };
1834
1835 let mut properties = HashMap::new();
1836 properties.insert("event_time".to_string(), json!("2024-06-15T08:00:00Z"));
1837 assert!(match_property(&prop, &properties).unwrap());
1838
1839 properties.insert("event_time".to_string(), json!("2024-06-15T12:00:00Z"));
1840 assert!(!match_property(&prop, &properties).unwrap());
1841 }
1842
1843 #[test]
1846 fn test_cohort_membership_in() {
1847 let mut cohorts = HashMap::new();
1849 cohorts.insert(
1850 "cohort_1".to_string(),
1851 CohortDefinition::new(
1852 "cohort_1".to_string(),
1853 vec![Property {
1854 key: "country".to_string(),
1855 value: json!("US"),
1856 operator: "exact".to_string(),
1857 property_type: None,
1858 }],
1859 ),
1860 );
1861
1862 let prop = Property {
1864 key: "$cohort".to_string(),
1865 value: json!("cohort_1"),
1866 operator: "in".to_string(),
1867 property_type: Some("cohort".to_string()),
1868 };
1869
1870 let mut properties = HashMap::new();
1872 properties.insert("country".to_string(), json!("US"));
1873
1874 let ctx = EvaluationContext {
1875 cohorts: &cohorts,
1876 flags: &HashMap::new(),
1877 distinct_id: "user-123",
1878 groups: &HashMap::new(),
1879 group_properties: &HashMap::new(),
1880 group_type_mapping: &HashMap::new(),
1881 };
1882 assert!(match_property_with_context(&prop, &properties, &ctx).unwrap());
1883
1884 properties.insert("country".to_string(), json!("UK"));
1886 assert!(!match_property_with_context(&prop, &properties, &ctx).unwrap());
1887 }
1888
1889 #[test]
1890 fn test_cohort_membership_not_in() {
1891 let mut cohorts = HashMap::new();
1892 cohorts.insert(
1893 "cohort_blocked".to_string(),
1894 CohortDefinition::new(
1895 "cohort_blocked".to_string(),
1896 vec![Property {
1897 key: "status".to_string(),
1898 value: json!("blocked"),
1899 operator: "exact".to_string(),
1900 property_type: None,
1901 }],
1902 ),
1903 );
1904
1905 let prop = Property {
1906 key: "$cohort".to_string(),
1907 value: json!("cohort_blocked"),
1908 operator: "not_in".to_string(),
1909 property_type: Some("cohort".to_string()),
1910 };
1911
1912 let mut properties = HashMap::new();
1913 properties.insert("status".to_string(), json!("active"));
1914
1915 let ctx = EvaluationContext {
1916 cohorts: &cohorts,
1917 flags: &HashMap::new(),
1918 distinct_id: "user-123",
1919 groups: &HashMap::new(),
1920 group_properties: &HashMap::new(),
1921 group_type_mapping: &HashMap::new(),
1922 };
1923 assert!(match_property_with_context(&prop, &properties, &ctx).unwrap());
1925
1926 properties.insert("status".to_string(), json!("blocked"));
1928 assert!(!match_property_with_context(&prop, &properties, &ctx).unwrap());
1929 }
1930
1931 #[test]
1932 fn test_cohort_not_found_returns_inconclusive() {
1933 let cohorts = HashMap::new(); let prop = Property {
1936 key: "$cohort".to_string(),
1937 value: json!("nonexistent_cohort"),
1938 operator: "in".to_string(),
1939 property_type: Some("cohort".to_string()),
1940 };
1941
1942 let properties = HashMap::new();
1943 let ctx = EvaluationContext {
1944 cohorts: &cohorts,
1945 flags: &HashMap::new(),
1946 distinct_id: "user-123",
1947 groups: &HashMap::new(),
1948 group_properties: &HashMap::new(),
1949 group_type_mapping: &HashMap::new(),
1950 };
1951
1952 let result = match_property_with_context(&prop, &properties, &ctx);
1953 assert!(result.is_err());
1954 assert!(result.unwrap_err().message.contains("Cohort"));
1955 }
1956
1957 #[test]
1960 fn test_flag_dependency_enabled() {
1961 let mut flags = HashMap::new();
1962 flags.insert(
1963 "prerequisite-flag".to_string(),
1964 FeatureFlag {
1965 key: "prerequisite-flag".to_string(),
1966 active: true,
1967 filters: FeatureFlagFilters {
1968 groups: vec![FeatureFlagCondition {
1969 properties: vec![],
1970 rollout_percentage: Some(100.0),
1971 variant: None,
1972 aggregation_group_type_index: None,
1973 }],
1974 multivariate: None,
1975 payloads: HashMap::new(),
1976 aggregation_group_type_index: None,
1977 },
1978 },
1979 );
1980
1981 let prop = Property {
1983 key: "$feature/prerequisite-flag".to_string(),
1984 value: json!(true),
1985 operator: "exact".to_string(),
1986 property_type: None,
1987 };
1988
1989 let properties = HashMap::new();
1990 let ctx = EvaluationContext {
1991 cohorts: &HashMap::new(),
1992 flags: &flags,
1993 distinct_id: "user-123",
1994 groups: &HashMap::new(),
1995 group_properties: &HashMap::new(),
1996 group_type_mapping: &HashMap::new(),
1997 };
1998
1999 assert!(match_property_with_context(&prop, &properties, &ctx).unwrap());
2001 }
2002
2003 #[test]
2004 fn test_flag_dependency_disabled() {
2005 let mut flags = HashMap::new();
2006 flags.insert(
2007 "disabled-flag".to_string(),
2008 FeatureFlag {
2009 key: "disabled-flag".to_string(),
2010 active: false, filters: FeatureFlagFilters {
2012 groups: vec![],
2013 multivariate: None,
2014 payloads: HashMap::new(),
2015 aggregation_group_type_index: None,
2016 },
2017 },
2018 );
2019
2020 let prop = Property {
2022 key: "$feature/disabled-flag".to_string(),
2023 value: json!(true),
2024 operator: "exact".to_string(),
2025 property_type: None,
2026 };
2027
2028 let properties = HashMap::new();
2029 let ctx = EvaluationContext {
2030 cohorts: &HashMap::new(),
2031 flags: &flags,
2032 distinct_id: "user-123",
2033 groups: &HashMap::new(),
2034 group_properties: &HashMap::new(),
2035 group_type_mapping: &HashMap::new(),
2036 };
2037
2038 assert!(!match_property_with_context(&prop, &properties, &ctx).unwrap());
2040 }
2041
2042 #[test]
2043 fn test_flag_dependency_variant_match() {
2044 let mut flags = HashMap::new();
2045 flags.insert(
2046 "ab-test-flag".to_string(),
2047 FeatureFlag {
2048 key: "ab-test-flag".to_string(),
2049 active: true,
2050 filters: FeatureFlagFilters {
2051 groups: vec![FeatureFlagCondition {
2052 properties: vec![],
2053 rollout_percentage: Some(100.0),
2054 variant: None,
2055 aggregation_group_type_index: None,
2056 }],
2057 multivariate: Some(MultivariateFilter {
2058 variants: vec![
2059 MultivariateVariant {
2060 key: "control".to_string(),
2061 rollout_percentage: 50.0,
2062 },
2063 MultivariateVariant {
2064 key: "test".to_string(),
2065 rollout_percentage: 50.0,
2066 },
2067 ],
2068 }),
2069 payloads: HashMap::new(),
2070 aggregation_group_type_index: None,
2071 },
2072 },
2073 );
2074
2075 let prop = Property {
2077 key: "$feature/ab-test-flag".to_string(),
2078 value: json!("control"),
2079 operator: "exact".to_string(),
2080 property_type: None,
2081 };
2082
2083 let properties = HashMap::new();
2084 let ctx = EvaluationContext {
2085 cohorts: &HashMap::new(),
2086 flags: &flags,
2087 distinct_id: "user-gets-control", groups: &HashMap::new(),
2089 group_properties: &HashMap::new(),
2090 group_type_mapping: &HashMap::new(),
2091 };
2092
2093 let result = match_property_with_context(&prop, &properties, &ctx);
2095 assert!(result.is_ok());
2096 }
2097
2098 #[test]
2099 fn test_flag_dependency_not_found_returns_inconclusive() {
2100 let flags = HashMap::new(); let prop = Property {
2103 key: "$feature/nonexistent-flag".to_string(),
2104 value: json!(true),
2105 operator: "exact".to_string(),
2106 property_type: None,
2107 };
2108
2109 let properties = HashMap::new();
2110 let ctx = EvaluationContext {
2111 cohorts: &HashMap::new(),
2112 flags: &flags,
2113 distinct_id: "user-123",
2114 groups: &HashMap::new(),
2115 group_properties: &HashMap::new(),
2116 group_type_mapping: &HashMap::new(),
2117 };
2118
2119 let result = match_property_with_context(&prop, &properties, &ctx);
2120 assert!(result.is_err());
2121 assert!(result.unwrap_err().message.contains("Flag"));
2122 }
2123
2124 #[test]
2127 fn test_parse_relative_date_edge_cases() {
2128 let prop = Property {
2130 key: "date".to_string(),
2131 value: json!("placeholder"),
2132 operator: "is_date_before".to_string(),
2133 property_type: None,
2134 };
2135
2136 let mut properties = HashMap::new();
2137 properties.insert("date".to_string(), json!("2024-01-01"));
2138
2139 let empty_prop = Property {
2141 value: json!(""),
2142 ..prop.clone()
2143 };
2144 assert!(match_property(&empty_prop, &properties).is_err());
2145
2146 let dash_prop = Property {
2148 value: json!("-"),
2149 ..prop.clone()
2150 };
2151 assert!(match_property(&dash_prop, &properties).is_err());
2152
2153 let no_unit_prop = Property {
2155 value: json!("-7"),
2156 ..prop.clone()
2157 };
2158 assert!(match_property(&no_unit_prop, &properties).is_err());
2159
2160 let no_number_prop = Property {
2162 value: json!("-d"),
2163 ..prop.clone()
2164 };
2165 assert!(match_property(&no_number_prop, &properties).is_err());
2166
2167 let invalid_unit_prop = Property {
2169 value: json!("-7x"),
2170 ..prop.clone()
2171 };
2172 assert!(match_property(&invalid_unit_prop, &properties).is_err());
2173 }
2174
2175 #[test]
2176 fn test_parse_relative_date_large_values() {
2177 let prop = Property {
2179 key: "created_at".to_string(),
2180 value: json!("-1000d"), operator: "is_date_before".to_string(),
2182 property_type: None,
2183 };
2184
2185 let mut properties = HashMap::new();
2186 let five_years_ago = chrono::Utc::now() - chrono::Duration::days(1825);
2188 properties.insert(
2189 "created_at".to_string(),
2190 json!(five_years_ago.format("%Y-%m-%d").to_string()),
2191 );
2192 assert!(match_property(&prop, &properties).unwrap());
2193 }
2194
2195 #[test]
2198 fn test_regex_with_invalid_pattern_returns_false() {
2199 let prop = Property {
2201 key: "email".to_string(),
2202 value: json!("(unclosed"),
2203 operator: "regex".to_string(),
2204 property_type: None,
2205 };
2206
2207 let mut properties = HashMap::new();
2208 properties.insert("email".to_string(), json!("test@example.com"));
2209
2210 assert!(!match_property(&prop, &properties).unwrap());
2212 }
2213
2214 #[test]
2215 fn test_not_regex_with_invalid_pattern_returns_true() {
2216 let prop = Property {
2218 key: "email".to_string(),
2219 value: json!("(unclosed"),
2220 operator: "not_regex".to_string(),
2221 property_type: None,
2222 };
2223
2224 let mut properties = HashMap::new();
2225 properties.insert("email".to_string(), json!("test@example.com"));
2226
2227 assert!(match_property(&prop, &properties).unwrap());
2229 }
2230
2231 #[test]
2232 fn test_regex_with_various_invalid_patterns() {
2233 let invalid_patterns = vec![
2234 "(unclosed", "[unclosed", "*invalid", "(?P<bad", r"\", ];
2240
2241 for pattern in invalid_patterns {
2242 let prop = Property {
2243 key: "value".to_string(),
2244 value: json!(pattern),
2245 operator: "regex".to_string(),
2246 property_type: None,
2247 };
2248
2249 let mut properties = HashMap::new();
2250 properties.insert("value".to_string(), json!("test"));
2251
2252 assert!(
2254 !match_property(&prop, &properties).unwrap(),
2255 "Invalid pattern '{}' should return false for regex",
2256 pattern
2257 );
2258
2259 let not_regex_prop = Property {
2261 operator: "not_regex".to_string(),
2262 ..prop
2263 };
2264 assert!(
2265 match_property(¬_regex_prop, &properties).unwrap(),
2266 "Invalid pattern '{}' should return true for not_regex",
2267 pattern
2268 );
2269 }
2270 }
2271
2272 #[test]
2275 fn test_parse_semver_basic() {
2276 assert_eq!(parse_semver("1.2.3"), Some((1, 2, 3)));
2277 assert_eq!(parse_semver("0.0.0"), Some((0, 0, 0)));
2278 assert_eq!(parse_semver("10.20.30"), Some((10, 20, 30)));
2279 }
2280
2281 #[test]
2282 fn test_parse_semver_v_prefix() {
2283 assert_eq!(parse_semver("v1.2.3"), Some((1, 2, 3)));
2284 assert_eq!(parse_semver("V1.2.3"), Some((1, 2, 3)));
2285 }
2286
2287 #[test]
2288 fn test_parse_semver_whitespace() {
2289 assert_eq!(parse_semver(" 1.2.3 "), Some((1, 2, 3)));
2290 assert_eq!(parse_semver(" v1.2.3 "), Some((1, 2, 3)));
2291 }
2292
2293 #[test]
2294 fn test_parse_semver_prerelease_stripped() {
2295 assert_eq!(parse_semver("1.2.3-alpha"), Some((1, 2, 3)));
2296 assert_eq!(parse_semver("1.2.3-beta.1"), Some((1, 2, 3)));
2297 assert_eq!(parse_semver("1.2.3-rc.1+build.123"), Some((1, 2, 3)));
2298 assert_eq!(parse_semver("1.2.3+build.456"), Some((1, 2, 3)));
2299 }
2300
2301 #[test]
2302 fn test_parse_semver_partial_versions() {
2303 assert_eq!(parse_semver("1.2"), Some((1, 2, 0)));
2304 assert_eq!(parse_semver("1"), Some((1, 0, 0)));
2305 assert_eq!(parse_semver("v1.2"), Some((1, 2, 0)));
2306 }
2307
2308 #[test]
2309 fn test_parse_semver_extra_components_ignored() {
2310 assert_eq!(parse_semver("1.2.3.4"), Some((1, 2, 3)));
2311 assert_eq!(parse_semver("1.2.3.4.5.6"), Some((1, 2, 3)));
2312 }
2313
2314 #[test]
2315 fn test_parse_semver_leading_zeros_rejected() {
2316 assert_eq!(parse_semver("01.02.03"), None);
2318 assert_eq!(parse_semver("001.002.003"), None);
2319 assert_eq!(parse_semver("1.07.3"), None);
2320 assert_eq!(parse_semver("1.2.03"), None);
2321 assert_eq!(parse_semver("v01.2.3"), None);
2322
2323 assert_eq!(parse_semver("0.1.0"), Some((0, 1, 0)));
2325 assert_eq!(parse_semver("1.0.0"), Some((1, 0, 0)));
2326 assert_eq!(parse_semver("0.0.0"), Some((0, 0, 0)));
2327 }
2328
2329 #[test]
2330 fn test_parse_semver_invalid() {
2331 assert_eq!(parse_semver(""), None);
2332 assert_eq!(parse_semver(" "), None);
2333 assert_eq!(parse_semver("v"), None);
2334 assert_eq!(parse_semver(".1.2.3"), None);
2335 assert_eq!(parse_semver("abc"), None);
2336 assert_eq!(parse_semver("1.abc.3"), None);
2337 assert_eq!(parse_semver("1.2.abc"), None);
2338 assert_eq!(parse_semver("not-a-version"), None);
2339 }
2340
2341 #[test]
2344 fn test_semver_eq_basic() {
2345 let prop = Property {
2346 key: "version".to_string(),
2347 value: json!("1.2.3"),
2348 operator: "semver_eq".to_string(),
2349 property_type: None,
2350 };
2351
2352 let mut properties = HashMap::new();
2353
2354 properties.insert("version".to_string(), json!("1.2.3"));
2355 assert!(match_property(&prop, &properties).unwrap());
2356
2357 properties.insert("version".to_string(), json!("1.2.4"));
2358 assert!(!match_property(&prop, &properties).unwrap());
2359
2360 properties.insert("version".to_string(), json!("1.3.3"));
2361 assert!(!match_property(&prop, &properties).unwrap());
2362
2363 properties.insert("version".to_string(), json!("2.2.3"));
2364 assert!(!match_property(&prop, &properties).unwrap());
2365 }
2366
2367 #[test]
2368 fn test_semver_eq_with_v_prefix() {
2369 let prop = Property {
2370 key: "version".to_string(),
2371 value: json!("1.2.3"),
2372 operator: "semver_eq".to_string(),
2373 property_type: None,
2374 };
2375
2376 let mut properties = HashMap::new();
2377
2378 properties.insert("version".to_string(), json!("v1.2.3"));
2380 assert!(match_property(&prop, &properties).unwrap());
2381
2382 let prop_with_v = Property {
2384 value: json!("v1.2.3"),
2385 ..prop.clone()
2386 };
2387 properties.insert("version".to_string(), json!("1.2.3"));
2388 assert!(match_property(&prop_with_v, &properties).unwrap());
2389 }
2390
2391 #[test]
2392 fn test_semver_eq_prerelease_stripped() {
2393 let prop = Property {
2394 key: "version".to_string(),
2395 value: json!("1.2.3"),
2396 operator: "semver_eq".to_string(),
2397 property_type: None,
2398 };
2399
2400 let mut properties = HashMap::new();
2401
2402 properties.insert("version".to_string(), json!("1.2.3-alpha"));
2403 assert!(match_property(&prop, &properties).unwrap());
2404
2405 properties.insert("version".to_string(), json!("1.2.3-beta.1"));
2406 assert!(match_property(&prop, &properties).unwrap());
2407
2408 properties.insert("version".to_string(), json!("1.2.3+build.456"));
2409 assert!(match_property(&prop, &properties).unwrap());
2410 }
2411
2412 #[test]
2413 fn test_semver_eq_partial_versions() {
2414 let prop = Property {
2415 key: "version".to_string(),
2416 value: json!("1.2.0"),
2417 operator: "semver_eq".to_string(),
2418 property_type: None,
2419 };
2420
2421 let mut properties = HashMap::new();
2422
2423 properties.insert("version".to_string(), json!("1.2"));
2425 assert!(match_property(&prop, &properties).unwrap());
2426
2427 let partial_prop = Property {
2429 value: json!("1.2"),
2430 ..prop.clone()
2431 };
2432 properties.insert("version".to_string(), json!("1.2.0"));
2433 assert!(match_property(&partial_prop, &properties).unwrap());
2434 }
2435
2436 #[test]
2437 fn test_semver_neq() {
2438 let prop = Property {
2439 key: "version".to_string(),
2440 value: json!("1.2.3"),
2441 operator: "semver_neq".to_string(),
2442 property_type: None,
2443 };
2444
2445 let mut properties = HashMap::new();
2446
2447 properties.insert("version".to_string(), json!("1.2.3"));
2448 assert!(!match_property(&prop, &properties).unwrap());
2449
2450 properties.insert("version".to_string(), json!("1.2.4"));
2451 assert!(match_property(&prop, &properties).unwrap());
2452
2453 properties.insert("version".to_string(), json!("2.0.0"));
2454 assert!(match_property(&prop, &properties).unwrap());
2455 }
2456
2457 #[test]
2460 fn test_semver_gt() {
2461 let prop = Property {
2462 key: "version".to_string(),
2463 value: json!("1.2.3"),
2464 operator: "semver_gt".to_string(),
2465 property_type: None,
2466 };
2467
2468 let mut properties = HashMap::new();
2469
2470 properties.insert("version".to_string(), json!("1.2.4"));
2472 assert!(match_property(&prop, &properties).unwrap());
2473
2474 properties.insert("version".to_string(), json!("1.3.0"));
2475 assert!(match_property(&prop, &properties).unwrap());
2476
2477 properties.insert("version".to_string(), json!("2.0.0"));
2478 assert!(match_property(&prop, &properties).unwrap());
2479
2480 properties.insert("version".to_string(), json!("1.2.3"));
2482 assert!(!match_property(&prop, &properties).unwrap());
2483
2484 properties.insert("version".to_string(), json!("1.2.2"));
2486 assert!(!match_property(&prop, &properties).unwrap());
2487
2488 properties.insert("version".to_string(), json!("1.1.9"));
2489 assert!(!match_property(&prop, &properties).unwrap());
2490
2491 properties.insert("version".to_string(), json!("0.9.9"));
2492 assert!(!match_property(&prop, &properties).unwrap());
2493 }
2494
2495 #[test]
2496 fn test_semver_gte() {
2497 let prop = Property {
2498 key: "version".to_string(),
2499 value: json!("1.2.3"),
2500 operator: "semver_gte".to_string(),
2501 property_type: None,
2502 };
2503
2504 let mut properties = HashMap::new();
2505
2506 properties.insert("version".to_string(), json!("1.2.4"));
2508 assert!(match_property(&prop, &properties).unwrap());
2509
2510 properties.insert("version".to_string(), json!("2.0.0"));
2511 assert!(match_property(&prop, &properties).unwrap());
2512
2513 properties.insert("version".to_string(), json!("1.2.3"));
2515 assert!(match_property(&prop, &properties).unwrap());
2516
2517 properties.insert("version".to_string(), json!("1.2.2"));
2519 assert!(!match_property(&prop, &properties).unwrap());
2520
2521 properties.insert("version".to_string(), json!("0.9.9"));
2522 assert!(!match_property(&prop, &properties).unwrap());
2523 }
2524
2525 #[test]
2526 fn test_semver_lt() {
2527 let prop = Property {
2528 key: "version".to_string(),
2529 value: json!("1.2.3"),
2530 operator: "semver_lt".to_string(),
2531 property_type: None,
2532 };
2533
2534 let mut properties = HashMap::new();
2535
2536 properties.insert("version".to_string(), json!("1.2.2"));
2538 assert!(match_property(&prop, &properties).unwrap());
2539
2540 properties.insert("version".to_string(), json!("1.1.9"));
2541 assert!(match_property(&prop, &properties).unwrap());
2542
2543 properties.insert("version".to_string(), json!("0.9.9"));
2544 assert!(match_property(&prop, &properties).unwrap());
2545
2546 properties.insert("version".to_string(), json!("1.2.3"));
2548 assert!(!match_property(&prop, &properties).unwrap());
2549
2550 properties.insert("version".to_string(), json!("1.2.4"));
2552 assert!(!match_property(&prop, &properties).unwrap());
2553
2554 properties.insert("version".to_string(), json!("2.0.0"));
2555 assert!(!match_property(&prop, &properties).unwrap());
2556 }
2557
2558 #[test]
2559 fn test_semver_lte() {
2560 let prop = Property {
2561 key: "version".to_string(),
2562 value: json!("1.2.3"),
2563 operator: "semver_lte".to_string(),
2564 property_type: None,
2565 };
2566
2567 let mut properties = HashMap::new();
2568
2569 properties.insert("version".to_string(), json!("1.2.2"));
2571 assert!(match_property(&prop, &properties).unwrap());
2572
2573 properties.insert("version".to_string(), json!("0.9.9"));
2574 assert!(match_property(&prop, &properties).unwrap());
2575
2576 properties.insert("version".to_string(), json!("1.2.3"));
2578 assert!(match_property(&prop, &properties).unwrap());
2579
2580 properties.insert("version".to_string(), json!("1.2.4"));
2582 assert!(!match_property(&prop, &properties).unwrap());
2583
2584 properties.insert("version".to_string(), json!("2.0.0"));
2585 assert!(!match_property(&prop, &properties).unwrap());
2586 }
2587
2588 #[test]
2591 fn test_semver_tilde_basic() {
2592 let prop = Property {
2594 key: "version".to_string(),
2595 value: json!("1.2.3"),
2596 operator: "semver_tilde".to_string(),
2597 property_type: None,
2598 };
2599
2600 let mut properties = HashMap::new();
2601
2602 properties.insert("version".to_string(), json!("1.2.3"));
2604 assert!(match_property(&prop, &properties).unwrap());
2605
2606 properties.insert("version".to_string(), json!("1.2.4"));
2608 assert!(match_property(&prop, &properties).unwrap());
2609
2610 properties.insert("version".to_string(), json!("1.2.99"));
2611 assert!(match_property(&prop, &properties).unwrap());
2612
2613 properties.insert("version".to_string(), json!("1.3.0"));
2615 assert!(!match_property(&prop, &properties).unwrap());
2616
2617 properties.insert("version".to_string(), json!("1.3.1"));
2619 assert!(!match_property(&prop, &properties).unwrap());
2620
2621 properties.insert("version".to_string(), json!("2.0.0"));
2622 assert!(!match_property(&prop, &properties).unwrap());
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
2632 #[test]
2633 fn test_semver_tilde_zero_versions() {
2634 let prop = Property {
2636 key: "version".to_string(),
2637 value: json!("0.2.3"),
2638 operator: "semver_tilde".to_string(),
2639 property_type: None,
2640 };
2641
2642 let mut properties = HashMap::new();
2643
2644 properties.insert("version".to_string(), json!("0.2.3"));
2645 assert!(match_property(&prop, &properties).unwrap());
2646
2647 properties.insert("version".to_string(), json!("0.2.9"));
2648 assert!(match_property(&prop, &properties).unwrap());
2649
2650 properties.insert("version".to_string(), json!("0.3.0"));
2651 assert!(!match_property(&prop, &properties).unwrap());
2652
2653 properties.insert("version".to_string(), json!("0.2.2"));
2654 assert!(!match_property(&prop, &properties).unwrap());
2655 }
2656
2657 #[test]
2660 fn test_semver_caret_major_nonzero() {
2661 let prop = Property {
2663 key: "version".to_string(),
2664 value: json!("1.2.3"),
2665 operator: "semver_caret".to_string(),
2666 property_type: None,
2667 };
2668
2669 let mut properties = HashMap::new();
2670
2671 properties.insert("version".to_string(), json!("1.2.3"));
2673 assert!(match_property(&prop, &properties).unwrap());
2674
2675 properties.insert("version".to_string(), json!("1.2.4"));
2677 assert!(match_property(&prop, &properties).unwrap());
2678
2679 properties.insert("version".to_string(), json!("1.3.0"));
2680 assert!(match_property(&prop, &properties).unwrap());
2681
2682 properties.insert("version".to_string(), json!("1.99.99"));
2683 assert!(match_property(&prop, &properties).unwrap());
2684
2685 properties.insert("version".to_string(), json!("2.0.0"));
2687 assert!(!match_property(&prop, &properties).unwrap());
2688
2689 properties.insert("version".to_string(), json!("2.0.1"));
2691 assert!(!match_property(&prop, &properties).unwrap());
2692
2693 properties.insert("version".to_string(), json!("1.2.2"));
2695 assert!(!match_property(&prop, &properties).unwrap());
2696
2697 properties.insert("version".to_string(), json!("0.9.9"));
2698 assert!(!match_property(&prop, &properties).unwrap());
2699 }
2700
2701 #[test]
2702 fn test_semver_caret_major_zero_minor_nonzero() {
2703 let prop = Property {
2705 key: "version".to_string(),
2706 value: json!("0.2.3"),
2707 operator: "semver_caret".to_string(),
2708 property_type: None,
2709 };
2710
2711 let mut properties = HashMap::new();
2712
2713 properties.insert("version".to_string(), json!("0.2.3"));
2715 assert!(match_property(&prop, &properties).unwrap());
2716
2717 properties.insert("version".to_string(), json!("0.2.4"));
2719 assert!(match_property(&prop, &properties).unwrap());
2720
2721 properties.insert("version".to_string(), json!("0.2.99"));
2722 assert!(match_property(&prop, &properties).unwrap());
2723
2724 properties.insert("version".to_string(), json!("0.3.0"));
2726 assert!(!match_property(&prop, &properties).unwrap());
2727
2728 properties.insert("version".to_string(), json!("0.3.1"));
2730 assert!(!match_property(&prop, &properties).unwrap());
2731
2732 properties.insert("version".to_string(), json!("1.0.0"));
2733 assert!(!match_property(&prop, &properties).unwrap());
2734
2735 properties.insert("version".to_string(), json!("0.2.2"));
2737 assert!(!match_property(&prop, &properties).unwrap());
2738
2739 properties.insert("version".to_string(), json!("0.1.9"));
2740 assert!(!match_property(&prop, &properties).unwrap());
2741 }
2742
2743 #[test]
2744 fn test_semver_caret_major_zero_minor_zero() {
2745 let prop = Property {
2747 key: "version".to_string(),
2748 value: json!("0.0.3"),
2749 operator: "semver_caret".to_string(),
2750 property_type: None,
2751 };
2752
2753 let mut properties = HashMap::new();
2754
2755 properties.insert("version".to_string(), json!("0.0.3"));
2757 assert!(match_property(&prop, &properties).unwrap());
2758
2759 properties.insert("version".to_string(), json!("0.0.4"));
2761 assert!(!match_property(&prop, &properties).unwrap());
2762
2763 properties.insert("version".to_string(), json!("0.0.5"));
2765 assert!(!match_property(&prop, &properties).unwrap());
2766
2767 properties.insert("version".to_string(), json!("0.1.0"));
2768 assert!(!match_property(&prop, &properties).unwrap());
2769
2770 properties.insert("version".to_string(), json!("0.0.2"));
2772 assert!(!match_property(&prop, &properties).unwrap());
2773 }
2774
2775 #[test]
2778 fn test_semver_wildcard_major() {
2779 let prop = Property {
2781 key: "version".to_string(),
2782 value: json!("1.*"),
2783 operator: "semver_wildcard".to_string(),
2784 property_type: None,
2785 };
2786
2787 let mut properties = HashMap::new();
2788
2789 properties.insert("version".to_string(), json!("1.0.0"));
2791 assert!(match_property(&prop, &properties).unwrap());
2792
2793 properties.insert("version".to_string(), json!("1.2.3"));
2795 assert!(match_property(&prop, &properties).unwrap());
2796
2797 properties.insert("version".to_string(), json!("1.99.99"));
2798 assert!(match_property(&prop, &properties).unwrap());
2799
2800 properties.insert("version".to_string(), json!("2.0.0"));
2802 assert!(!match_property(&prop, &properties).unwrap());
2803
2804 properties.insert("version".to_string(), json!("2.0.1"));
2806 assert!(!match_property(&prop, &properties).unwrap());
2807
2808 properties.insert("version".to_string(), json!("0.9.9"));
2810 assert!(!match_property(&prop, &properties).unwrap());
2811 }
2812
2813 #[test]
2814 fn test_semver_wildcard_minor() {
2815 let prop = Property {
2817 key: "version".to_string(),
2818 value: json!("1.2.*"),
2819 operator: "semver_wildcard".to_string(),
2820 property_type: None,
2821 };
2822
2823 let mut properties = HashMap::new();
2824
2825 properties.insert("version".to_string(), json!("1.2.0"));
2827 assert!(match_property(&prop, &properties).unwrap());
2828
2829 properties.insert("version".to_string(), json!("1.2.3"));
2831 assert!(match_property(&prop, &properties).unwrap());
2832
2833 properties.insert("version".to_string(), json!("1.2.99"));
2834 assert!(match_property(&prop, &properties).unwrap());
2835
2836 properties.insert("version".to_string(), json!("1.3.0"));
2838 assert!(!match_property(&prop, &properties).unwrap());
2839
2840 properties.insert("version".to_string(), json!("1.3.1"));
2842 assert!(!match_property(&prop, &properties).unwrap());
2843
2844 properties.insert("version".to_string(), json!("2.0.0"));
2845 assert!(!match_property(&prop, &properties).unwrap());
2846
2847 properties.insert("version".to_string(), json!("1.1.9"));
2849 assert!(!match_property(&prop, &properties).unwrap());
2850 }
2851
2852 #[test]
2853 fn test_semver_wildcard_zero() {
2854 let prop = Property {
2856 key: "version".to_string(),
2857 value: json!("0.*"),
2858 operator: "semver_wildcard".to_string(),
2859 property_type: None,
2860 };
2861
2862 let mut properties = HashMap::new();
2863
2864 properties.insert("version".to_string(), json!("0.0.0"));
2865 assert!(match_property(&prop, &properties).unwrap());
2866
2867 properties.insert("version".to_string(), json!("0.99.99"));
2868 assert!(match_property(&prop, &properties).unwrap());
2869
2870 properties.insert("version".to_string(), json!("1.0.0"));
2871 assert!(!match_property(&prop, &properties).unwrap());
2872 }
2873
2874 #[test]
2877 fn test_semver_invalid_property_value() {
2878 let prop = Property {
2879 key: "version".to_string(),
2880 value: json!("1.2.3"),
2881 operator: "semver_eq".to_string(),
2882 property_type: None,
2883 };
2884
2885 let mut properties = HashMap::new();
2886
2887 properties.insert("version".to_string(), json!("not-a-version"));
2889 assert!(match_property(&prop, &properties).is_err());
2890
2891 properties.insert("version".to_string(), json!(""));
2892 assert!(match_property(&prop, &properties).is_err());
2893
2894 properties.insert("version".to_string(), json!(".1.2.3"));
2895 assert!(match_property(&prop, &properties).is_err());
2896
2897 properties.insert("version".to_string(), json!("abc.def.ghi"));
2898 assert!(match_property(&prop, &properties).is_err());
2899 }
2900
2901 #[test]
2902 fn test_semver_invalid_target_value() {
2903 let mut properties = HashMap::new();
2904 properties.insert("version".to_string(), json!("1.2.3"));
2905
2906 let prop = Property {
2908 key: "version".to_string(),
2909 value: json!("not-valid"),
2910 operator: "semver_eq".to_string(),
2911 property_type: None,
2912 };
2913 assert!(match_property(&prop, &properties).is_err());
2914
2915 let prop = Property {
2916 key: "version".to_string(),
2917 value: json!(""),
2918 operator: "semver_gt".to_string(),
2919 property_type: None,
2920 };
2921 assert!(match_property(&prop, &properties).is_err());
2922 }
2923
2924 #[test]
2925 fn test_semver_invalid_wildcard_pattern() {
2926 let mut properties = HashMap::new();
2927 properties.insert("version".to_string(), json!("1.2.3"));
2928
2929 let invalid_patterns = vec![
2931 "*", "*.2.3", "1.*.3", "1.2.3.*", "abc.*", ];
2937
2938 for pattern in invalid_patterns {
2939 let prop = Property {
2940 key: "version".to_string(),
2941 value: json!(pattern),
2942 operator: "semver_wildcard".to_string(),
2943 property_type: None,
2944 };
2945 assert!(
2946 match_property(&prop, &properties).is_err(),
2947 "Pattern '{}' should be invalid",
2948 pattern
2949 );
2950 }
2951 }
2952
2953 #[test]
2954 fn test_semver_missing_property() {
2955 let prop = Property {
2956 key: "version".to_string(),
2957 value: json!("1.2.3"),
2958 operator: "semver_eq".to_string(),
2959 property_type: None,
2960 };
2961
2962 let properties = HashMap::new(); assert!(match_property(&prop, &properties).is_err());
2964 }
2965
2966 #[test]
2967 fn test_semver_null_property_value() {
2968 let prop = Property {
2969 key: "version".to_string(),
2970 value: json!("1.2.3"),
2971 operator: "semver_eq".to_string(),
2972 property_type: None,
2973 };
2974
2975 let mut properties = HashMap::new();
2976 properties.insert("version".to_string(), json!(null));
2977
2978 assert!(match_property(&prop, &properties).is_err());
2980 }
2981
2982 #[test]
2983 fn test_semver_numeric_property_value() {
2984 let prop = Property {
2986 key: "version".to_string(),
2987 value: json!("1.0.0"),
2988 operator: "semver_eq".to_string(),
2989 property_type: None,
2990 };
2991
2992 let mut properties = HashMap::new();
2993 properties.insert("version".to_string(), json!(1));
2995 assert!(match_property(&prop, &properties).unwrap());
2996 }
2997
2998 #[test]
3001 fn test_semver_four_part_versions() {
3002 let prop = Property {
3003 key: "version".to_string(),
3004 value: json!("1.2.3.4"),
3005 operator: "semver_eq".to_string(),
3006 property_type: None,
3007 };
3008
3009 let mut properties = HashMap::new();
3010
3011 properties.insert("version".to_string(), json!("1.2.3"));
3013 assert!(match_property(&prop, &properties).unwrap());
3014
3015 properties.insert("version".to_string(), json!("1.2.3.4"));
3016 assert!(match_property(&prop, &properties).unwrap());
3017
3018 properties.insert("version".to_string(), json!("1.2.3.999"));
3019 assert!(match_property(&prop, &properties).unwrap());
3020 }
3021
3022 #[test]
3023 fn test_semver_large_version_numbers() {
3024 let prop = Property {
3025 key: "version".to_string(),
3026 value: json!("1000.2000.3000"),
3027 operator: "semver_eq".to_string(),
3028 property_type: None,
3029 };
3030
3031 let mut properties = HashMap::new();
3032 properties.insert("version".to_string(), json!("1000.2000.3000"));
3033 assert!(match_property(&prop, &properties).unwrap());
3034 }
3035
3036 #[test]
3037 fn test_semver_comparison_ordering() {
3038 let cases = vec![
3040 ("0.0.1", "0.0.2", "semver_lt", true),
3041 ("0.1.0", "0.0.99", "semver_gt", true),
3042 ("1.0.0", "0.99.99", "semver_gt", true),
3043 ("1.0.0", "1.0.0", "semver_eq", true),
3044 ("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), ];
3049
3050 for (prop_val, target_val, op, expected) in cases {
3051 let prop = Property {
3052 key: "version".to_string(),
3053 value: json!(target_val),
3054 operator: op.to_string(),
3055 property_type: None,
3056 };
3057
3058 let mut properties = HashMap::new();
3059 properties.insert("version".to_string(), json!(prop_val));
3060
3061 assert_eq!(
3062 match_property(&prop, &properties).unwrap(),
3063 expected,
3064 "{} {} {} should be {}",
3065 prop_val,
3066 op,
3067 target_val,
3068 expected
3069 );
3070 }
3071 }
3072
3073 #[test]
3074 fn test_match_property_semver_rejects_leading_zeros() {
3075 let bad_versions = ["1.07.3", "01.02.03", "1.2.03", "v01.2.3", "001.0.0"];
3080
3081 for bad in bad_versions {
3083 let prop = Property {
3084 key: "version".to_string(),
3085 value: json!("1.2.3"),
3086 operator: "semver_eq".to_string(),
3087 property_type: None,
3088 };
3089 let mut properties = HashMap::new();
3090 properties.insert("version".to_string(), json!(bad));
3091 assert!(
3092 match_property(&prop, &properties).is_err(),
3093 "override '{}' should be rejected",
3094 bad
3095 );
3096 }
3097
3098 for good in ["0.1.0", "1.0.0", "0.0.0"] {
3100 let prop = Property {
3101 key: "version".to_string(),
3102 value: json!(good),
3103 operator: "semver_eq".to_string(),
3104 property_type: None,
3105 };
3106 let mut properties = HashMap::new();
3107 properties.insert("version".to_string(), json!(good));
3108 assert!(
3109 match_property(&prop, &properties).unwrap(),
3110 "'{}' should parse and match itself",
3111 good
3112 );
3113 }
3114
3115 let mut properties = HashMap::new();
3117 properties.insert("version".to_string(), json!("1.2.3"));
3118
3119 for op in ["semver_gt", "semver_caret", "semver_tilde"] {
3120 for bad in bad_versions {
3121 let prop = Property {
3122 key: "version".to_string(),
3123 value: json!(bad),
3124 operator: op.to_string(),
3125 property_type: None,
3126 };
3127 assert!(
3128 match_property(&prop, &properties).is_err(),
3129 "target '{}' for {} should be rejected",
3130 bad,
3131 op
3132 );
3133 }
3134 }
3135
3136 for bad_pattern in ["01.*", "1.07.*", "v01.2.*"] {
3138 let prop = Property {
3139 key: "version".to_string(),
3140 value: json!(bad_pattern),
3141 operator: "semver_wildcard".to_string(),
3142 property_type: None,
3143 };
3144 assert!(
3145 match_property(&prop, &properties).is_err(),
3146 "wildcard target '{}' should be rejected",
3147 bad_pattern
3148 );
3149 }
3150 }
3151}