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> {
905 let value = value.trim();
906 if value.is_empty() {
907 return None;
908 }
909
910 let value = value
912 .strip_prefix('v')
913 .or_else(|| value.strip_prefix('V'))
914 .unwrap_or(value);
915 if value.is_empty() {
916 return None;
917 }
918
919 let value = value.split(['-', '+']).next().unwrap_or(value);
921 if value.is_empty() {
922 return None;
923 }
924
925 if value.starts_with('.') {
927 return None;
928 }
929
930 let parts: Vec<&str> = value.split('.').collect();
932 if parts.is_empty() {
933 return None;
934 }
935
936 let major: u64 = parts.first().and_then(|s| s.parse().ok())?;
937 let minor: u64 = parts.get(1).map(|s| s.parse().ok()).unwrap_or(Some(0))?;
938 let patch: u64 = parts.get(2).map(|s| s.parse().ok()).unwrap_or(Some(0))?;
939
940 Some((major, minor, patch))
941}
942
943fn parse_semver_wildcard(pattern: &str) -> Option<(SemverTuple, SemverTuple)> {
946 let pattern = pattern.trim();
947 if pattern.is_empty() {
948 return None;
949 }
950
951 let pattern = pattern
953 .strip_prefix('v')
954 .or_else(|| pattern.strip_prefix('V'))
955 .unwrap_or(pattern);
956 if pattern.is_empty() {
957 return None;
958 }
959
960 let parts: Vec<&str> = pattern.split('.').collect();
961
962 match parts.as_slice() {
963 [major_str, "*"] => {
965 let major: u64 = major_str.parse().ok()?;
966 Some(((major, 0, 0), (major + 1, 0, 0)))
967 }
968 [major_str, minor_str, "*"] => {
970 let major: u64 = major_str.parse().ok()?;
971 let minor: u64 = minor_str.parse().ok()?;
972 Some(((major, minor, 0), (major, minor + 1, 0)))
973 }
974 _ => None,
975 }
976}
977
978fn compute_tilde_bounds(version: SemverTuple) -> (SemverTuple, SemverTuple) {
980 let (major, minor, patch) = version;
981 ((major, minor, patch), (major, minor + 1, 0))
982}
983
984fn compute_caret_bounds(version: SemverTuple) -> (SemverTuple, SemverTuple) {
989 let (major, minor, patch) = version;
990 if major > 0 {
991 ((major, minor, patch), (major + 1, 0, 0))
992 } else if minor > 0 {
993 ((0, minor, patch), (0, minor + 1, 0))
994 } else {
995 ((0, 0, patch), (0, 0, patch + 1))
996 }
997}
998
999fn match_property(
1000 property: &Property,
1001 properties: &HashMap<String, serde_json::Value>,
1002) -> Result<bool, InconclusiveMatchError> {
1003 let value = match properties.get(&property.key) {
1004 Some(v) => v,
1005 None => {
1006 if property.operator == "is_not_set" {
1008 return Ok(true);
1009 }
1010 if property.operator == "is_set" {
1012 return Ok(false);
1013 }
1014 return Err(InconclusiveMatchError::new(&format!(
1016 "Property '{}' not found in provided properties",
1017 property.key
1018 )));
1019 }
1020 };
1021
1022 Ok(match property.operator.as_str() {
1023 "exact" => {
1024 if property.value.is_array() {
1025 if let Some(arr) = property.value.as_array() {
1026 for val in arr {
1027 if compare_values(val, value) {
1028 return Ok(true);
1029 }
1030 }
1031 return Ok(false);
1032 }
1033 }
1034 compare_values(&property.value, value)
1035 }
1036 "is_not" => {
1037 if property.value.is_array() {
1038 if let Some(arr) = property.value.as_array() {
1039 for val in arr {
1040 if compare_values(val, value) {
1041 return Ok(false);
1042 }
1043 }
1044 return Ok(true);
1045 }
1046 }
1047 !compare_values(&property.value, value)
1048 }
1049 "is_set" => true, "is_not_set" => false, "icontains" => {
1052 let prop_str = value_to_string(value);
1053 let search_str = value_to_string(&property.value);
1054 prop_str.to_lowercase().contains(&search_str.to_lowercase())
1055 }
1056 "not_icontains" => {
1057 let prop_str = value_to_string(value);
1058 let search_str = value_to_string(&property.value);
1059 !prop_str.to_lowercase().contains(&search_str.to_lowercase())
1060 }
1061 "regex" => {
1062 let prop_str = value_to_string(value);
1063 let regex_str = value_to_string(&property.value);
1064 get_cached_regex(®ex_str)
1065 .map(|re| re.is_match(&prop_str))
1066 .unwrap_or(false)
1067 }
1068 "not_regex" => {
1069 let prop_str = value_to_string(value);
1070 let regex_str = value_to_string(&property.value);
1071 get_cached_regex(®ex_str)
1072 .map(|re| !re.is_match(&prop_str))
1073 .unwrap_or(true)
1074 }
1075 "gt" | "gte" | "lt" | "lte" => compare_numeric(&property.operator, &property.value, value),
1076 "is_date_before" | "is_date_after" => {
1077 let target_date = parse_date_value(&property.value).ok_or_else(|| {
1078 InconclusiveMatchError::new(&format!(
1079 "Unable to parse target date value: {:?}",
1080 property.value
1081 ))
1082 })?;
1083
1084 let prop_date = parse_date_value(value).ok_or_else(|| {
1085 InconclusiveMatchError::new(&format!(
1086 "Unable to parse property date value for '{}': {:?}",
1087 property.key, value
1088 ))
1089 })?;
1090
1091 if property.operator == "is_date_before" {
1092 prop_date < target_date
1093 } else {
1094 prop_date > target_date
1095 }
1096 }
1097 "semver_eq" | "semver_neq" | "semver_gt" | "semver_gte" | "semver_lt" | "semver_lte" => {
1099 let prop_str = value_to_string(value);
1100 let target_str = value_to_string(&property.value);
1101
1102 let prop_version = parse_semver(&prop_str).ok_or_else(|| {
1103 InconclusiveMatchError::new(&format!(
1104 "Unable to parse property semver value for '{}': {:?}",
1105 property.key, value
1106 ))
1107 })?;
1108
1109 let target_version = parse_semver(&target_str).ok_or_else(|| {
1110 InconclusiveMatchError::new(&format!(
1111 "Unable to parse target semver value: {:?}",
1112 property.value
1113 ))
1114 })?;
1115
1116 match property.operator.as_str() {
1117 "semver_eq" => prop_version == target_version,
1118 "semver_neq" => prop_version != target_version,
1119 "semver_gt" => prop_version > target_version,
1120 "semver_gte" => prop_version >= target_version,
1121 "semver_lt" => prop_version < target_version,
1122 "semver_lte" => prop_version <= target_version,
1123 _ => unreachable!(),
1124 }
1125 }
1126 "semver_tilde" => {
1127 let prop_str = value_to_string(value);
1128 let target_str = value_to_string(&property.value);
1129
1130 let prop_version = parse_semver(&prop_str).ok_or_else(|| {
1131 InconclusiveMatchError::new(&format!(
1132 "Unable to parse property semver value for '{}': {:?}",
1133 property.key, value
1134 ))
1135 })?;
1136
1137 let target_version = parse_semver(&target_str).ok_or_else(|| {
1138 InconclusiveMatchError::new(&format!(
1139 "Unable to parse target semver value: {:?}",
1140 property.value
1141 ))
1142 })?;
1143
1144 let (lower, upper) = compute_tilde_bounds(target_version);
1145 prop_version >= lower && prop_version < upper
1146 }
1147 "semver_caret" => {
1148 let prop_str = value_to_string(value);
1149 let target_str = value_to_string(&property.value);
1150
1151 let prop_version = parse_semver(&prop_str).ok_or_else(|| {
1152 InconclusiveMatchError::new(&format!(
1153 "Unable to parse property semver value for '{}': {:?}",
1154 property.key, value
1155 ))
1156 })?;
1157
1158 let target_version = parse_semver(&target_str).ok_or_else(|| {
1159 InconclusiveMatchError::new(&format!(
1160 "Unable to parse target semver value: {:?}",
1161 property.value
1162 ))
1163 })?;
1164
1165 let (lower, upper) = compute_caret_bounds(target_version);
1166 prop_version >= lower && prop_version < upper
1167 }
1168 "semver_wildcard" => {
1169 let prop_str = value_to_string(value);
1170 let target_str = value_to_string(&property.value);
1171
1172 let prop_version = parse_semver(&prop_str).ok_or_else(|| {
1173 InconclusiveMatchError::new(&format!(
1174 "Unable to parse property semver value for '{}': {:?}",
1175 property.key, value
1176 ))
1177 })?;
1178
1179 let (lower, upper) = parse_semver_wildcard(&target_str).ok_or_else(|| {
1180 InconclusiveMatchError::new(&format!(
1181 "Unable to parse target semver wildcard pattern: {:?}",
1182 property.value
1183 ))
1184 })?;
1185
1186 prop_version >= lower && prop_version < upper
1187 }
1188 unknown => {
1189 return Err(InconclusiveMatchError::new(&format!(
1190 "Unknown operator: {}",
1191 unknown
1192 )));
1193 }
1194 })
1195}
1196
1197fn compare_values(a: &serde_json::Value, b: &serde_json::Value) -> bool {
1198 if let (Some(a_str), Some(b_str)) = (a.as_str(), b.as_str()) {
1200 return a_str.eq_ignore_ascii_case(b_str);
1201 }
1202
1203 a == b
1205}
1206
1207fn value_to_string(value: &serde_json::Value) -> String {
1208 match value {
1209 serde_json::Value::String(s) => s.clone(),
1210 serde_json::Value::Number(n) => n.to_string(),
1211 serde_json::Value::Bool(b) => b.to_string(),
1212 _ => value.to_string(),
1213 }
1214}
1215
1216fn compare_numeric(
1217 operator: &str,
1218 property_value: &serde_json::Value,
1219 value: &serde_json::Value,
1220) -> bool {
1221 let prop_num = match property_value {
1222 serde_json::Value::Number(n) => n.as_f64(),
1223 serde_json::Value::String(s) => s.parse::<f64>().ok(),
1224 _ => None,
1225 };
1226
1227 let val_num = match value {
1228 serde_json::Value::Number(n) => n.as_f64(),
1229 serde_json::Value::String(s) => s.parse::<f64>().ok(),
1230 _ => None,
1231 };
1232
1233 if let (Some(prop), Some(val)) = (prop_num, val_num) {
1234 match operator {
1235 "gt" => val > prop,
1236 "gte" => val >= prop,
1237 "lt" => val < prop,
1238 "lte" => val <= prop,
1239 _ => false,
1240 }
1241 } else {
1242 let prop_str = value_to_string(property_value);
1244 let val_str = value_to_string(value);
1245 match operator {
1246 "gt" => val_str > prop_str,
1247 "gte" => val_str >= prop_str,
1248 "lt" => val_str < prop_str,
1249 "lte" => val_str <= prop_str,
1250 _ => false,
1251 }
1252 }
1253}
1254
1255#[cfg(test)]
1256mod tests {
1257 use super::*;
1258 use serde_json::json;
1259
1260 const TEST_SALT: &str = "test-salt";
1262
1263 #[test]
1264 fn test_hash_key() {
1265 let hash = hash_key("test-flag", "user-123", TEST_SALT);
1266 assert!((0.0..=1.0).contains(&hash));
1267
1268 let hash2 = hash_key("test-flag", "user-123", TEST_SALT);
1270 assert_eq!(hash, hash2);
1271
1272 let hash3 = hash_key("test-flag", "user-456", TEST_SALT);
1274 assert_ne!(hash, hash3);
1275 }
1276
1277 #[test]
1278 fn test_simple_flag_match() {
1279 let flag = FeatureFlag {
1280 key: "test-flag".to_string(),
1281 active: true,
1282 filters: FeatureFlagFilters {
1283 groups: vec![FeatureFlagCondition {
1284 properties: vec![],
1285 rollout_percentage: Some(100.0),
1286 variant: None,
1287 aggregation_group_type_index: None,
1288 }],
1289 multivariate: None,
1290 payloads: HashMap::new(),
1291 aggregation_group_type_index: None,
1292 },
1293 };
1294
1295 let properties = HashMap::new();
1296 let result = match_feature_flag(
1297 &flag,
1298 "user-123",
1299 &properties,
1300 &HashMap::new(),
1301 &HashMap::new(),
1302 &HashMap::new(),
1303 )
1304 .unwrap();
1305 assert_eq!(result, FlagValue::Boolean(true));
1306 }
1307
1308 #[test]
1309 fn test_property_matching() {
1310 let prop = Property {
1311 key: "country".to_string(),
1312 value: json!("US"),
1313 operator: "exact".to_string(),
1314 property_type: None,
1315 };
1316
1317 let mut properties = HashMap::new();
1318 properties.insert("country".to_string(), json!("US"));
1319
1320 assert!(match_property(&prop, &properties).unwrap());
1321
1322 properties.insert("country".to_string(), json!("UK"));
1323 assert!(!match_property(&prop, &properties).unwrap());
1324 }
1325
1326 #[test]
1327 fn test_multivariate_variants() {
1328 let flag = FeatureFlag {
1329 key: "test-flag".to_string(),
1330 active: true,
1331 filters: FeatureFlagFilters {
1332 groups: vec![FeatureFlagCondition {
1333 properties: vec![],
1334 rollout_percentage: Some(100.0),
1335 variant: None,
1336 aggregation_group_type_index: None,
1337 }],
1338 multivariate: Some(MultivariateFilter {
1339 variants: vec![
1340 MultivariateVariant {
1341 key: "control".to_string(),
1342 rollout_percentage: 50.0,
1343 },
1344 MultivariateVariant {
1345 key: "test".to_string(),
1346 rollout_percentage: 50.0,
1347 },
1348 ],
1349 }),
1350 payloads: HashMap::new(),
1351 aggregation_group_type_index: None,
1352 },
1353 };
1354
1355 let properties = HashMap::new();
1356 let result = match_feature_flag(
1357 &flag,
1358 "user-123",
1359 &properties,
1360 &HashMap::new(),
1361 &HashMap::new(),
1362 &HashMap::new(),
1363 )
1364 .unwrap();
1365
1366 match result {
1367 FlagValue::String(variant) => {
1368 assert!(variant == "control" || variant == "test");
1369 }
1370 _ => panic!("Expected string variant"),
1371 }
1372 }
1373
1374 #[test]
1375 fn test_inactive_flag() {
1376 let flag = FeatureFlag {
1377 key: "inactive-flag".to_string(),
1378 active: false,
1379 filters: FeatureFlagFilters {
1380 groups: vec![FeatureFlagCondition {
1381 properties: vec![],
1382 rollout_percentage: Some(100.0),
1383 variant: None,
1384 aggregation_group_type_index: None,
1385 }],
1386 multivariate: None,
1387 payloads: HashMap::new(),
1388 aggregation_group_type_index: None,
1389 },
1390 };
1391
1392 let properties = HashMap::new();
1393 let result = match_feature_flag(
1394 &flag,
1395 "user-123",
1396 &properties,
1397 &HashMap::new(),
1398 &HashMap::new(),
1399 &HashMap::new(),
1400 )
1401 .unwrap();
1402 assert_eq!(result, FlagValue::Boolean(false));
1403 }
1404
1405 #[test]
1406 fn test_rollout_percentage() {
1407 let flag = FeatureFlag {
1408 key: "rollout-flag".to_string(),
1409 active: true,
1410 filters: FeatureFlagFilters {
1411 groups: vec![FeatureFlagCondition {
1412 properties: vec![],
1413 rollout_percentage: Some(30.0), variant: None,
1415 aggregation_group_type_index: None,
1416 }],
1417 multivariate: None,
1418 payloads: HashMap::new(),
1419 aggregation_group_type_index: None,
1420 },
1421 };
1422
1423 let properties = HashMap::new();
1424
1425 let mut enabled_count = 0;
1427 for i in 0..1000 {
1428 let result = match_feature_flag(
1429 &flag,
1430 &format!("user-{}", i),
1431 &properties,
1432 &HashMap::new(),
1433 &HashMap::new(),
1434 &HashMap::new(),
1435 )
1436 .unwrap();
1437 if result == FlagValue::Boolean(true) {
1438 enabled_count += 1;
1439 }
1440 }
1441
1442 assert!(enabled_count > 250 && enabled_count < 350);
1444 }
1445
1446 #[test]
1447 fn test_regex_operator() {
1448 let prop = Property {
1449 key: "email".to_string(),
1450 value: json!(".*@company\\.com$"),
1451 operator: "regex".to_string(),
1452 property_type: None,
1453 };
1454
1455 let mut properties = HashMap::new();
1456 properties.insert("email".to_string(), json!("user@company.com"));
1457 assert!(match_property(&prop, &properties).unwrap());
1458
1459 properties.insert("email".to_string(), json!("user@example.com"));
1460 assert!(!match_property(&prop, &properties).unwrap());
1461 }
1462
1463 #[test]
1464 fn test_icontains_operator() {
1465 let prop = Property {
1466 key: "name".to_string(),
1467 value: json!("ADMIN"),
1468 operator: "icontains".to_string(),
1469 property_type: None,
1470 };
1471
1472 let mut properties = HashMap::new();
1473 properties.insert("name".to_string(), json!("admin_user"));
1474 assert!(match_property(&prop, &properties).unwrap());
1475
1476 properties.insert("name".to_string(), json!("regular_user"));
1477 assert!(!match_property(&prop, &properties).unwrap());
1478 }
1479
1480 #[test]
1481 fn test_numeric_operators() {
1482 let prop_gt = Property {
1484 key: "age".to_string(),
1485 value: json!(18),
1486 operator: "gt".to_string(),
1487 property_type: None,
1488 };
1489
1490 let mut properties = HashMap::new();
1491 properties.insert("age".to_string(), json!(25));
1492 assert!(match_property(&prop_gt, &properties).unwrap());
1493
1494 properties.insert("age".to_string(), json!(15));
1495 assert!(!match_property(&prop_gt, &properties).unwrap());
1496
1497 let prop_lte = Property {
1499 key: "score".to_string(),
1500 value: json!(100),
1501 operator: "lte".to_string(),
1502 property_type: None,
1503 };
1504
1505 properties.insert("score".to_string(), json!(100));
1506 assert!(match_property(&prop_lte, &properties).unwrap());
1507
1508 properties.insert("score".to_string(), json!(101));
1509 assert!(!match_property(&prop_lte, &properties).unwrap());
1510 }
1511
1512 #[test]
1513 fn test_is_set_operator() {
1514 let prop = Property {
1515 key: "email".to_string(),
1516 value: json!(true),
1517 operator: "is_set".to_string(),
1518 property_type: None,
1519 };
1520
1521 let mut properties = HashMap::new();
1522 properties.insert("email".to_string(), json!("test@example.com"));
1523 assert!(match_property(&prop, &properties).unwrap());
1524
1525 properties.remove("email");
1526 assert!(!match_property(&prop, &properties).unwrap());
1527 }
1528
1529 #[test]
1530 fn test_is_not_set_operator() {
1531 let prop = Property {
1532 key: "phone".to_string(),
1533 value: json!(true),
1534 operator: "is_not_set".to_string(),
1535 property_type: None,
1536 };
1537
1538 let mut properties = HashMap::new();
1539 assert!(match_property(&prop, &properties).unwrap());
1540
1541 properties.insert("phone".to_string(), json!("+1234567890"));
1542 assert!(!match_property(&prop, &properties).unwrap());
1543 }
1544
1545 #[test]
1546 fn test_empty_groups() {
1547 let flag = FeatureFlag {
1548 key: "empty-groups".to_string(),
1549 active: true,
1550 filters: FeatureFlagFilters {
1551 groups: vec![],
1552 multivariate: None,
1553 payloads: HashMap::new(),
1554 aggregation_group_type_index: None,
1555 },
1556 };
1557
1558 let properties = HashMap::new();
1559 let result = match_feature_flag(
1560 &flag,
1561 "user-123",
1562 &properties,
1563 &HashMap::new(),
1564 &HashMap::new(),
1565 &HashMap::new(),
1566 )
1567 .unwrap();
1568 assert_eq!(result, FlagValue::Boolean(false));
1569 }
1570
1571 #[test]
1572 fn test_hash_scale_constant() {
1573 assert_eq!(LONG_SCALE, 0xFFFFFFFFFFFFFFFu64 as f64);
1575 assert_ne!(LONG_SCALE, 0xFFFFFFFFFFFFFFFFu64 as f64);
1576 }
1577
1578 #[test]
1581 fn test_unknown_operator_returns_inconclusive_error() {
1582 let prop = Property {
1583 key: "status".to_string(),
1584 value: json!("active"),
1585 operator: "unknown_operator".to_string(),
1586 property_type: None,
1587 };
1588
1589 let mut properties = HashMap::new();
1590 properties.insert("status".to_string(), json!("active"));
1591
1592 let result = match_property(&prop, &properties);
1593 assert!(result.is_err());
1594 let err = result.unwrap_err();
1595 assert!(err.message.contains("unknown_operator"));
1596 }
1597
1598 #[test]
1599 fn test_is_date_before_with_relative_date() {
1600 let prop = Property {
1601 key: "signup_date".to_string(),
1602 value: json!("-7d"), operator: "is_date_before".to_string(),
1604 property_type: None,
1605 };
1606
1607 let mut properties = HashMap::new();
1608 let ten_days_ago = chrono::Utc::now() - chrono::Duration::days(10);
1610 properties.insert(
1611 "signup_date".to_string(),
1612 json!(ten_days_ago.format("%Y-%m-%d").to_string()),
1613 );
1614 assert!(match_property(&prop, &properties).unwrap());
1615
1616 let three_days_ago = chrono::Utc::now() - chrono::Duration::days(3);
1618 properties.insert(
1619 "signup_date".to_string(),
1620 json!(three_days_ago.format("%Y-%m-%d").to_string()),
1621 );
1622 assert!(!match_property(&prop, &properties).unwrap());
1623 }
1624
1625 #[test]
1626 fn test_is_date_after_with_relative_date() {
1627 let prop = Property {
1628 key: "last_seen".to_string(),
1629 value: json!("-30d"), operator: "is_date_after".to_string(),
1631 property_type: None,
1632 };
1633
1634 let mut properties = HashMap::new();
1635 let ten_days_ago = chrono::Utc::now() - chrono::Duration::days(10);
1637 properties.insert(
1638 "last_seen".to_string(),
1639 json!(ten_days_ago.format("%Y-%m-%d").to_string()),
1640 );
1641 assert!(match_property(&prop, &properties).unwrap());
1642
1643 let sixty_days_ago = chrono::Utc::now() - chrono::Duration::days(60);
1645 properties.insert(
1646 "last_seen".to_string(),
1647 json!(sixty_days_ago.format("%Y-%m-%d").to_string()),
1648 );
1649 assert!(!match_property(&prop, &properties).unwrap());
1650 }
1651
1652 #[test]
1653 fn test_is_date_before_with_iso_date() {
1654 let prop = Property {
1655 key: "expiry_date".to_string(),
1656 value: json!("2024-06-15"),
1657 operator: "is_date_before".to_string(),
1658 property_type: None,
1659 };
1660
1661 let mut properties = HashMap::new();
1662 properties.insert("expiry_date".to_string(), json!("2024-06-10"));
1663 assert!(match_property(&prop, &properties).unwrap());
1664
1665 properties.insert("expiry_date".to_string(), json!("2024-06-20"));
1666 assert!(!match_property(&prop, &properties).unwrap());
1667 }
1668
1669 #[test]
1670 fn test_is_date_after_with_iso_date() {
1671 let prop = Property {
1672 key: "start_date".to_string(),
1673 value: json!("2024-01-01"),
1674 operator: "is_date_after".to_string(),
1675 property_type: None,
1676 };
1677
1678 let mut properties = HashMap::new();
1679 properties.insert("start_date".to_string(), json!("2024-03-15"));
1680 assert!(match_property(&prop, &properties).unwrap());
1681
1682 properties.insert("start_date".to_string(), json!("2023-12-01"));
1683 assert!(!match_property(&prop, &properties).unwrap());
1684 }
1685
1686 #[test]
1687 fn test_is_date_with_relative_hours() {
1688 let prop = Property {
1689 key: "last_active".to_string(),
1690 value: json!("-24h"), operator: "is_date_after".to_string(),
1692 property_type: None,
1693 };
1694
1695 let mut properties = HashMap::new();
1696 let twelve_hours_ago = chrono::Utc::now() - chrono::Duration::hours(12);
1698 properties.insert(
1699 "last_active".to_string(),
1700 json!(twelve_hours_ago.to_rfc3339()),
1701 );
1702 assert!(match_property(&prop, &properties).unwrap());
1703
1704 let forty_eight_hours_ago = chrono::Utc::now() - chrono::Duration::hours(48);
1706 properties.insert(
1707 "last_active".to_string(),
1708 json!(forty_eight_hours_ago.to_rfc3339()),
1709 );
1710 assert!(!match_property(&prop, &properties).unwrap());
1711 }
1712
1713 #[test]
1714 fn test_is_date_with_relative_weeks() {
1715 let prop = Property {
1716 key: "joined".to_string(),
1717 value: json!("-2w"), operator: "is_date_before".to_string(),
1719 property_type: None,
1720 };
1721
1722 let mut properties = HashMap::new();
1723 let three_weeks_ago = chrono::Utc::now() - chrono::Duration::weeks(3);
1725 properties.insert(
1726 "joined".to_string(),
1727 json!(three_weeks_ago.format("%Y-%m-%d").to_string()),
1728 );
1729 assert!(match_property(&prop, &properties).unwrap());
1730
1731 let one_week_ago = chrono::Utc::now() - chrono::Duration::weeks(1);
1733 properties.insert(
1734 "joined".to_string(),
1735 json!(one_week_ago.format("%Y-%m-%d").to_string()),
1736 );
1737 assert!(!match_property(&prop, &properties).unwrap());
1738 }
1739
1740 #[test]
1741 fn test_is_date_with_relative_months() {
1742 let prop = Property {
1743 key: "subscription_date".to_string(),
1744 value: json!("-3m"), operator: "is_date_after".to_string(),
1746 property_type: None,
1747 };
1748
1749 let mut properties = HashMap::new();
1750 let one_month_ago = chrono::Utc::now() - chrono::Duration::days(30);
1752 properties.insert(
1753 "subscription_date".to_string(),
1754 json!(one_month_ago.format("%Y-%m-%d").to_string()),
1755 );
1756 assert!(match_property(&prop, &properties).unwrap());
1757
1758 let six_months_ago = chrono::Utc::now() - chrono::Duration::days(180);
1760 properties.insert(
1761 "subscription_date".to_string(),
1762 json!(six_months_ago.format("%Y-%m-%d").to_string()),
1763 );
1764 assert!(!match_property(&prop, &properties).unwrap());
1765 }
1766
1767 #[test]
1768 fn test_is_date_with_relative_years() {
1769 let prop = Property {
1770 key: "created_at".to_string(),
1771 value: json!("-1y"), operator: "is_date_before".to_string(),
1773 property_type: None,
1774 };
1775
1776 let mut properties = HashMap::new();
1777 let two_years_ago = chrono::Utc::now() - chrono::Duration::days(730);
1779 properties.insert(
1780 "created_at".to_string(),
1781 json!(two_years_ago.format("%Y-%m-%d").to_string()),
1782 );
1783 assert!(match_property(&prop, &properties).unwrap());
1784
1785 let six_months_ago = chrono::Utc::now() - chrono::Duration::days(180);
1787 properties.insert(
1788 "created_at".to_string(),
1789 json!(six_months_ago.format("%Y-%m-%d").to_string()),
1790 );
1791 assert!(!match_property(&prop, &properties).unwrap());
1792 }
1793
1794 #[test]
1795 fn test_is_date_with_invalid_date_format() {
1796 let prop = Property {
1797 key: "date".to_string(),
1798 value: json!("-7d"),
1799 operator: "is_date_before".to_string(),
1800 property_type: None,
1801 };
1802
1803 let mut properties = HashMap::new();
1804 properties.insert("date".to_string(), json!("not-a-date"));
1805
1806 let result = match_property(&prop, &properties);
1808 assert!(result.is_err());
1809 }
1810
1811 #[test]
1812 fn test_is_date_with_iso_datetime() {
1813 let prop = Property {
1814 key: "event_time".to_string(),
1815 value: json!("2024-06-15T10:30:00Z"),
1816 operator: "is_date_before".to_string(),
1817 property_type: None,
1818 };
1819
1820 let mut properties = HashMap::new();
1821 properties.insert("event_time".to_string(), json!("2024-06-15T08:00:00Z"));
1822 assert!(match_property(&prop, &properties).unwrap());
1823
1824 properties.insert("event_time".to_string(), json!("2024-06-15T12:00:00Z"));
1825 assert!(!match_property(&prop, &properties).unwrap());
1826 }
1827
1828 #[test]
1831 fn test_cohort_membership_in() {
1832 let mut cohorts = HashMap::new();
1834 cohorts.insert(
1835 "cohort_1".to_string(),
1836 CohortDefinition::new(
1837 "cohort_1".to_string(),
1838 vec![Property {
1839 key: "country".to_string(),
1840 value: json!("US"),
1841 operator: "exact".to_string(),
1842 property_type: None,
1843 }],
1844 ),
1845 );
1846
1847 let prop = Property {
1849 key: "$cohort".to_string(),
1850 value: json!("cohort_1"),
1851 operator: "in".to_string(),
1852 property_type: Some("cohort".to_string()),
1853 };
1854
1855 let mut properties = HashMap::new();
1857 properties.insert("country".to_string(), json!("US"));
1858
1859 let ctx = EvaluationContext {
1860 cohorts: &cohorts,
1861 flags: &HashMap::new(),
1862 distinct_id: "user-123",
1863 groups: &HashMap::new(),
1864 group_properties: &HashMap::new(),
1865 group_type_mapping: &HashMap::new(),
1866 };
1867 assert!(match_property_with_context(&prop, &properties, &ctx).unwrap());
1868
1869 properties.insert("country".to_string(), json!("UK"));
1871 assert!(!match_property_with_context(&prop, &properties, &ctx).unwrap());
1872 }
1873
1874 #[test]
1875 fn test_cohort_membership_not_in() {
1876 let mut cohorts = HashMap::new();
1877 cohorts.insert(
1878 "cohort_blocked".to_string(),
1879 CohortDefinition::new(
1880 "cohort_blocked".to_string(),
1881 vec![Property {
1882 key: "status".to_string(),
1883 value: json!("blocked"),
1884 operator: "exact".to_string(),
1885 property_type: None,
1886 }],
1887 ),
1888 );
1889
1890 let prop = Property {
1891 key: "$cohort".to_string(),
1892 value: json!("cohort_blocked"),
1893 operator: "not_in".to_string(),
1894 property_type: Some("cohort".to_string()),
1895 };
1896
1897 let mut properties = HashMap::new();
1898 properties.insert("status".to_string(), json!("active"));
1899
1900 let ctx = EvaluationContext {
1901 cohorts: &cohorts,
1902 flags: &HashMap::new(),
1903 distinct_id: "user-123",
1904 groups: &HashMap::new(),
1905 group_properties: &HashMap::new(),
1906 group_type_mapping: &HashMap::new(),
1907 };
1908 assert!(match_property_with_context(&prop, &properties, &ctx).unwrap());
1910
1911 properties.insert("status".to_string(), json!("blocked"));
1913 assert!(!match_property_with_context(&prop, &properties, &ctx).unwrap());
1914 }
1915
1916 #[test]
1917 fn test_cohort_not_found_returns_inconclusive() {
1918 let cohorts = HashMap::new(); let prop = Property {
1921 key: "$cohort".to_string(),
1922 value: json!("nonexistent_cohort"),
1923 operator: "in".to_string(),
1924 property_type: Some("cohort".to_string()),
1925 };
1926
1927 let properties = HashMap::new();
1928 let ctx = EvaluationContext {
1929 cohorts: &cohorts,
1930 flags: &HashMap::new(),
1931 distinct_id: "user-123",
1932 groups: &HashMap::new(),
1933 group_properties: &HashMap::new(),
1934 group_type_mapping: &HashMap::new(),
1935 };
1936
1937 let result = match_property_with_context(&prop, &properties, &ctx);
1938 assert!(result.is_err());
1939 assert!(result.unwrap_err().message.contains("Cohort"));
1940 }
1941
1942 #[test]
1945 fn test_flag_dependency_enabled() {
1946 let mut flags = HashMap::new();
1947 flags.insert(
1948 "prerequisite-flag".to_string(),
1949 FeatureFlag {
1950 key: "prerequisite-flag".to_string(),
1951 active: true,
1952 filters: FeatureFlagFilters {
1953 groups: vec![FeatureFlagCondition {
1954 properties: vec![],
1955 rollout_percentage: Some(100.0),
1956 variant: None,
1957 aggregation_group_type_index: None,
1958 }],
1959 multivariate: None,
1960 payloads: HashMap::new(),
1961 aggregation_group_type_index: None,
1962 },
1963 },
1964 );
1965
1966 let prop = Property {
1968 key: "$feature/prerequisite-flag".to_string(),
1969 value: json!(true),
1970 operator: "exact".to_string(),
1971 property_type: None,
1972 };
1973
1974 let properties = HashMap::new();
1975 let ctx = EvaluationContext {
1976 cohorts: &HashMap::new(),
1977 flags: &flags,
1978 distinct_id: "user-123",
1979 groups: &HashMap::new(),
1980 group_properties: &HashMap::new(),
1981 group_type_mapping: &HashMap::new(),
1982 };
1983
1984 assert!(match_property_with_context(&prop, &properties, &ctx).unwrap());
1986 }
1987
1988 #[test]
1989 fn test_flag_dependency_disabled() {
1990 let mut flags = HashMap::new();
1991 flags.insert(
1992 "disabled-flag".to_string(),
1993 FeatureFlag {
1994 key: "disabled-flag".to_string(),
1995 active: false, filters: FeatureFlagFilters {
1997 groups: vec![],
1998 multivariate: None,
1999 payloads: HashMap::new(),
2000 aggregation_group_type_index: None,
2001 },
2002 },
2003 );
2004
2005 let prop = Property {
2007 key: "$feature/disabled-flag".to_string(),
2008 value: json!(true),
2009 operator: "exact".to_string(),
2010 property_type: None,
2011 };
2012
2013 let properties = HashMap::new();
2014 let ctx = EvaluationContext {
2015 cohorts: &HashMap::new(),
2016 flags: &flags,
2017 distinct_id: "user-123",
2018 groups: &HashMap::new(),
2019 group_properties: &HashMap::new(),
2020 group_type_mapping: &HashMap::new(),
2021 };
2022
2023 assert!(!match_property_with_context(&prop, &properties, &ctx).unwrap());
2025 }
2026
2027 #[test]
2028 fn test_flag_dependency_variant_match() {
2029 let mut flags = HashMap::new();
2030 flags.insert(
2031 "ab-test-flag".to_string(),
2032 FeatureFlag {
2033 key: "ab-test-flag".to_string(),
2034 active: true,
2035 filters: FeatureFlagFilters {
2036 groups: vec![FeatureFlagCondition {
2037 properties: vec![],
2038 rollout_percentage: Some(100.0),
2039 variant: None,
2040 aggregation_group_type_index: None,
2041 }],
2042 multivariate: Some(MultivariateFilter {
2043 variants: vec![
2044 MultivariateVariant {
2045 key: "control".to_string(),
2046 rollout_percentage: 50.0,
2047 },
2048 MultivariateVariant {
2049 key: "test".to_string(),
2050 rollout_percentage: 50.0,
2051 },
2052 ],
2053 }),
2054 payloads: HashMap::new(),
2055 aggregation_group_type_index: None,
2056 },
2057 },
2058 );
2059
2060 let prop = Property {
2062 key: "$feature/ab-test-flag".to_string(),
2063 value: json!("control"),
2064 operator: "exact".to_string(),
2065 property_type: None,
2066 };
2067
2068 let properties = HashMap::new();
2069 let ctx = EvaluationContext {
2070 cohorts: &HashMap::new(),
2071 flags: &flags,
2072 distinct_id: "user-gets-control", groups: &HashMap::new(),
2074 group_properties: &HashMap::new(),
2075 group_type_mapping: &HashMap::new(),
2076 };
2077
2078 let result = match_property_with_context(&prop, &properties, &ctx);
2080 assert!(result.is_ok());
2081 }
2082
2083 #[test]
2084 fn test_flag_dependency_not_found_returns_inconclusive() {
2085 let flags = HashMap::new(); let prop = Property {
2088 key: "$feature/nonexistent-flag".to_string(),
2089 value: json!(true),
2090 operator: "exact".to_string(),
2091 property_type: None,
2092 };
2093
2094 let properties = HashMap::new();
2095 let ctx = EvaluationContext {
2096 cohorts: &HashMap::new(),
2097 flags: &flags,
2098 distinct_id: "user-123",
2099 groups: &HashMap::new(),
2100 group_properties: &HashMap::new(),
2101 group_type_mapping: &HashMap::new(),
2102 };
2103
2104 let result = match_property_with_context(&prop, &properties, &ctx);
2105 assert!(result.is_err());
2106 assert!(result.unwrap_err().message.contains("Flag"));
2107 }
2108
2109 #[test]
2112 fn test_parse_relative_date_edge_cases() {
2113 let prop = Property {
2115 key: "date".to_string(),
2116 value: json!("placeholder"),
2117 operator: "is_date_before".to_string(),
2118 property_type: None,
2119 };
2120
2121 let mut properties = HashMap::new();
2122 properties.insert("date".to_string(), json!("2024-01-01"));
2123
2124 let empty_prop = Property {
2126 value: json!(""),
2127 ..prop.clone()
2128 };
2129 assert!(match_property(&empty_prop, &properties).is_err());
2130
2131 let dash_prop = Property {
2133 value: json!("-"),
2134 ..prop.clone()
2135 };
2136 assert!(match_property(&dash_prop, &properties).is_err());
2137
2138 let no_unit_prop = Property {
2140 value: json!("-7"),
2141 ..prop.clone()
2142 };
2143 assert!(match_property(&no_unit_prop, &properties).is_err());
2144
2145 let no_number_prop = Property {
2147 value: json!("-d"),
2148 ..prop.clone()
2149 };
2150 assert!(match_property(&no_number_prop, &properties).is_err());
2151
2152 let invalid_unit_prop = Property {
2154 value: json!("-7x"),
2155 ..prop.clone()
2156 };
2157 assert!(match_property(&invalid_unit_prop, &properties).is_err());
2158 }
2159
2160 #[test]
2161 fn test_parse_relative_date_large_values() {
2162 let prop = Property {
2164 key: "created_at".to_string(),
2165 value: json!("-1000d"), operator: "is_date_before".to_string(),
2167 property_type: None,
2168 };
2169
2170 let mut properties = HashMap::new();
2171 let five_years_ago = chrono::Utc::now() - chrono::Duration::days(1825);
2173 properties.insert(
2174 "created_at".to_string(),
2175 json!(five_years_ago.format("%Y-%m-%d").to_string()),
2176 );
2177 assert!(match_property(&prop, &properties).unwrap());
2178 }
2179
2180 #[test]
2183 fn test_regex_with_invalid_pattern_returns_false() {
2184 let prop = Property {
2186 key: "email".to_string(),
2187 value: json!("(unclosed"),
2188 operator: "regex".to_string(),
2189 property_type: None,
2190 };
2191
2192 let mut properties = HashMap::new();
2193 properties.insert("email".to_string(), json!("test@example.com"));
2194
2195 assert!(!match_property(&prop, &properties).unwrap());
2197 }
2198
2199 #[test]
2200 fn test_not_regex_with_invalid_pattern_returns_true() {
2201 let prop = Property {
2203 key: "email".to_string(),
2204 value: json!("(unclosed"),
2205 operator: "not_regex".to_string(),
2206 property_type: None,
2207 };
2208
2209 let mut properties = HashMap::new();
2210 properties.insert("email".to_string(), json!("test@example.com"));
2211
2212 assert!(match_property(&prop, &properties).unwrap());
2214 }
2215
2216 #[test]
2217 fn test_regex_with_various_invalid_patterns() {
2218 let invalid_patterns = vec![
2219 "(unclosed", "[unclosed", "*invalid", "(?P<bad", r"\", ];
2225
2226 for pattern in invalid_patterns {
2227 let prop = Property {
2228 key: "value".to_string(),
2229 value: json!(pattern),
2230 operator: "regex".to_string(),
2231 property_type: None,
2232 };
2233
2234 let mut properties = HashMap::new();
2235 properties.insert("value".to_string(), json!("test"));
2236
2237 assert!(
2239 !match_property(&prop, &properties).unwrap(),
2240 "Invalid pattern '{}' should return false for regex",
2241 pattern
2242 );
2243
2244 let not_regex_prop = Property {
2246 operator: "not_regex".to_string(),
2247 ..prop
2248 };
2249 assert!(
2250 match_property(¬_regex_prop, &properties).unwrap(),
2251 "Invalid pattern '{}' should return true for not_regex",
2252 pattern
2253 );
2254 }
2255 }
2256
2257 #[test]
2260 fn test_parse_semver_basic() {
2261 assert_eq!(parse_semver("1.2.3"), Some((1, 2, 3)));
2262 assert_eq!(parse_semver("0.0.0"), Some((0, 0, 0)));
2263 assert_eq!(parse_semver("10.20.30"), Some((10, 20, 30)));
2264 }
2265
2266 #[test]
2267 fn test_parse_semver_v_prefix() {
2268 assert_eq!(parse_semver("v1.2.3"), Some((1, 2, 3)));
2269 assert_eq!(parse_semver("V1.2.3"), Some((1, 2, 3)));
2270 }
2271
2272 #[test]
2273 fn test_parse_semver_whitespace() {
2274 assert_eq!(parse_semver(" 1.2.3 "), Some((1, 2, 3)));
2275 assert_eq!(parse_semver(" v1.2.3 "), Some((1, 2, 3)));
2276 }
2277
2278 #[test]
2279 fn test_parse_semver_prerelease_stripped() {
2280 assert_eq!(parse_semver("1.2.3-alpha"), Some((1, 2, 3)));
2281 assert_eq!(parse_semver("1.2.3-beta.1"), Some((1, 2, 3)));
2282 assert_eq!(parse_semver("1.2.3-rc.1+build.123"), Some((1, 2, 3)));
2283 assert_eq!(parse_semver("1.2.3+build.456"), Some((1, 2, 3)));
2284 }
2285
2286 #[test]
2287 fn test_parse_semver_partial_versions() {
2288 assert_eq!(parse_semver("1.2"), Some((1, 2, 0)));
2289 assert_eq!(parse_semver("1"), Some((1, 0, 0)));
2290 assert_eq!(parse_semver("v1.2"), Some((1, 2, 0)));
2291 }
2292
2293 #[test]
2294 fn test_parse_semver_extra_components_ignored() {
2295 assert_eq!(parse_semver("1.2.3.4"), Some((1, 2, 3)));
2296 assert_eq!(parse_semver("1.2.3.4.5.6"), Some((1, 2, 3)));
2297 }
2298
2299 #[test]
2300 fn test_parse_semver_leading_zeros() {
2301 assert_eq!(parse_semver("01.02.03"), Some((1, 2, 3)));
2302 assert_eq!(parse_semver("001.002.003"), Some((1, 2, 3)));
2303 }
2304
2305 #[test]
2306 fn test_parse_semver_invalid() {
2307 assert_eq!(parse_semver(""), None);
2308 assert_eq!(parse_semver(" "), None);
2309 assert_eq!(parse_semver("v"), None);
2310 assert_eq!(parse_semver(".1.2.3"), None);
2311 assert_eq!(parse_semver("abc"), None);
2312 assert_eq!(parse_semver("1.abc.3"), None);
2313 assert_eq!(parse_semver("1.2.abc"), None);
2314 assert_eq!(parse_semver("not-a-version"), None);
2315 }
2316
2317 #[test]
2320 fn test_semver_eq_basic() {
2321 let prop = Property {
2322 key: "version".to_string(),
2323 value: json!("1.2.3"),
2324 operator: "semver_eq".to_string(),
2325 property_type: None,
2326 };
2327
2328 let mut properties = HashMap::new();
2329
2330 properties.insert("version".to_string(), json!("1.2.3"));
2331 assert!(match_property(&prop, &properties).unwrap());
2332
2333 properties.insert("version".to_string(), json!("1.2.4"));
2334 assert!(!match_property(&prop, &properties).unwrap());
2335
2336 properties.insert("version".to_string(), json!("1.3.3"));
2337 assert!(!match_property(&prop, &properties).unwrap());
2338
2339 properties.insert("version".to_string(), json!("2.2.3"));
2340 assert!(!match_property(&prop, &properties).unwrap());
2341 }
2342
2343 #[test]
2344 fn test_semver_eq_with_v_prefix() {
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!("v1.2.3"));
2356 assert!(match_property(&prop, &properties).unwrap());
2357
2358 let prop_with_v = Property {
2360 value: json!("v1.2.3"),
2361 ..prop.clone()
2362 };
2363 properties.insert("version".to_string(), json!("1.2.3"));
2364 assert!(match_property(&prop_with_v, &properties).unwrap());
2365 }
2366
2367 #[test]
2368 fn test_semver_eq_prerelease_stripped() {
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!("1.2.3-alpha"));
2379 assert!(match_property(&prop, &properties).unwrap());
2380
2381 properties.insert("version".to_string(), json!("1.2.3-beta.1"));
2382 assert!(match_property(&prop, &properties).unwrap());
2383
2384 properties.insert("version".to_string(), json!("1.2.3+build.456"));
2385 assert!(match_property(&prop, &properties).unwrap());
2386 }
2387
2388 #[test]
2389 fn test_semver_eq_partial_versions() {
2390 let prop = Property {
2391 key: "version".to_string(),
2392 value: json!("1.2.0"),
2393 operator: "semver_eq".to_string(),
2394 property_type: None,
2395 };
2396
2397 let mut properties = HashMap::new();
2398
2399 properties.insert("version".to_string(), json!("1.2"));
2401 assert!(match_property(&prop, &properties).unwrap());
2402
2403 let partial_prop = Property {
2405 value: json!("1.2"),
2406 ..prop.clone()
2407 };
2408 properties.insert("version".to_string(), json!("1.2.0"));
2409 assert!(match_property(&partial_prop, &properties).unwrap());
2410 }
2411
2412 #[test]
2413 fn test_semver_neq() {
2414 let prop = Property {
2415 key: "version".to_string(),
2416 value: json!("1.2.3"),
2417 operator: "semver_neq".to_string(),
2418 property_type: None,
2419 };
2420
2421 let mut properties = HashMap::new();
2422
2423 properties.insert("version".to_string(), json!("1.2.3"));
2424 assert!(!match_property(&prop, &properties).unwrap());
2425
2426 properties.insert("version".to_string(), json!("1.2.4"));
2427 assert!(match_property(&prop, &properties).unwrap());
2428
2429 properties.insert("version".to_string(), json!("2.0.0"));
2430 assert!(match_property(&prop, &properties).unwrap());
2431 }
2432
2433 #[test]
2436 fn test_semver_gt() {
2437 let prop = Property {
2438 key: "version".to_string(),
2439 value: json!("1.2.3"),
2440 operator: "semver_gt".to_string(),
2441 property_type: None,
2442 };
2443
2444 let mut properties = HashMap::new();
2445
2446 properties.insert("version".to_string(), json!("1.2.4"));
2448 assert!(match_property(&prop, &properties).unwrap());
2449
2450 properties.insert("version".to_string(), json!("1.3.0"));
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 properties.insert("version".to_string(), json!("1.2.3"));
2458 assert!(!match_property(&prop, &properties).unwrap());
2459
2460 properties.insert("version".to_string(), json!("1.2.2"));
2462 assert!(!match_property(&prop, &properties).unwrap());
2463
2464 properties.insert("version".to_string(), json!("1.1.9"));
2465 assert!(!match_property(&prop, &properties).unwrap());
2466
2467 properties.insert("version".to_string(), json!("0.9.9"));
2468 assert!(!match_property(&prop, &properties).unwrap());
2469 }
2470
2471 #[test]
2472 fn test_semver_gte() {
2473 let prop = Property {
2474 key: "version".to_string(),
2475 value: json!("1.2.3"),
2476 operator: "semver_gte".to_string(),
2477 property_type: None,
2478 };
2479
2480 let mut properties = HashMap::new();
2481
2482 properties.insert("version".to_string(), json!("1.2.4"));
2484 assert!(match_property(&prop, &properties).unwrap());
2485
2486 properties.insert("version".to_string(), json!("2.0.0"));
2487 assert!(match_property(&prop, &properties).unwrap());
2488
2489 properties.insert("version".to_string(), json!("1.2.3"));
2491 assert!(match_property(&prop, &properties).unwrap());
2492
2493 properties.insert("version".to_string(), json!("1.2.2"));
2495 assert!(!match_property(&prop, &properties).unwrap());
2496
2497 properties.insert("version".to_string(), json!("0.9.9"));
2498 assert!(!match_property(&prop, &properties).unwrap());
2499 }
2500
2501 #[test]
2502 fn test_semver_lt() {
2503 let prop = Property {
2504 key: "version".to_string(),
2505 value: json!("1.2.3"),
2506 operator: "semver_lt".to_string(),
2507 property_type: None,
2508 };
2509
2510 let mut properties = HashMap::new();
2511
2512 properties.insert("version".to_string(), json!("1.2.2"));
2514 assert!(match_property(&prop, &properties).unwrap());
2515
2516 properties.insert("version".to_string(), json!("1.1.9"));
2517 assert!(match_property(&prop, &properties).unwrap());
2518
2519 properties.insert("version".to_string(), json!("0.9.9"));
2520 assert!(match_property(&prop, &properties).unwrap());
2521
2522 properties.insert("version".to_string(), json!("1.2.3"));
2524 assert!(!match_property(&prop, &properties).unwrap());
2525
2526 properties.insert("version".to_string(), json!("1.2.4"));
2528 assert!(!match_property(&prop, &properties).unwrap());
2529
2530 properties.insert("version".to_string(), json!("2.0.0"));
2531 assert!(!match_property(&prop, &properties).unwrap());
2532 }
2533
2534 #[test]
2535 fn test_semver_lte() {
2536 let prop = Property {
2537 key: "version".to_string(),
2538 value: json!("1.2.3"),
2539 operator: "semver_lte".to_string(),
2540 property_type: None,
2541 };
2542
2543 let mut properties = HashMap::new();
2544
2545 properties.insert("version".to_string(), json!("1.2.2"));
2547 assert!(match_property(&prop, &properties).unwrap());
2548
2549 properties.insert("version".to_string(), json!("0.9.9"));
2550 assert!(match_property(&prop, &properties).unwrap());
2551
2552 properties.insert("version".to_string(), json!("1.2.3"));
2554 assert!(match_property(&prop, &properties).unwrap());
2555
2556 properties.insert("version".to_string(), json!("1.2.4"));
2558 assert!(!match_property(&prop, &properties).unwrap());
2559
2560 properties.insert("version".to_string(), json!("2.0.0"));
2561 assert!(!match_property(&prop, &properties).unwrap());
2562 }
2563
2564 #[test]
2567 fn test_semver_tilde_basic() {
2568 let prop = Property {
2570 key: "version".to_string(),
2571 value: json!("1.2.3"),
2572 operator: "semver_tilde".to_string(),
2573 property_type: None,
2574 };
2575
2576 let mut properties = HashMap::new();
2577
2578 properties.insert("version".to_string(), json!("1.2.3"));
2580 assert!(match_property(&prop, &properties).unwrap());
2581
2582 properties.insert("version".to_string(), json!("1.2.4"));
2584 assert!(match_property(&prop, &properties).unwrap());
2585
2586 properties.insert("version".to_string(), json!("1.2.99"));
2587 assert!(match_property(&prop, &properties).unwrap());
2588
2589 properties.insert("version".to_string(), json!("1.3.0"));
2591 assert!(!match_property(&prop, &properties).unwrap());
2592
2593 properties.insert("version".to_string(), json!("1.3.1"));
2595 assert!(!match_property(&prop, &properties).unwrap());
2596
2597 properties.insert("version".to_string(), json!("2.0.0"));
2598 assert!(!match_property(&prop, &properties).unwrap());
2599
2600 properties.insert("version".to_string(), json!("1.2.2"));
2602 assert!(!match_property(&prop, &properties).unwrap());
2603
2604 properties.insert("version".to_string(), json!("1.1.9"));
2605 assert!(!match_property(&prop, &properties).unwrap());
2606 }
2607
2608 #[test]
2609 fn test_semver_tilde_zero_versions() {
2610 let prop = Property {
2612 key: "version".to_string(),
2613 value: json!("0.2.3"),
2614 operator: "semver_tilde".to_string(),
2615 property_type: None,
2616 };
2617
2618 let mut properties = HashMap::new();
2619
2620 properties.insert("version".to_string(), json!("0.2.3"));
2621 assert!(match_property(&prop, &properties).unwrap());
2622
2623 properties.insert("version".to_string(), json!("0.2.9"));
2624 assert!(match_property(&prop, &properties).unwrap());
2625
2626 properties.insert("version".to_string(), json!("0.3.0"));
2627 assert!(!match_property(&prop, &properties).unwrap());
2628
2629 properties.insert("version".to_string(), json!("0.2.2"));
2630 assert!(!match_property(&prop, &properties).unwrap());
2631 }
2632
2633 #[test]
2636 fn test_semver_caret_major_nonzero() {
2637 let prop = Property {
2639 key: "version".to_string(),
2640 value: json!("1.2.3"),
2641 operator: "semver_caret".to_string(),
2642 property_type: None,
2643 };
2644
2645 let mut properties = HashMap::new();
2646
2647 properties.insert("version".to_string(), json!("1.2.3"));
2649 assert!(match_property(&prop, &properties).unwrap());
2650
2651 properties.insert("version".to_string(), json!("1.2.4"));
2653 assert!(match_property(&prop, &properties).unwrap());
2654
2655 properties.insert("version".to_string(), json!("1.3.0"));
2656 assert!(match_property(&prop, &properties).unwrap());
2657
2658 properties.insert("version".to_string(), json!("1.99.99"));
2659 assert!(match_property(&prop, &properties).unwrap());
2660
2661 properties.insert("version".to_string(), json!("2.0.0"));
2663 assert!(!match_property(&prop, &properties).unwrap());
2664
2665 properties.insert("version".to_string(), json!("2.0.1"));
2667 assert!(!match_property(&prop, &properties).unwrap());
2668
2669 properties.insert("version".to_string(), json!("1.2.2"));
2671 assert!(!match_property(&prop, &properties).unwrap());
2672
2673 properties.insert("version".to_string(), json!("0.9.9"));
2674 assert!(!match_property(&prop, &properties).unwrap());
2675 }
2676
2677 #[test]
2678 fn test_semver_caret_major_zero_minor_nonzero() {
2679 let prop = Property {
2681 key: "version".to_string(),
2682 value: json!("0.2.3"),
2683 operator: "semver_caret".to_string(),
2684 property_type: None,
2685 };
2686
2687 let mut properties = HashMap::new();
2688
2689 properties.insert("version".to_string(), json!("0.2.3"));
2691 assert!(match_property(&prop, &properties).unwrap());
2692
2693 properties.insert("version".to_string(), json!("0.2.4"));
2695 assert!(match_property(&prop, &properties).unwrap());
2696
2697 properties.insert("version".to_string(), json!("0.2.99"));
2698 assert!(match_property(&prop, &properties).unwrap());
2699
2700 properties.insert("version".to_string(), json!("0.3.0"));
2702 assert!(!match_property(&prop, &properties).unwrap());
2703
2704 properties.insert("version".to_string(), json!("0.3.1"));
2706 assert!(!match_property(&prop, &properties).unwrap());
2707
2708 properties.insert("version".to_string(), json!("1.0.0"));
2709 assert!(!match_property(&prop, &properties).unwrap());
2710
2711 properties.insert("version".to_string(), json!("0.2.2"));
2713 assert!(!match_property(&prop, &properties).unwrap());
2714
2715 properties.insert("version".to_string(), json!("0.1.9"));
2716 assert!(!match_property(&prop, &properties).unwrap());
2717 }
2718
2719 #[test]
2720 fn test_semver_caret_major_zero_minor_zero() {
2721 let prop = Property {
2723 key: "version".to_string(),
2724 value: json!("0.0.3"),
2725 operator: "semver_caret".to_string(),
2726 property_type: None,
2727 };
2728
2729 let mut properties = HashMap::new();
2730
2731 properties.insert("version".to_string(), json!("0.0.3"));
2733 assert!(match_property(&prop, &properties).unwrap());
2734
2735 properties.insert("version".to_string(), json!("0.0.4"));
2737 assert!(!match_property(&prop, &properties).unwrap());
2738
2739 properties.insert("version".to_string(), json!("0.0.5"));
2741 assert!(!match_property(&prop, &properties).unwrap());
2742
2743 properties.insert("version".to_string(), json!("0.1.0"));
2744 assert!(!match_property(&prop, &properties).unwrap());
2745
2746 properties.insert("version".to_string(), json!("0.0.2"));
2748 assert!(!match_property(&prop, &properties).unwrap());
2749 }
2750
2751 #[test]
2754 fn test_semver_wildcard_major() {
2755 let prop = Property {
2757 key: "version".to_string(),
2758 value: json!("1.*"),
2759 operator: "semver_wildcard".to_string(),
2760 property_type: None,
2761 };
2762
2763 let mut properties = HashMap::new();
2764
2765 properties.insert("version".to_string(), json!("1.0.0"));
2767 assert!(match_property(&prop, &properties).unwrap());
2768
2769 properties.insert("version".to_string(), json!("1.2.3"));
2771 assert!(match_property(&prop, &properties).unwrap());
2772
2773 properties.insert("version".to_string(), json!("1.99.99"));
2774 assert!(match_property(&prop, &properties).unwrap());
2775
2776 properties.insert("version".to_string(), json!("2.0.0"));
2778 assert!(!match_property(&prop, &properties).unwrap());
2779
2780 properties.insert("version".to_string(), json!("2.0.1"));
2782 assert!(!match_property(&prop, &properties).unwrap());
2783
2784 properties.insert("version".to_string(), json!("0.9.9"));
2786 assert!(!match_property(&prop, &properties).unwrap());
2787 }
2788
2789 #[test]
2790 fn test_semver_wildcard_minor() {
2791 let prop = Property {
2793 key: "version".to_string(),
2794 value: json!("1.2.*"),
2795 operator: "semver_wildcard".to_string(),
2796 property_type: None,
2797 };
2798
2799 let mut properties = HashMap::new();
2800
2801 properties.insert("version".to_string(), json!("1.2.0"));
2803 assert!(match_property(&prop, &properties).unwrap());
2804
2805 properties.insert("version".to_string(), json!("1.2.3"));
2807 assert!(match_property(&prop, &properties).unwrap());
2808
2809 properties.insert("version".to_string(), json!("1.2.99"));
2810 assert!(match_property(&prop, &properties).unwrap());
2811
2812 properties.insert("version".to_string(), json!("1.3.0"));
2814 assert!(!match_property(&prop, &properties).unwrap());
2815
2816 properties.insert("version".to_string(), json!("1.3.1"));
2818 assert!(!match_property(&prop, &properties).unwrap());
2819
2820 properties.insert("version".to_string(), json!("2.0.0"));
2821 assert!(!match_property(&prop, &properties).unwrap());
2822
2823 properties.insert("version".to_string(), json!("1.1.9"));
2825 assert!(!match_property(&prop, &properties).unwrap());
2826 }
2827
2828 #[test]
2829 fn test_semver_wildcard_zero() {
2830 let prop = Property {
2832 key: "version".to_string(),
2833 value: json!("0.*"),
2834 operator: "semver_wildcard".to_string(),
2835 property_type: None,
2836 };
2837
2838 let mut properties = HashMap::new();
2839
2840 properties.insert("version".to_string(), json!("0.0.0"));
2841 assert!(match_property(&prop, &properties).unwrap());
2842
2843 properties.insert("version".to_string(), json!("0.99.99"));
2844 assert!(match_property(&prop, &properties).unwrap());
2845
2846 properties.insert("version".to_string(), json!("1.0.0"));
2847 assert!(!match_property(&prop, &properties).unwrap());
2848 }
2849
2850 #[test]
2853 fn test_semver_invalid_property_value() {
2854 let prop = Property {
2855 key: "version".to_string(),
2856 value: json!("1.2.3"),
2857 operator: "semver_eq".to_string(),
2858 property_type: None,
2859 };
2860
2861 let mut properties = HashMap::new();
2862
2863 properties.insert("version".to_string(), json!("not-a-version"));
2865 assert!(match_property(&prop, &properties).is_err());
2866
2867 properties.insert("version".to_string(), json!(""));
2868 assert!(match_property(&prop, &properties).is_err());
2869
2870 properties.insert("version".to_string(), json!(".1.2.3"));
2871 assert!(match_property(&prop, &properties).is_err());
2872
2873 properties.insert("version".to_string(), json!("abc.def.ghi"));
2874 assert!(match_property(&prop, &properties).is_err());
2875 }
2876
2877 #[test]
2878 fn test_semver_invalid_target_value() {
2879 let mut properties = HashMap::new();
2880 properties.insert("version".to_string(), json!("1.2.3"));
2881
2882 let prop = Property {
2884 key: "version".to_string(),
2885 value: json!("not-valid"),
2886 operator: "semver_eq".to_string(),
2887 property_type: None,
2888 };
2889 assert!(match_property(&prop, &properties).is_err());
2890
2891 let prop = Property {
2892 key: "version".to_string(),
2893 value: json!(""),
2894 operator: "semver_gt".to_string(),
2895 property_type: None,
2896 };
2897 assert!(match_property(&prop, &properties).is_err());
2898 }
2899
2900 #[test]
2901 fn test_semver_invalid_wildcard_pattern() {
2902 let mut properties = HashMap::new();
2903 properties.insert("version".to_string(), json!("1.2.3"));
2904
2905 let invalid_patterns = vec![
2907 "*", "*.2.3", "1.*.3", "1.2.3.*", "abc.*", ];
2913
2914 for pattern in invalid_patterns {
2915 let prop = Property {
2916 key: "version".to_string(),
2917 value: json!(pattern),
2918 operator: "semver_wildcard".to_string(),
2919 property_type: None,
2920 };
2921 assert!(
2922 match_property(&prop, &properties).is_err(),
2923 "Pattern '{}' should be invalid",
2924 pattern
2925 );
2926 }
2927 }
2928
2929 #[test]
2930 fn test_semver_missing_property() {
2931 let prop = Property {
2932 key: "version".to_string(),
2933 value: json!("1.2.3"),
2934 operator: "semver_eq".to_string(),
2935 property_type: None,
2936 };
2937
2938 let properties = HashMap::new(); assert!(match_property(&prop, &properties).is_err());
2940 }
2941
2942 #[test]
2943 fn test_semver_null_property_value() {
2944 let prop = Property {
2945 key: "version".to_string(),
2946 value: json!("1.2.3"),
2947 operator: "semver_eq".to_string(),
2948 property_type: None,
2949 };
2950
2951 let mut properties = HashMap::new();
2952 properties.insert("version".to_string(), json!(null));
2953
2954 assert!(match_property(&prop, &properties).is_err());
2956 }
2957
2958 #[test]
2959 fn test_semver_numeric_property_value() {
2960 let prop = Property {
2962 key: "version".to_string(),
2963 value: json!("1.0.0"),
2964 operator: "semver_eq".to_string(),
2965 property_type: None,
2966 };
2967
2968 let mut properties = HashMap::new();
2969 properties.insert("version".to_string(), json!(1));
2971 assert!(match_property(&prop, &properties).unwrap());
2972 }
2973
2974 #[test]
2977 fn test_semver_four_part_versions() {
2978 let prop = Property {
2979 key: "version".to_string(),
2980 value: json!("1.2.3.4"),
2981 operator: "semver_eq".to_string(),
2982 property_type: None,
2983 };
2984
2985 let mut properties = HashMap::new();
2986
2987 properties.insert("version".to_string(), json!("1.2.3"));
2989 assert!(match_property(&prop, &properties).unwrap());
2990
2991 properties.insert("version".to_string(), json!("1.2.3.4"));
2992 assert!(match_property(&prop, &properties).unwrap());
2993
2994 properties.insert("version".to_string(), json!("1.2.3.999"));
2995 assert!(match_property(&prop, &properties).unwrap());
2996 }
2997
2998 #[test]
2999 fn test_semver_large_version_numbers() {
3000 let prop = Property {
3001 key: "version".to_string(),
3002 value: json!("1000.2000.3000"),
3003 operator: "semver_eq".to_string(),
3004 property_type: None,
3005 };
3006
3007 let mut properties = HashMap::new();
3008 properties.insert("version".to_string(), json!("1000.2000.3000"));
3009 assert!(match_property(&prop, &properties).unwrap());
3010 }
3011
3012 #[test]
3013 fn test_semver_comparison_ordering() {
3014 let cases = vec![
3016 ("0.0.1", "0.0.2", "semver_lt", true),
3017 ("0.1.0", "0.0.99", "semver_gt", true),
3018 ("1.0.0", "0.99.99", "semver_gt", true),
3019 ("1.0.0", "1.0.0", "semver_eq", true),
3020 ("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), ];
3025
3026 for (prop_val, target_val, op, expected) in cases {
3027 let prop = Property {
3028 key: "version".to_string(),
3029 value: json!(target_val),
3030 operator: op.to_string(),
3031 property_type: None,
3032 };
3033
3034 let mut properties = HashMap::new();
3035 properties.insert("version".to_string(), json!(prop_val));
3036
3037 assert_eq!(
3038 match_property(&prop, &properties).unwrap(),
3039 expected,
3040 "{} {} {} should be {}",
3041 prop_val,
3042 op,
3043 target_val,
3044 expected
3045 );
3046 }
3047 }
3048}