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}
119
120#[derive(Debug, Clone, Serialize, Deserialize)]
125pub struct FeatureFlagCondition {
126 #[serde(default)]
128 pub properties: Vec<Property>,
129 pub rollout_percentage: Option<f64>,
131 pub variant: Option<String>,
133}
134
135#[derive(Debug, Clone, Serialize, Deserialize)]
139pub struct Property {
140 pub key: String,
142 pub value: serde_json::Value,
144 #[serde(default = "default_operator")]
148 pub operator: String,
149 #[serde(rename = "type")]
151 pub property_type: Option<String>,
152}
153
154fn default_operator() -> String {
155 "exact".to_string()
156}
157
158#[derive(Debug, Clone, Serialize, Deserialize)]
160pub struct CohortDefinition {
161 pub id: String,
162 #[serde(default)]
166 pub properties: serde_json::Value,
167}
168
169impl CohortDefinition {
170 pub fn new(id: String, properties: Vec<Property>) -> Self {
172 Self {
173 id,
174 properties: serde_json::to_value(properties).unwrap_or_default(),
175 }
176 }
177
178 pub fn parse_properties(&self) -> Vec<Property> {
182 if let Some(arr) = self.properties.as_array() {
184 return arr
185 .iter()
186 .filter_map(|v| serde_json::from_value::<Property>(v.clone()).ok())
187 .collect();
188 }
189
190 if let Some(obj) = self.properties.as_object() {
192 if let Some(values) = obj.get("values") {
193 if let Some(values_arr) = values.as_array() {
194 return values_arr
195 .iter()
196 .filter_map(|v| {
197 if v.get("type").and_then(|t| t.as_str()) == Some("property") {
199 serde_json::from_value::<Property>(v.clone()).ok()
200 } else if let Some(inner_values) = v.get("values") {
201 inner_values.as_array().and_then(|arr| {
203 arr.iter()
204 .filter_map(|inner| {
205 serde_json::from_value::<Property>(inner.clone()).ok()
206 })
207 .next()
208 })
209 } else {
210 None
211 }
212 })
213 .collect();
214 }
215 }
216 }
217
218 Vec::new()
219 }
220}
221
222pub struct EvaluationContext<'a> {
224 pub cohorts: &'a HashMap<String, CohortDefinition>,
225 pub flags: &'a HashMap<String, FeatureFlag>,
226 pub distinct_id: &'a str,
227}
228
229#[derive(Debug, Clone, Serialize, Deserialize, Default)]
231pub struct MultivariateFilter {
232 pub variants: Vec<MultivariateVariant>,
234}
235
236#[derive(Debug, Clone, Serialize, Deserialize)]
238pub struct MultivariateVariant {
239 pub key: String,
241 pub rollout_percentage: f64,
243}
244
245#[derive(Debug, Clone, Serialize, Deserialize)]
250#[serde(untagged)]
251pub enum FeatureFlagsResponse {
252 V2 {
254 flags: HashMap<String, FlagDetail>,
256 #[serde(rename = "errorsWhileComputingFlags")]
258 #[serde(default)]
259 errors_while_computing_flags: bool,
260 #[serde(rename = "quotaLimited")]
263 #[serde(default)]
264 quota_limited: bool,
265 #[serde(rename = "requestId")]
269 #[serde(default)]
270 request_id: Option<String>,
271 },
272 Legacy {
274 #[serde(rename = "featureFlags")]
276 feature_flags: HashMap<String, FlagValue>,
277 #[serde(rename = "featureFlagPayloads")]
279 #[serde(default)]
280 feature_flag_payloads: HashMap<String, serde_json::Value>,
281 #[serde(default)]
283 errors: Option<Vec<String>>,
284 },
285}
286
287impl FeatureFlagsResponse {
288 pub fn normalize(
290 self,
291 ) -> (
292 HashMap<String, FlagValue>,
293 HashMap<String, serde_json::Value>,
294 ) {
295 match self {
296 FeatureFlagsResponse::V2 { flags, .. } => {
297 let mut feature_flags = HashMap::new();
298 let mut payloads = HashMap::new();
299
300 for (key, detail) in flags {
301 if detail.enabled {
302 if let Some(variant) = detail.variant {
303 feature_flags.insert(key.clone(), FlagValue::String(variant));
304 } else {
305 feature_flags.insert(key.clone(), FlagValue::Boolean(true));
306 }
307 } else {
308 feature_flags.insert(key.clone(), FlagValue::Boolean(false));
309 }
310
311 if let Some(metadata) = detail.metadata {
312 if let Some(payload) = metadata.payload {
313 payloads.insert(key, payload);
314 }
315 }
316 }
317
318 (feature_flags, payloads)
319 }
320 FeatureFlagsResponse::Legacy {
321 feature_flags,
322 feature_flag_payloads,
323 ..
324 } => (feature_flags, feature_flag_payloads),
325 }
326 }
327}
328
329#[derive(Debug, Clone, Serialize, Deserialize)]
334pub struct FlagDetail {
335 pub key: String,
337 pub enabled: bool,
339 pub variant: Option<String>,
341 #[serde(default)]
343 pub reason: Option<FlagReason>,
344 #[serde(default)]
346 pub metadata: Option<FlagMetadata>,
347}
348
349#[derive(Debug, Clone, Serialize, Deserialize)]
351pub struct FlagReason {
352 pub code: String,
354 #[serde(default)]
356 pub condition_index: Option<usize>,
357 #[serde(default)]
359 pub description: Option<String>,
360}
361
362#[derive(Debug, Clone, Serialize, Deserialize)]
364pub struct FlagMetadata {
365 pub id: u64,
367 pub version: u32,
369 pub description: Option<String>,
371 pub payload: Option<serde_json::Value>,
373}
374
375const LONG_SCALE: f64 = 0xFFFFFFFFFFFFFFFu64 as f64; pub fn hash_key(key: &str, distinct_id: &str, salt: &str) -> f64 {
383 let hash_key = format!("{key}.{distinct_id}{salt}");
384 let mut hasher = Sha1::new();
385 hasher.update(hash_key.as_bytes());
386 let result = hasher.finalize();
387 let hex_str = format!("{result:x}");
388 let hash_val = u64::from_str_radix(&hex_str[..15], 16).unwrap_or(0);
389 hash_val as f64 / LONG_SCALE
390}
391
392pub fn get_matching_variant(flag: &FeatureFlag, distinct_id: &str) -> Option<String> {
398 let hash_value = hash_key(&flag.key, distinct_id, VARIANT_HASH_SALT);
399 let variants = flag.filters.multivariate.as_ref()?.variants.as_slice();
400
401 let mut value_min = 0.0;
402 for variant in variants {
403 let value_max = value_min + variant.rollout_percentage / 100.0;
404 if hash_value >= value_min && hash_value < value_max {
405 return Some(variant.key.clone());
406 }
407 value_min = value_max;
408 }
409 None
410}
411
412#[must_use = "feature flag evaluation result should be used"]
413pub fn match_feature_flag(
414 flag: &FeatureFlag,
415 distinct_id: &str,
416 properties: &HashMap<String, serde_json::Value>,
417) -> Result<FlagValue, InconclusiveMatchError> {
418 if !flag.active {
419 return Ok(FlagValue::Boolean(false));
420 }
421
422 let conditions = &flag.filters.groups;
423
424 let mut sorted_conditions = conditions.clone();
426 sorted_conditions.sort_by_key(|c| if c.variant.is_some() { 0 } else { 1 });
427
428 let mut is_inconclusive = false;
429
430 for condition in sorted_conditions {
431 match is_condition_match(flag, distinct_id, &condition, properties) {
432 Ok(true) => {
433 if let Some(variant_override) = &condition.variant {
434 if let Some(ref multivariate) = flag.filters.multivariate {
436 let valid_variants: Vec<String> = multivariate
437 .variants
438 .iter()
439 .map(|v| v.key.clone())
440 .collect();
441
442 if valid_variants.contains(variant_override) {
443 return Ok(FlagValue::String(variant_override.clone()));
444 }
445 }
446 }
447
448 if let Some(variant) = get_matching_variant(flag, distinct_id) {
450 return Ok(FlagValue::String(variant));
451 }
452 return Ok(FlagValue::Boolean(true));
453 }
454 Ok(false) => continue,
455 Err(_) => {
456 is_inconclusive = true;
457 }
458 }
459 }
460
461 if is_inconclusive {
462 return Err(InconclusiveMatchError::new(
463 "Can't determine if feature flag is enabled or not with given properties",
464 ));
465 }
466
467 Ok(FlagValue::Boolean(false))
468}
469
470fn is_condition_match(
471 flag: &FeatureFlag,
472 distinct_id: &str,
473 condition: &FeatureFlagCondition,
474 properties: &HashMap<String, serde_json::Value>,
475) -> Result<bool, InconclusiveMatchError> {
476 for prop in &condition.properties {
478 if !match_property(prop, properties)? {
479 return Ok(false);
480 }
481 }
482
483 if let Some(rollout_percentage) = condition.rollout_percentage {
485 let hash_value = hash_key(&flag.key, distinct_id, ROLLOUT_HASH_SALT);
486 if hash_value > (rollout_percentage / 100.0) {
487 return Ok(false);
488 }
489 }
490
491 Ok(true)
492}
493
494#[must_use = "feature flag evaluation result should be used"]
497pub fn match_feature_flag_with_context(
498 flag: &FeatureFlag,
499 distinct_id: &str,
500 properties: &HashMap<String, serde_json::Value>,
501 ctx: &EvaluationContext,
502) -> Result<FlagValue, InconclusiveMatchError> {
503 if !flag.active {
504 return Ok(FlagValue::Boolean(false));
505 }
506
507 let conditions = &flag.filters.groups;
508
509 let mut sorted_conditions = conditions.clone();
511 sorted_conditions.sort_by_key(|c| if c.variant.is_some() { 0 } else { 1 });
512
513 let mut is_inconclusive = false;
514
515 for condition in sorted_conditions {
516 match is_condition_match_with_context(flag, distinct_id, &condition, properties, ctx) {
517 Ok(true) => {
518 if let Some(variant_override) = &condition.variant {
519 if let Some(ref multivariate) = flag.filters.multivariate {
521 let valid_variants: Vec<String> = multivariate
522 .variants
523 .iter()
524 .map(|v| v.key.clone())
525 .collect();
526
527 if valid_variants.contains(variant_override) {
528 return Ok(FlagValue::String(variant_override.clone()));
529 }
530 }
531 }
532
533 if let Some(variant) = get_matching_variant(flag, distinct_id) {
535 return Ok(FlagValue::String(variant));
536 }
537 return Ok(FlagValue::Boolean(true));
538 }
539 Ok(false) => continue,
540 Err(_) => {
541 is_inconclusive = true;
542 }
543 }
544 }
545
546 if is_inconclusive {
547 return Err(InconclusiveMatchError::new(
548 "Can't determine if feature flag is enabled or not with given properties",
549 ));
550 }
551
552 Ok(FlagValue::Boolean(false))
553}
554
555fn is_condition_match_with_context(
556 flag: &FeatureFlag,
557 distinct_id: &str,
558 condition: &FeatureFlagCondition,
559 properties: &HashMap<String, serde_json::Value>,
560 ctx: &EvaluationContext,
561) -> Result<bool, InconclusiveMatchError> {
562 for prop in &condition.properties {
564 if !match_property_with_context(prop, properties, ctx)? {
565 return Ok(false);
566 }
567 }
568
569 if let Some(rollout_percentage) = condition.rollout_percentage {
571 let hash_value = hash_key(&flag.key, distinct_id, ROLLOUT_HASH_SALT);
572 if hash_value > (rollout_percentage / 100.0) {
573 return Ok(false);
574 }
575 }
576
577 Ok(true)
578}
579
580pub fn match_property_with_context(
582 property: &Property,
583 properties: &HashMap<String, serde_json::Value>,
584 ctx: &EvaluationContext,
585) -> Result<bool, InconclusiveMatchError> {
586 if property.property_type.as_deref() == Some("cohort") {
588 return match_cohort_property(property, properties, ctx);
589 }
590
591 if property.key.starts_with("$feature/") {
593 return match_flag_dependency_property(property, ctx);
594 }
595
596 match_property(property, properties)
598}
599
600fn match_cohort_property(
602 property: &Property,
603 properties: &HashMap<String, serde_json::Value>,
604 ctx: &EvaluationContext,
605) -> Result<bool, InconclusiveMatchError> {
606 let cohort_id = property
607 .value
608 .as_str()
609 .ok_or_else(|| InconclusiveMatchError::new("Cohort ID must be a string"))?;
610
611 let cohort = ctx.cohorts.get(cohort_id).ok_or_else(|| {
612 InconclusiveMatchError::new(&format!("Cohort '{}' not found in local cache", cohort_id))
613 })?;
614
615 let cohort_properties = cohort.parse_properties();
617 let mut is_in_cohort = true;
618 for cohort_prop in &cohort_properties {
619 match match_property(cohort_prop, properties) {
620 Ok(true) => continue,
621 Ok(false) => {
622 is_in_cohort = false;
623 break;
624 }
625 Err(e) => {
626 return Err(InconclusiveMatchError::new(&format!(
628 "Cannot evaluate cohort '{}' property '{}': {}",
629 cohort_id, cohort_prop.key, e.message
630 )));
631 }
632 }
633 }
634
635 Ok(match property.operator.as_str() {
637 "in" => is_in_cohort,
638 "not_in" => !is_in_cohort,
639 op => {
640 return Err(InconclusiveMatchError::new(&format!(
641 "Unknown cohort operator: {}",
642 op
643 )));
644 }
645 })
646}
647
648fn match_flag_dependency_property(
650 property: &Property,
651 ctx: &EvaluationContext,
652) -> Result<bool, InconclusiveMatchError> {
653 let flag_key = property
655 .key
656 .strip_prefix("$feature/")
657 .ok_or_else(|| InconclusiveMatchError::new("Invalid flag dependency format"))?;
658
659 let flag = ctx.flags.get(flag_key).ok_or_else(|| {
660 InconclusiveMatchError::new(&format!("Flag '{}' not found in local cache", flag_key))
661 })?;
662
663 let empty_props = HashMap::new();
665 let flag_value = match_feature_flag(flag, ctx.distinct_id, &empty_props)?;
666
667 let expected = &property.value;
669
670 let matches = match (&flag_value, expected) {
671 (FlagValue::Boolean(b), serde_json::Value::Bool(expected_b)) => b == expected_b,
672 (FlagValue::String(s), serde_json::Value::String(expected_s)) => {
673 s.eq_ignore_ascii_case(expected_s)
674 }
675 (FlagValue::Boolean(true), serde_json::Value::String(s)) => {
676 s.is_empty() || s == "true"
679 }
680 (FlagValue::Boolean(false), serde_json::Value::String(s)) => s.is_empty() || s == "false",
681 (FlagValue::String(s), serde_json::Value::Bool(true)) => {
682 !s.is_empty()
684 }
685 (FlagValue::String(_), serde_json::Value::Bool(false)) => false,
686 _ => false,
687 };
688
689 Ok(match property.operator.as_str() {
691 "exact" => matches,
692 "is_not" => !matches,
693 op => {
694 return Err(InconclusiveMatchError::new(&format!(
695 "Unknown flag dependency operator: {}",
696 op
697 )));
698 }
699 })
700}
701
702fn parse_relative_date(value: &str) -> Option<DateTime<Utc>> {
705 let value = value.trim();
706 if value.len() < 3 || !value.starts_with('-') {
708 return None;
709 }
710
711 let (num_str, unit) = value[1..].split_at(value.len() - 2);
712 let num: i64 = num_str.parse().ok()?;
713
714 let duration = match unit {
715 "h" => chrono::Duration::hours(num),
716 "d" => chrono::Duration::days(num),
717 "w" => chrono::Duration::weeks(num),
718 "m" => chrono::Duration::days(num * 30), "y" => chrono::Duration::days(num * 365), _ => return None,
721 };
722
723 Some(Utc::now() - duration)
724}
725
726fn parse_date_value(value: &serde_json::Value) -> Option<DateTime<Utc>> {
728 let date_str = value.as_str()?;
729
730 if date_str.starts_with('-') && date_str.len() > 1 {
732 if let Some(dt) = parse_relative_date(date_str) {
733 return Some(dt);
734 }
735 }
736
737 if let Ok(dt) = DateTime::parse_from_rfc3339(date_str) {
739 return Some(dt.with_timezone(&Utc));
740 }
741
742 if let Ok(date) = NaiveDate::parse_from_str(date_str, "%Y-%m-%d") {
744 return Some(
745 date.and_hms_opt(0, 0, 0)
746 .expect("midnight is always valid")
747 .and_utc(),
748 );
749 }
750
751 None
752}
753
754type SemverTuple = (u64, u64, u64);
756
757fn parse_semver(value: &str) -> Option<SemverTuple> {
768 let value = value.trim();
769 if value.is_empty() {
770 return None;
771 }
772
773 let value = value
775 .strip_prefix('v')
776 .or_else(|| value.strip_prefix('V'))
777 .unwrap_or(value);
778 if value.is_empty() {
779 return None;
780 }
781
782 let value = value.split(['-', '+']).next().unwrap_or(value);
784 if value.is_empty() {
785 return None;
786 }
787
788 if value.starts_with('.') {
790 return None;
791 }
792
793 let parts: Vec<&str> = value.split('.').collect();
795 if parts.is_empty() {
796 return None;
797 }
798
799 let major: u64 = parts.first().and_then(|s| s.parse().ok())?;
800 let minor: u64 = parts.get(1).map(|s| s.parse().ok()).unwrap_or(Some(0))?;
801 let patch: u64 = parts.get(2).map(|s| s.parse().ok()).unwrap_or(Some(0))?;
802
803 Some((major, minor, patch))
804}
805
806fn parse_semver_wildcard(pattern: &str) -> Option<(SemverTuple, SemverTuple)> {
809 let pattern = pattern.trim();
810 if pattern.is_empty() {
811 return None;
812 }
813
814 let pattern = pattern
816 .strip_prefix('v')
817 .or_else(|| pattern.strip_prefix('V'))
818 .unwrap_or(pattern);
819 if pattern.is_empty() {
820 return None;
821 }
822
823 let parts: Vec<&str> = pattern.split('.').collect();
824
825 match parts.as_slice() {
826 [major_str, "*"] => {
828 let major: u64 = major_str.parse().ok()?;
829 Some(((major, 0, 0), (major + 1, 0, 0)))
830 }
831 [major_str, minor_str, "*"] => {
833 let major: u64 = major_str.parse().ok()?;
834 let minor: u64 = minor_str.parse().ok()?;
835 Some(((major, minor, 0), (major, minor + 1, 0)))
836 }
837 _ => None,
838 }
839}
840
841fn compute_tilde_bounds(version: SemverTuple) -> (SemverTuple, SemverTuple) {
843 let (major, minor, patch) = version;
844 ((major, minor, patch), (major, minor + 1, 0))
845}
846
847fn compute_caret_bounds(version: SemverTuple) -> (SemverTuple, SemverTuple) {
852 let (major, minor, patch) = version;
853 if major > 0 {
854 ((major, minor, patch), (major + 1, 0, 0))
855 } else if minor > 0 {
856 ((0, minor, patch), (0, minor + 1, 0))
857 } else {
858 ((0, 0, patch), (0, 0, patch + 1))
859 }
860}
861
862fn match_property(
863 property: &Property,
864 properties: &HashMap<String, serde_json::Value>,
865) -> Result<bool, InconclusiveMatchError> {
866 let value = match properties.get(&property.key) {
867 Some(v) => v,
868 None => {
869 if property.operator == "is_not_set" {
871 return Ok(true);
872 }
873 if property.operator == "is_set" {
875 return Ok(false);
876 }
877 return Err(InconclusiveMatchError::new(&format!(
879 "Property '{}' not found in provided properties",
880 property.key
881 )));
882 }
883 };
884
885 Ok(match property.operator.as_str() {
886 "exact" => {
887 if property.value.is_array() {
888 if let Some(arr) = property.value.as_array() {
889 for val in arr {
890 if compare_values(val, value) {
891 return Ok(true);
892 }
893 }
894 return Ok(false);
895 }
896 }
897 compare_values(&property.value, value)
898 }
899 "is_not" => {
900 if property.value.is_array() {
901 if let Some(arr) = property.value.as_array() {
902 for val in arr {
903 if compare_values(val, value) {
904 return Ok(false);
905 }
906 }
907 return Ok(true);
908 }
909 }
910 !compare_values(&property.value, value)
911 }
912 "is_set" => true, "is_not_set" => false, "icontains" => {
915 let prop_str = value_to_string(value);
916 let search_str = value_to_string(&property.value);
917 prop_str.to_lowercase().contains(&search_str.to_lowercase())
918 }
919 "not_icontains" => {
920 let prop_str = value_to_string(value);
921 let search_str = value_to_string(&property.value);
922 !prop_str.to_lowercase().contains(&search_str.to_lowercase())
923 }
924 "regex" => {
925 let prop_str = value_to_string(value);
926 let regex_str = value_to_string(&property.value);
927 get_cached_regex(®ex_str)
928 .map(|re| re.is_match(&prop_str))
929 .unwrap_or(false)
930 }
931 "not_regex" => {
932 let prop_str = value_to_string(value);
933 let regex_str = value_to_string(&property.value);
934 get_cached_regex(®ex_str)
935 .map(|re| !re.is_match(&prop_str))
936 .unwrap_or(true)
937 }
938 "gt" | "gte" | "lt" | "lte" => compare_numeric(&property.operator, &property.value, value),
939 "is_date_before" | "is_date_after" => {
940 let target_date = parse_date_value(&property.value).ok_or_else(|| {
941 InconclusiveMatchError::new(&format!(
942 "Unable to parse target date value: {:?}",
943 property.value
944 ))
945 })?;
946
947 let prop_date = parse_date_value(value).ok_or_else(|| {
948 InconclusiveMatchError::new(&format!(
949 "Unable to parse property date value for '{}': {:?}",
950 property.key, value
951 ))
952 })?;
953
954 if property.operator == "is_date_before" {
955 prop_date < target_date
956 } else {
957 prop_date > target_date
958 }
959 }
960 "semver_eq" | "semver_neq" | "semver_gt" | "semver_gte" | "semver_lt" | "semver_lte" => {
962 let prop_str = value_to_string(value);
963 let target_str = value_to_string(&property.value);
964
965 let prop_version = parse_semver(&prop_str).ok_or_else(|| {
966 InconclusiveMatchError::new(&format!(
967 "Unable to parse property semver value for '{}': {:?}",
968 property.key, value
969 ))
970 })?;
971
972 let target_version = parse_semver(&target_str).ok_or_else(|| {
973 InconclusiveMatchError::new(&format!(
974 "Unable to parse target semver value: {:?}",
975 property.value
976 ))
977 })?;
978
979 match property.operator.as_str() {
980 "semver_eq" => prop_version == target_version,
981 "semver_neq" => prop_version != target_version,
982 "semver_gt" => prop_version > target_version,
983 "semver_gte" => prop_version >= target_version,
984 "semver_lt" => prop_version < target_version,
985 "semver_lte" => prop_version <= target_version,
986 _ => unreachable!(),
987 }
988 }
989 "semver_tilde" => {
990 let prop_str = value_to_string(value);
991 let target_str = value_to_string(&property.value);
992
993 let prop_version = parse_semver(&prop_str).ok_or_else(|| {
994 InconclusiveMatchError::new(&format!(
995 "Unable to parse property semver value for '{}': {:?}",
996 property.key, value
997 ))
998 })?;
999
1000 let target_version = parse_semver(&target_str).ok_or_else(|| {
1001 InconclusiveMatchError::new(&format!(
1002 "Unable to parse target semver value: {:?}",
1003 property.value
1004 ))
1005 })?;
1006
1007 let (lower, upper) = compute_tilde_bounds(target_version);
1008 prop_version >= lower && prop_version < upper
1009 }
1010 "semver_caret" => {
1011 let prop_str = value_to_string(value);
1012 let target_str = value_to_string(&property.value);
1013
1014 let prop_version = parse_semver(&prop_str).ok_or_else(|| {
1015 InconclusiveMatchError::new(&format!(
1016 "Unable to parse property semver value for '{}': {:?}",
1017 property.key, value
1018 ))
1019 })?;
1020
1021 let target_version = parse_semver(&target_str).ok_or_else(|| {
1022 InconclusiveMatchError::new(&format!(
1023 "Unable to parse target semver value: {:?}",
1024 property.value
1025 ))
1026 })?;
1027
1028 let (lower, upper) = compute_caret_bounds(target_version);
1029 prop_version >= lower && prop_version < upper
1030 }
1031 "semver_wildcard" => {
1032 let prop_str = value_to_string(value);
1033 let target_str = value_to_string(&property.value);
1034
1035 let prop_version = parse_semver(&prop_str).ok_or_else(|| {
1036 InconclusiveMatchError::new(&format!(
1037 "Unable to parse property semver value for '{}': {:?}",
1038 property.key, value
1039 ))
1040 })?;
1041
1042 let (lower, upper) = parse_semver_wildcard(&target_str).ok_or_else(|| {
1043 InconclusiveMatchError::new(&format!(
1044 "Unable to parse target semver wildcard pattern: {:?}",
1045 property.value
1046 ))
1047 })?;
1048
1049 prop_version >= lower && prop_version < upper
1050 }
1051 unknown => {
1052 return Err(InconclusiveMatchError::new(&format!(
1053 "Unknown operator: {}",
1054 unknown
1055 )));
1056 }
1057 })
1058}
1059
1060fn compare_values(a: &serde_json::Value, b: &serde_json::Value) -> bool {
1061 if let (Some(a_str), Some(b_str)) = (a.as_str(), b.as_str()) {
1063 return a_str.eq_ignore_ascii_case(b_str);
1064 }
1065
1066 a == b
1068}
1069
1070fn value_to_string(value: &serde_json::Value) -> String {
1071 match value {
1072 serde_json::Value::String(s) => s.clone(),
1073 serde_json::Value::Number(n) => n.to_string(),
1074 serde_json::Value::Bool(b) => b.to_string(),
1075 _ => value.to_string(),
1076 }
1077}
1078
1079fn compare_numeric(
1080 operator: &str,
1081 property_value: &serde_json::Value,
1082 value: &serde_json::Value,
1083) -> bool {
1084 let prop_num = match property_value {
1085 serde_json::Value::Number(n) => n.as_f64(),
1086 serde_json::Value::String(s) => s.parse::<f64>().ok(),
1087 _ => None,
1088 };
1089
1090 let val_num = match value {
1091 serde_json::Value::Number(n) => n.as_f64(),
1092 serde_json::Value::String(s) => s.parse::<f64>().ok(),
1093 _ => None,
1094 };
1095
1096 if let (Some(prop), Some(val)) = (prop_num, val_num) {
1097 match operator {
1098 "gt" => val > prop,
1099 "gte" => val >= prop,
1100 "lt" => val < prop,
1101 "lte" => val <= prop,
1102 _ => false,
1103 }
1104 } else {
1105 let prop_str = value_to_string(property_value);
1107 let val_str = value_to_string(value);
1108 match operator {
1109 "gt" => val_str > prop_str,
1110 "gte" => val_str >= prop_str,
1111 "lt" => val_str < prop_str,
1112 "lte" => val_str <= prop_str,
1113 _ => false,
1114 }
1115 }
1116}
1117
1118#[cfg(test)]
1119mod tests {
1120 use super::*;
1121 use serde_json::json;
1122
1123 const TEST_SALT: &str = "test-salt";
1125
1126 #[test]
1127 fn test_hash_key() {
1128 let hash = hash_key("test-flag", "user-123", TEST_SALT);
1129 assert!((0.0..=1.0).contains(&hash));
1130
1131 let hash2 = hash_key("test-flag", "user-123", TEST_SALT);
1133 assert_eq!(hash, hash2);
1134
1135 let hash3 = hash_key("test-flag", "user-456", TEST_SALT);
1137 assert_ne!(hash, hash3);
1138 }
1139
1140 #[test]
1141 fn test_simple_flag_match() {
1142 let flag = FeatureFlag {
1143 key: "test-flag".to_string(),
1144 active: true,
1145 filters: FeatureFlagFilters {
1146 groups: vec![FeatureFlagCondition {
1147 properties: vec![],
1148 rollout_percentage: Some(100.0),
1149 variant: None,
1150 }],
1151 multivariate: None,
1152 payloads: HashMap::new(),
1153 },
1154 };
1155
1156 let properties = HashMap::new();
1157 let result = match_feature_flag(&flag, "user-123", &properties).unwrap();
1158 assert_eq!(result, FlagValue::Boolean(true));
1159 }
1160
1161 #[test]
1162 fn test_property_matching() {
1163 let prop = Property {
1164 key: "country".to_string(),
1165 value: json!("US"),
1166 operator: "exact".to_string(),
1167 property_type: None,
1168 };
1169
1170 let mut properties = HashMap::new();
1171 properties.insert("country".to_string(), json!("US"));
1172
1173 assert!(match_property(&prop, &properties).unwrap());
1174
1175 properties.insert("country".to_string(), json!("UK"));
1176 assert!(!match_property(&prop, &properties).unwrap());
1177 }
1178
1179 #[test]
1180 fn test_multivariate_variants() {
1181 let flag = FeatureFlag {
1182 key: "test-flag".to_string(),
1183 active: true,
1184 filters: FeatureFlagFilters {
1185 groups: vec![FeatureFlagCondition {
1186 properties: vec![],
1187 rollout_percentage: Some(100.0),
1188 variant: None,
1189 }],
1190 multivariate: Some(MultivariateFilter {
1191 variants: vec![
1192 MultivariateVariant {
1193 key: "control".to_string(),
1194 rollout_percentage: 50.0,
1195 },
1196 MultivariateVariant {
1197 key: "test".to_string(),
1198 rollout_percentage: 50.0,
1199 },
1200 ],
1201 }),
1202 payloads: HashMap::new(),
1203 },
1204 };
1205
1206 let properties = HashMap::new();
1207 let result = match_feature_flag(&flag, "user-123", &properties).unwrap();
1208
1209 match result {
1210 FlagValue::String(variant) => {
1211 assert!(variant == "control" || variant == "test");
1212 }
1213 _ => panic!("Expected string variant"),
1214 }
1215 }
1216
1217 #[test]
1218 fn test_inactive_flag() {
1219 let flag = FeatureFlag {
1220 key: "inactive-flag".to_string(),
1221 active: false,
1222 filters: FeatureFlagFilters {
1223 groups: vec![FeatureFlagCondition {
1224 properties: vec![],
1225 rollout_percentage: Some(100.0),
1226 variant: None,
1227 }],
1228 multivariate: None,
1229 payloads: HashMap::new(),
1230 },
1231 };
1232
1233 let properties = HashMap::new();
1234 let result = match_feature_flag(&flag, "user-123", &properties).unwrap();
1235 assert_eq!(result, FlagValue::Boolean(false));
1236 }
1237
1238 #[test]
1239 fn test_rollout_percentage() {
1240 let flag = FeatureFlag {
1241 key: "rollout-flag".to_string(),
1242 active: true,
1243 filters: FeatureFlagFilters {
1244 groups: vec![FeatureFlagCondition {
1245 properties: vec![],
1246 rollout_percentage: Some(30.0), variant: None,
1248 }],
1249 multivariate: None,
1250 payloads: HashMap::new(),
1251 },
1252 };
1253
1254 let properties = HashMap::new();
1255
1256 let mut enabled_count = 0;
1258 for i in 0..1000 {
1259 let result = match_feature_flag(&flag, &format!("user-{}", i), &properties).unwrap();
1260 if result == FlagValue::Boolean(true) {
1261 enabled_count += 1;
1262 }
1263 }
1264
1265 assert!(enabled_count > 250 && enabled_count < 350);
1267 }
1268
1269 #[test]
1270 fn test_regex_operator() {
1271 let prop = Property {
1272 key: "email".to_string(),
1273 value: json!(".*@company\\.com$"),
1274 operator: "regex".to_string(),
1275 property_type: None,
1276 };
1277
1278 let mut properties = HashMap::new();
1279 properties.insert("email".to_string(), json!("user@company.com"));
1280 assert!(match_property(&prop, &properties).unwrap());
1281
1282 properties.insert("email".to_string(), json!("user@example.com"));
1283 assert!(!match_property(&prop, &properties).unwrap());
1284 }
1285
1286 #[test]
1287 fn test_icontains_operator() {
1288 let prop = Property {
1289 key: "name".to_string(),
1290 value: json!("ADMIN"),
1291 operator: "icontains".to_string(),
1292 property_type: None,
1293 };
1294
1295 let mut properties = HashMap::new();
1296 properties.insert("name".to_string(), json!("admin_user"));
1297 assert!(match_property(&prop, &properties).unwrap());
1298
1299 properties.insert("name".to_string(), json!("regular_user"));
1300 assert!(!match_property(&prop, &properties).unwrap());
1301 }
1302
1303 #[test]
1304 fn test_numeric_operators() {
1305 let prop_gt = Property {
1307 key: "age".to_string(),
1308 value: json!(18),
1309 operator: "gt".to_string(),
1310 property_type: None,
1311 };
1312
1313 let mut properties = HashMap::new();
1314 properties.insert("age".to_string(), json!(25));
1315 assert!(match_property(&prop_gt, &properties).unwrap());
1316
1317 properties.insert("age".to_string(), json!(15));
1318 assert!(!match_property(&prop_gt, &properties).unwrap());
1319
1320 let prop_lte = Property {
1322 key: "score".to_string(),
1323 value: json!(100),
1324 operator: "lte".to_string(),
1325 property_type: None,
1326 };
1327
1328 properties.insert("score".to_string(), json!(100));
1329 assert!(match_property(&prop_lte, &properties).unwrap());
1330
1331 properties.insert("score".to_string(), json!(101));
1332 assert!(!match_property(&prop_lte, &properties).unwrap());
1333 }
1334
1335 #[test]
1336 fn test_is_set_operator() {
1337 let prop = Property {
1338 key: "email".to_string(),
1339 value: json!(true),
1340 operator: "is_set".to_string(),
1341 property_type: None,
1342 };
1343
1344 let mut properties = HashMap::new();
1345 properties.insert("email".to_string(), json!("test@example.com"));
1346 assert!(match_property(&prop, &properties).unwrap());
1347
1348 properties.remove("email");
1349 assert!(!match_property(&prop, &properties).unwrap());
1350 }
1351
1352 #[test]
1353 fn test_is_not_set_operator() {
1354 let prop = Property {
1355 key: "phone".to_string(),
1356 value: json!(true),
1357 operator: "is_not_set".to_string(),
1358 property_type: None,
1359 };
1360
1361 let mut properties = HashMap::new();
1362 assert!(match_property(&prop, &properties).unwrap());
1363
1364 properties.insert("phone".to_string(), json!("+1234567890"));
1365 assert!(!match_property(&prop, &properties).unwrap());
1366 }
1367
1368 #[test]
1369 fn test_empty_groups() {
1370 let flag = FeatureFlag {
1371 key: "empty-groups".to_string(),
1372 active: true,
1373 filters: FeatureFlagFilters {
1374 groups: vec![],
1375 multivariate: None,
1376 payloads: HashMap::new(),
1377 },
1378 };
1379
1380 let properties = HashMap::new();
1381 let result = match_feature_flag(&flag, "user-123", &properties).unwrap();
1382 assert_eq!(result, FlagValue::Boolean(false));
1383 }
1384
1385 #[test]
1386 fn test_hash_scale_constant() {
1387 assert_eq!(LONG_SCALE, 0xFFFFFFFFFFFFFFFu64 as f64);
1389 assert_ne!(LONG_SCALE, 0xFFFFFFFFFFFFFFFFu64 as f64);
1390 }
1391
1392 #[test]
1395 fn test_unknown_operator_returns_inconclusive_error() {
1396 let prop = Property {
1397 key: "status".to_string(),
1398 value: json!("active"),
1399 operator: "unknown_operator".to_string(),
1400 property_type: None,
1401 };
1402
1403 let mut properties = HashMap::new();
1404 properties.insert("status".to_string(), json!("active"));
1405
1406 let result = match_property(&prop, &properties);
1407 assert!(result.is_err());
1408 let err = result.unwrap_err();
1409 assert!(err.message.contains("unknown_operator"));
1410 }
1411
1412 #[test]
1413 fn test_is_date_before_with_relative_date() {
1414 let prop = Property {
1415 key: "signup_date".to_string(),
1416 value: json!("-7d"), operator: "is_date_before".to_string(),
1418 property_type: None,
1419 };
1420
1421 let mut properties = HashMap::new();
1422 let ten_days_ago = chrono::Utc::now() - chrono::Duration::days(10);
1424 properties.insert(
1425 "signup_date".to_string(),
1426 json!(ten_days_ago.format("%Y-%m-%d").to_string()),
1427 );
1428 assert!(match_property(&prop, &properties).unwrap());
1429
1430 let three_days_ago = chrono::Utc::now() - chrono::Duration::days(3);
1432 properties.insert(
1433 "signup_date".to_string(),
1434 json!(three_days_ago.format("%Y-%m-%d").to_string()),
1435 );
1436 assert!(!match_property(&prop, &properties).unwrap());
1437 }
1438
1439 #[test]
1440 fn test_is_date_after_with_relative_date() {
1441 let prop = Property {
1442 key: "last_seen".to_string(),
1443 value: json!("-30d"), operator: "is_date_after".to_string(),
1445 property_type: None,
1446 };
1447
1448 let mut properties = HashMap::new();
1449 let ten_days_ago = chrono::Utc::now() - chrono::Duration::days(10);
1451 properties.insert(
1452 "last_seen".to_string(),
1453 json!(ten_days_ago.format("%Y-%m-%d").to_string()),
1454 );
1455 assert!(match_property(&prop, &properties).unwrap());
1456
1457 let sixty_days_ago = chrono::Utc::now() - chrono::Duration::days(60);
1459 properties.insert(
1460 "last_seen".to_string(),
1461 json!(sixty_days_ago.format("%Y-%m-%d").to_string()),
1462 );
1463 assert!(!match_property(&prop, &properties).unwrap());
1464 }
1465
1466 #[test]
1467 fn test_is_date_before_with_iso_date() {
1468 let prop = Property {
1469 key: "expiry_date".to_string(),
1470 value: json!("2024-06-15"),
1471 operator: "is_date_before".to_string(),
1472 property_type: None,
1473 };
1474
1475 let mut properties = HashMap::new();
1476 properties.insert("expiry_date".to_string(), json!("2024-06-10"));
1477 assert!(match_property(&prop, &properties).unwrap());
1478
1479 properties.insert("expiry_date".to_string(), json!("2024-06-20"));
1480 assert!(!match_property(&prop, &properties).unwrap());
1481 }
1482
1483 #[test]
1484 fn test_is_date_after_with_iso_date() {
1485 let prop = Property {
1486 key: "start_date".to_string(),
1487 value: json!("2024-01-01"),
1488 operator: "is_date_after".to_string(),
1489 property_type: None,
1490 };
1491
1492 let mut properties = HashMap::new();
1493 properties.insert("start_date".to_string(), json!("2024-03-15"));
1494 assert!(match_property(&prop, &properties).unwrap());
1495
1496 properties.insert("start_date".to_string(), json!("2023-12-01"));
1497 assert!(!match_property(&prop, &properties).unwrap());
1498 }
1499
1500 #[test]
1501 fn test_is_date_with_relative_hours() {
1502 let prop = Property {
1503 key: "last_active".to_string(),
1504 value: json!("-24h"), operator: "is_date_after".to_string(),
1506 property_type: None,
1507 };
1508
1509 let mut properties = HashMap::new();
1510 let twelve_hours_ago = chrono::Utc::now() - chrono::Duration::hours(12);
1512 properties.insert(
1513 "last_active".to_string(),
1514 json!(twelve_hours_ago.to_rfc3339()),
1515 );
1516 assert!(match_property(&prop, &properties).unwrap());
1517
1518 let forty_eight_hours_ago = chrono::Utc::now() - chrono::Duration::hours(48);
1520 properties.insert(
1521 "last_active".to_string(),
1522 json!(forty_eight_hours_ago.to_rfc3339()),
1523 );
1524 assert!(!match_property(&prop, &properties).unwrap());
1525 }
1526
1527 #[test]
1528 fn test_is_date_with_relative_weeks() {
1529 let prop = Property {
1530 key: "joined".to_string(),
1531 value: json!("-2w"), operator: "is_date_before".to_string(),
1533 property_type: None,
1534 };
1535
1536 let mut properties = HashMap::new();
1537 let three_weeks_ago = chrono::Utc::now() - chrono::Duration::weeks(3);
1539 properties.insert(
1540 "joined".to_string(),
1541 json!(three_weeks_ago.format("%Y-%m-%d").to_string()),
1542 );
1543 assert!(match_property(&prop, &properties).unwrap());
1544
1545 let one_week_ago = chrono::Utc::now() - chrono::Duration::weeks(1);
1547 properties.insert(
1548 "joined".to_string(),
1549 json!(one_week_ago.format("%Y-%m-%d").to_string()),
1550 );
1551 assert!(!match_property(&prop, &properties).unwrap());
1552 }
1553
1554 #[test]
1555 fn test_is_date_with_relative_months() {
1556 let prop = Property {
1557 key: "subscription_date".to_string(),
1558 value: json!("-3m"), operator: "is_date_after".to_string(),
1560 property_type: None,
1561 };
1562
1563 let mut properties = HashMap::new();
1564 let one_month_ago = chrono::Utc::now() - chrono::Duration::days(30);
1566 properties.insert(
1567 "subscription_date".to_string(),
1568 json!(one_month_ago.format("%Y-%m-%d").to_string()),
1569 );
1570 assert!(match_property(&prop, &properties).unwrap());
1571
1572 let six_months_ago = chrono::Utc::now() - chrono::Duration::days(180);
1574 properties.insert(
1575 "subscription_date".to_string(),
1576 json!(six_months_ago.format("%Y-%m-%d").to_string()),
1577 );
1578 assert!(!match_property(&prop, &properties).unwrap());
1579 }
1580
1581 #[test]
1582 fn test_is_date_with_relative_years() {
1583 let prop = Property {
1584 key: "created_at".to_string(),
1585 value: json!("-1y"), operator: "is_date_before".to_string(),
1587 property_type: None,
1588 };
1589
1590 let mut properties = HashMap::new();
1591 let two_years_ago = chrono::Utc::now() - chrono::Duration::days(730);
1593 properties.insert(
1594 "created_at".to_string(),
1595 json!(two_years_ago.format("%Y-%m-%d").to_string()),
1596 );
1597 assert!(match_property(&prop, &properties).unwrap());
1598
1599 let six_months_ago = chrono::Utc::now() - chrono::Duration::days(180);
1601 properties.insert(
1602 "created_at".to_string(),
1603 json!(six_months_ago.format("%Y-%m-%d").to_string()),
1604 );
1605 assert!(!match_property(&prop, &properties).unwrap());
1606 }
1607
1608 #[test]
1609 fn test_is_date_with_invalid_date_format() {
1610 let prop = Property {
1611 key: "date".to_string(),
1612 value: json!("-7d"),
1613 operator: "is_date_before".to_string(),
1614 property_type: None,
1615 };
1616
1617 let mut properties = HashMap::new();
1618 properties.insert("date".to_string(), json!("not-a-date"));
1619
1620 let result = match_property(&prop, &properties);
1622 assert!(result.is_err());
1623 }
1624
1625 #[test]
1626 fn test_is_date_with_iso_datetime() {
1627 let prop = Property {
1628 key: "event_time".to_string(),
1629 value: json!("2024-06-15T10:30:00Z"),
1630 operator: "is_date_before".to_string(),
1631 property_type: None,
1632 };
1633
1634 let mut properties = HashMap::new();
1635 properties.insert("event_time".to_string(), json!("2024-06-15T08:00:00Z"));
1636 assert!(match_property(&prop, &properties).unwrap());
1637
1638 properties.insert("event_time".to_string(), json!("2024-06-15T12:00:00Z"));
1639 assert!(!match_property(&prop, &properties).unwrap());
1640 }
1641
1642 #[test]
1645 fn test_cohort_membership_in() {
1646 let mut cohorts = HashMap::new();
1648 cohorts.insert(
1649 "cohort_1".to_string(),
1650 CohortDefinition::new(
1651 "cohort_1".to_string(),
1652 vec![Property {
1653 key: "country".to_string(),
1654 value: json!("US"),
1655 operator: "exact".to_string(),
1656 property_type: None,
1657 }],
1658 ),
1659 );
1660
1661 let prop = Property {
1663 key: "$cohort".to_string(),
1664 value: json!("cohort_1"),
1665 operator: "in".to_string(),
1666 property_type: Some("cohort".to_string()),
1667 };
1668
1669 let mut properties = HashMap::new();
1671 properties.insert("country".to_string(), json!("US"));
1672
1673 let ctx = EvaluationContext {
1674 cohorts: &cohorts,
1675 flags: &HashMap::new(),
1676 distinct_id: "user-123",
1677 };
1678 assert!(match_property_with_context(&prop, &properties, &ctx).unwrap());
1679
1680 properties.insert("country".to_string(), json!("UK"));
1682 assert!(!match_property_with_context(&prop, &properties, &ctx).unwrap());
1683 }
1684
1685 #[test]
1686 fn test_cohort_membership_not_in() {
1687 let mut cohorts = HashMap::new();
1688 cohorts.insert(
1689 "cohort_blocked".to_string(),
1690 CohortDefinition::new(
1691 "cohort_blocked".to_string(),
1692 vec![Property {
1693 key: "status".to_string(),
1694 value: json!("blocked"),
1695 operator: "exact".to_string(),
1696 property_type: None,
1697 }],
1698 ),
1699 );
1700
1701 let prop = Property {
1702 key: "$cohort".to_string(),
1703 value: json!("cohort_blocked"),
1704 operator: "not_in".to_string(),
1705 property_type: Some("cohort".to_string()),
1706 };
1707
1708 let mut properties = HashMap::new();
1709 properties.insert("status".to_string(), json!("active"));
1710
1711 let ctx = EvaluationContext {
1712 cohorts: &cohorts,
1713 flags: &HashMap::new(),
1714 distinct_id: "user-123",
1715 };
1716 assert!(match_property_with_context(&prop, &properties, &ctx).unwrap());
1718
1719 properties.insert("status".to_string(), json!("blocked"));
1721 assert!(!match_property_with_context(&prop, &properties, &ctx).unwrap());
1722 }
1723
1724 #[test]
1725 fn test_cohort_not_found_returns_inconclusive() {
1726 let cohorts = HashMap::new(); let prop = Property {
1729 key: "$cohort".to_string(),
1730 value: json!("nonexistent_cohort"),
1731 operator: "in".to_string(),
1732 property_type: Some("cohort".to_string()),
1733 };
1734
1735 let properties = HashMap::new();
1736 let ctx = EvaluationContext {
1737 cohorts: &cohorts,
1738 flags: &HashMap::new(),
1739 distinct_id: "user-123",
1740 };
1741
1742 let result = match_property_with_context(&prop, &properties, &ctx);
1743 assert!(result.is_err());
1744 assert!(result.unwrap_err().message.contains("Cohort"));
1745 }
1746
1747 #[test]
1750 fn test_flag_dependency_enabled() {
1751 let mut flags = HashMap::new();
1752 flags.insert(
1753 "prerequisite-flag".to_string(),
1754 FeatureFlag {
1755 key: "prerequisite-flag".to_string(),
1756 active: true,
1757 filters: FeatureFlagFilters {
1758 groups: vec![FeatureFlagCondition {
1759 properties: vec![],
1760 rollout_percentage: Some(100.0),
1761 variant: None,
1762 }],
1763 multivariate: None,
1764 payloads: HashMap::new(),
1765 },
1766 },
1767 );
1768
1769 let prop = Property {
1771 key: "$feature/prerequisite-flag".to_string(),
1772 value: json!(true),
1773 operator: "exact".to_string(),
1774 property_type: None,
1775 };
1776
1777 let properties = HashMap::new();
1778 let ctx = EvaluationContext {
1779 cohorts: &HashMap::new(),
1780 flags: &flags,
1781 distinct_id: "user-123",
1782 };
1783
1784 assert!(match_property_with_context(&prop, &properties, &ctx).unwrap());
1786 }
1787
1788 #[test]
1789 fn test_flag_dependency_disabled() {
1790 let mut flags = HashMap::new();
1791 flags.insert(
1792 "disabled-flag".to_string(),
1793 FeatureFlag {
1794 key: "disabled-flag".to_string(),
1795 active: false, filters: FeatureFlagFilters {
1797 groups: vec![],
1798 multivariate: None,
1799 payloads: HashMap::new(),
1800 },
1801 },
1802 );
1803
1804 let prop = Property {
1806 key: "$feature/disabled-flag".to_string(),
1807 value: json!(true),
1808 operator: "exact".to_string(),
1809 property_type: None,
1810 };
1811
1812 let properties = HashMap::new();
1813 let ctx = EvaluationContext {
1814 cohorts: &HashMap::new(),
1815 flags: &flags,
1816 distinct_id: "user-123",
1817 };
1818
1819 assert!(!match_property_with_context(&prop, &properties, &ctx).unwrap());
1821 }
1822
1823 #[test]
1824 fn test_flag_dependency_variant_match() {
1825 let mut flags = HashMap::new();
1826 flags.insert(
1827 "ab-test-flag".to_string(),
1828 FeatureFlag {
1829 key: "ab-test-flag".to_string(),
1830 active: true,
1831 filters: FeatureFlagFilters {
1832 groups: vec![FeatureFlagCondition {
1833 properties: vec![],
1834 rollout_percentage: Some(100.0),
1835 variant: None,
1836 }],
1837 multivariate: Some(MultivariateFilter {
1838 variants: vec![
1839 MultivariateVariant {
1840 key: "control".to_string(),
1841 rollout_percentage: 50.0,
1842 },
1843 MultivariateVariant {
1844 key: "test".to_string(),
1845 rollout_percentage: 50.0,
1846 },
1847 ],
1848 }),
1849 payloads: HashMap::new(),
1850 },
1851 },
1852 );
1853
1854 let prop = Property {
1856 key: "$feature/ab-test-flag".to_string(),
1857 value: json!("control"),
1858 operator: "exact".to_string(),
1859 property_type: None,
1860 };
1861
1862 let properties = HashMap::new();
1863 let ctx = EvaluationContext {
1864 cohorts: &HashMap::new(),
1865 flags: &flags,
1866 distinct_id: "user-gets-control", };
1868
1869 let result = match_property_with_context(&prop, &properties, &ctx);
1871 assert!(result.is_ok());
1872 }
1873
1874 #[test]
1875 fn test_flag_dependency_not_found_returns_inconclusive() {
1876 let flags = HashMap::new(); let prop = Property {
1879 key: "$feature/nonexistent-flag".to_string(),
1880 value: json!(true),
1881 operator: "exact".to_string(),
1882 property_type: None,
1883 };
1884
1885 let properties = HashMap::new();
1886 let ctx = EvaluationContext {
1887 cohorts: &HashMap::new(),
1888 flags: &flags,
1889 distinct_id: "user-123",
1890 };
1891
1892 let result = match_property_with_context(&prop, &properties, &ctx);
1893 assert!(result.is_err());
1894 assert!(result.unwrap_err().message.contains("Flag"));
1895 }
1896
1897 #[test]
1900 fn test_parse_relative_date_edge_cases() {
1901 let prop = Property {
1903 key: "date".to_string(),
1904 value: json!("placeholder"),
1905 operator: "is_date_before".to_string(),
1906 property_type: None,
1907 };
1908
1909 let mut properties = HashMap::new();
1910 properties.insert("date".to_string(), json!("2024-01-01"));
1911
1912 let empty_prop = Property {
1914 value: json!(""),
1915 ..prop.clone()
1916 };
1917 assert!(match_property(&empty_prop, &properties).is_err());
1918
1919 let dash_prop = Property {
1921 value: json!("-"),
1922 ..prop.clone()
1923 };
1924 assert!(match_property(&dash_prop, &properties).is_err());
1925
1926 let no_unit_prop = Property {
1928 value: json!("-7"),
1929 ..prop.clone()
1930 };
1931 assert!(match_property(&no_unit_prop, &properties).is_err());
1932
1933 let no_number_prop = Property {
1935 value: json!("-d"),
1936 ..prop.clone()
1937 };
1938 assert!(match_property(&no_number_prop, &properties).is_err());
1939
1940 let invalid_unit_prop = Property {
1942 value: json!("-7x"),
1943 ..prop.clone()
1944 };
1945 assert!(match_property(&invalid_unit_prop, &properties).is_err());
1946 }
1947
1948 #[test]
1949 fn test_parse_relative_date_large_values() {
1950 let prop = Property {
1952 key: "created_at".to_string(),
1953 value: json!("-1000d"), operator: "is_date_before".to_string(),
1955 property_type: None,
1956 };
1957
1958 let mut properties = HashMap::new();
1959 let five_years_ago = chrono::Utc::now() - chrono::Duration::days(1825);
1961 properties.insert(
1962 "created_at".to_string(),
1963 json!(five_years_ago.format("%Y-%m-%d").to_string()),
1964 );
1965 assert!(match_property(&prop, &properties).unwrap());
1966 }
1967
1968 #[test]
1971 fn test_regex_with_invalid_pattern_returns_false() {
1972 let prop = Property {
1974 key: "email".to_string(),
1975 value: json!("(unclosed"),
1976 operator: "regex".to_string(),
1977 property_type: None,
1978 };
1979
1980 let mut properties = HashMap::new();
1981 properties.insert("email".to_string(), json!("test@example.com"));
1982
1983 assert!(!match_property(&prop, &properties).unwrap());
1985 }
1986
1987 #[test]
1988 fn test_not_regex_with_invalid_pattern_returns_true() {
1989 let prop = Property {
1991 key: "email".to_string(),
1992 value: json!("(unclosed"),
1993 operator: "not_regex".to_string(),
1994 property_type: None,
1995 };
1996
1997 let mut properties = HashMap::new();
1998 properties.insert("email".to_string(), json!("test@example.com"));
1999
2000 assert!(match_property(&prop, &properties).unwrap());
2002 }
2003
2004 #[test]
2005 fn test_regex_with_various_invalid_patterns() {
2006 let invalid_patterns = vec![
2007 "(unclosed", "[unclosed", "*invalid", "(?P<bad", r"\", ];
2013
2014 for pattern in invalid_patterns {
2015 let prop = Property {
2016 key: "value".to_string(),
2017 value: json!(pattern),
2018 operator: "regex".to_string(),
2019 property_type: None,
2020 };
2021
2022 let mut properties = HashMap::new();
2023 properties.insert("value".to_string(), json!("test"));
2024
2025 assert!(
2027 !match_property(&prop, &properties).unwrap(),
2028 "Invalid pattern '{}' should return false for regex",
2029 pattern
2030 );
2031
2032 let not_regex_prop = Property {
2034 operator: "not_regex".to_string(),
2035 ..prop
2036 };
2037 assert!(
2038 match_property(¬_regex_prop, &properties).unwrap(),
2039 "Invalid pattern '{}' should return true for not_regex",
2040 pattern
2041 );
2042 }
2043 }
2044
2045 #[test]
2048 fn test_parse_semver_basic() {
2049 assert_eq!(parse_semver("1.2.3"), Some((1, 2, 3)));
2050 assert_eq!(parse_semver("0.0.0"), Some((0, 0, 0)));
2051 assert_eq!(parse_semver("10.20.30"), Some((10, 20, 30)));
2052 }
2053
2054 #[test]
2055 fn test_parse_semver_v_prefix() {
2056 assert_eq!(parse_semver("v1.2.3"), Some((1, 2, 3)));
2057 assert_eq!(parse_semver("V1.2.3"), Some((1, 2, 3)));
2058 }
2059
2060 #[test]
2061 fn test_parse_semver_whitespace() {
2062 assert_eq!(parse_semver(" 1.2.3 "), Some((1, 2, 3)));
2063 assert_eq!(parse_semver(" v1.2.3 "), Some((1, 2, 3)));
2064 }
2065
2066 #[test]
2067 fn test_parse_semver_prerelease_stripped() {
2068 assert_eq!(parse_semver("1.2.3-alpha"), Some((1, 2, 3)));
2069 assert_eq!(parse_semver("1.2.3-beta.1"), Some((1, 2, 3)));
2070 assert_eq!(parse_semver("1.2.3-rc.1+build.123"), Some((1, 2, 3)));
2071 assert_eq!(parse_semver("1.2.3+build.456"), Some((1, 2, 3)));
2072 }
2073
2074 #[test]
2075 fn test_parse_semver_partial_versions() {
2076 assert_eq!(parse_semver("1.2"), Some((1, 2, 0)));
2077 assert_eq!(parse_semver("1"), Some((1, 0, 0)));
2078 assert_eq!(parse_semver("v1.2"), Some((1, 2, 0)));
2079 }
2080
2081 #[test]
2082 fn test_parse_semver_extra_components_ignored() {
2083 assert_eq!(parse_semver("1.2.3.4"), Some((1, 2, 3)));
2084 assert_eq!(parse_semver("1.2.3.4.5.6"), Some((1, 2, 3)));
2085 }
2086
2087 #[test]
2088 fn test_parse_semver_leading_zeros() {
2089 assert_eq!(parse_semver("01.02.03"), Some((1, 2, 3)));
2090 assert_eq!(parse_semver("001.002.003"), Some((1, 2, 3)));
2091 }
2092
2093 #[test]
2094 fn test_parse_semver_invalid() {
2095 assert_eq!(parse_semver(""), None);
2096 assert_eq!(parse_semver(" "), None);
2097 assert_eq!(parse_semver("v"), None);
2098 assert_eq!(parse_semver(".1.2.3"), None);
2099 assert_eq!(parse_semver("abc"), None);
2100 assert_eq!(parse_semver("1.abc.3"), None);
2101 assert_eq!(parse_semver("1.2.abc"), None);
2102 assert_eq!(parse_semver("not-a-version"), None);
2103 }
2104
2105 #[test]
2108 fn test_semver_eq_basic() {
2109 let prop = Property {
2110 key: "version".to_string(),
2111 value: json!("1.2.3"),
2112 operator: "semver_eq".to_string(),
2113 property_type: None,
2114 };
2115
2116 let mut properties = HashMap::new();
2117
2118 properties.insert("version".to_string(), json!("1.2.3"));
2119 assert!(match_property(&prop, &properties).unwrap());
2120
2121 properties.insert("version".to_string(), json!("1.2.4"));
2122 assert!(!match_property(&prop, &properties).unwrap());
2123
2124 properties.insert("version".to_string(), json!("1.3.3"));
2125 assert!(!match_property(&prop, &properties).unwrap());
2126
2127 properties.insert("version".to_string(), json!("2.2.3"));
2128 assert!(!match_property(&prop, &properties).unwrap());
2129 }
2130
2131 #[test]
2132 fn test_semver_eq_with_v_prefix() {
2133 let prop = Property {
2134 key: "version".to_string(),
2135 value: json!("1.2.3"),
2136 operator: "semver_eq".to_string(),
2137 property_type: None,
2138 };
2139
2140 let mut properties = HashMap::new();
2141
2142 properties.insert("version".to_string(), json!("v1.2.3"));
2144 assert!(match_property(&prop, &properties).unwrap());
2145
2146 let prop_with_v = Property {
2148 value: json!("v1.2.3"),
2149 ..prop.clone()
2150 };
2151 properties.insert("version".to_string(), json!("1.2.3"));
2152 assert!(match_property(&prop_with_v, &properties).unwrap());
2153 }
2154
2155 #[test]
2156 fn test_semver_eq_prerelease_stripped() {
2157 let prop = Property {
2158 key: "version".to_string(),
2159 value: json!("1.2.3"),
2160 operator: "semver_eq".to_string(),
2161 property_type: None,
2162 };
2163
2164 let mut properties = HashMap::new();
2165
2166 properties.insert("version".to_string(), json!("1.2.3-alpha"));
2167 assert!(match_property(&prop, &properties).unwrap());
2168
2169 properties.insert("version".to_string(), json!("1.2.3-beta.1"));
2170 assert!(match_property(&prop, &properties).unwrap());
2171
2172 properties.insert("version".to_string(), json!("1.2.3+build.456"));
2173 assert!(match_property(&prop, &properties).unwrap());
2174 }
2175
2176 #[test]
2177 fn test_semver_eq_partial_versions() {
2178 let prop = Property {
2179 key: "version".to_string(),
2180 value: json!("1.2.0"),
2181 operator: "semver_eq".to_string(),
2182 property_type: None,
2183 };
2184
2185 let mut properties = HashMap::new();
2186
2187 properties.insert("version".to_string(), json!("1.2"));
2189 assert!(match_property(&prop, &properties).unwrap());
2190
2191 let partial_prop = Property {
2193 value: json!("1.2"),
2194 ..prop.clone()
2195 };
2196 properties.insert("version".to_string(), json!("1.2.0"));
2197 assert!(match_property(&partial_prop, &properties).unwrap());
2198 }
2199
2200 #[test]
2201 fn test_semver_neq() {
2202 let prop = Property {
2203 key: "version".to_string(),
2204 value: json!("1.2.3"),
2205 operator: "semver_neq".to_string(),
2206 property_type: None,
2207 };
2208
2209 let mut properties = HashMap::new();
2210
2211 properties.insert("version".to_string(), json!("1.2.3"));
2212 assert!(!match_property(&prop, &properties).unwrap());
2213
2214 properties.insert("version".to_string(), json!("1.2.4"));
2215 assert!(match_property(&prop, &properties).unwrap());
2216
2217 properties.insert("version".to_string(), json!("2.0.0"));
2218 assert!(match_property(&prop, &properties).unwrap());
2219 }
2220
2221 #[test]
2224 fn test_semver_gt() {
2225 let prop = Property {
2226 key: "version".to_string(),
2227 value: json!("1.2.3"),
2228 operator: "semver_gt".to_string(),
2229 property_type: None,
2230 };
2231
2232 let mut properties = HashMap::new();
2233
2234 properties.insert("version".to_string(), json!("1.2.4"));
2236 assert!(match_property(&prop, &properties).unwrap());
2237
2238 properties.insert("version".to_string(), json!("1.3.0"));
2239 assert!(match_property(&prop, &properties).unwrap());
2240
2241 properties.insert("version".to_string(), json!("2.0.0"));
2242 assert!(match_property(&prop, &properties).unwrap());
2243
2244 properties.insert("version".to_string(), json!("1.2.3"));
2246 assert!(!match_property(&prop, &properties).unwrap());
2247
2248 properties.insert("version".to_string(), json!("1.2.2"));
2250 assert!(!match_property(&prop, &properties).unwrap());
2251
2252 properties.insert("version".to_string(), json!("1.1.9"));
2253 assert!(!match_property(&prop, &properties).unwrap());
2254
2255 properties.insert("version".to_string(), json!("0.9.9"));
2256 assert!(!match_property(&prop, &properties).unwrap());
2257 }
2258
2259 #[test]
2260 fn test_semver_gte() {
2261 let prop = Property {
2262 key: "version".to_string(),
2263 value: json!("1.2.3"),
2264 operator: "semver_gte".to_string(),
2265 property_type: None,
2266 };
2267
2268 let mut properties = HashMap::new();
2269
2270 properties.insert("version".to_string(), json!("1.2.4"));
2272 assert!(match_property(&prop, &properties).unwrap());
2273
2274 properties.insert("version".to_string(), json!("2.0.0"));
2275 assert!(match_property(&prop, &properties).unwrap());
2276
2277 properties.insert("version".to_string(), json!("1.2.3"));
2279 assert!(match_property(&prop, &properties).unwrap());
2280
2281 properties.insert("version".to_string(), json!("1.2.2"));
2283 assert!(!match_property(&prop, &properties).unwrap());
2284
2285 properties.insert("version".to_string(), json!("0.9.9"));
2286 assert!(!match_property(&prop, &properties).unwrap());
2287 }
2288
2289 #[test]
2290 fn test_semver_lt() {
2291 let prop = Property {
2292 key: "version".to_string(),
2293 value: json!("1.2.3"),
2294 operator: "semver_lt".to_string(),
2295 property_type: None,
2296 };
2297
2298 let mut properties = HashMap::new();
2299
2300 properties.insert("version".to_string(), json!("1.2.2"));
2302 assert!(match_property(&prop, &properties).unwrap());
2303
2304 properties.insert("version".to_string(), json!("1.1.9"));
2305 assert!(match_property(&prop, &properties).unwrap());
2306
2307 properties.insert("version".to_string(), json!("0.9.9"));
2308 assert!(match_property(&prop, &properties).unwrap());
2309
2310 properties.insert("version".to_string(), json!("1.2.3"));
2312 assert!(!match_property(&prop, &properties).unwrap());
2313
2314 properties.insert("version".to_string(), json!("1.2.4"));
2316 assert!(!match_property(&prop, &properties).unwrap());
2317
2318 properties.insert("version".to_string(), json!("2.0.0"));
2319 assert!(!match_property(&prop, &properties).unwrap());
2320 }
2321
2322 #[test]
2323 fn test_semver_lte() {
2324 let prop = Property {
2325 key: "version".to_string(),
2326 value: json!("1.2.3"),
2327 operator: "semver_lte".to_string(),
2328 property_type: None,
2329 };
2330
2331 let mut properties = HashMap::new();
2332
2333 properties.insert("version".to_string(), json!("1.2.2"));
2335 assert!(match_property(&prop, &properties).unwrap());
2336
2337 properties.insert("version".to_string(), json!("0.9.9"));
2338 assert!(match_property(&prop, &properties).unwrap());
2339
2340 properties.insert("version".to_string(), json!("1.2.3"));
2342 assert!(match_property(&prop, &properties).unwrap());
2343
2344 properties.insert("version".to_string(), json!("1.2.4"));
2346 assert!(!match_property(&prop, &properties).unwrap());
2347
2348 properties.insert("version".to_string(), json!("2.0.0"));
2349 assert!(!match_property(&prop, &properties).unwrap());
2350 }
2351
2352 #[test]
2355 fn test_semver_tilde_basic() {
2356 let prop = Property {
2358 key: "version".to_string(),
2359 value: json!("1.2.3"),
2360 operator: "semver_tilde".to_string(),
2361 property_type: None,
2362 };
2363
2364 let mut properties = HashMap::new();
2365
2366 properties.insert("version".to_string(), json!("1.2.3"));
2368 assert!(match_property(&prop, &properties).unwrap());
2369
2370 properties.insert("version".to_string(), json!("1.2.4"));
2372 assert!(match_property(&prop, &properties).unwrap());
2373
2374 properties.insert("version".to_string(), json!("1.2.99"));
2375 assert!(match_property(&prop, &properties).unwrap());
2376
2377 properties.insert("version".to_string(), json!("1.3.0"));
2379 assert!(!match_property(&prop, &properties).unwrap());
2380
2381 properties.insert("version".to_string(), json!("1.3.1"));
2383 assert!(!match_property(&prop, &properties).unwrap());
2384
2385 properties.insert("version".to_string(), json!("2.0.0"));
2386 assert!(!match_property(&prop, &properties).unwrap());
2387
2388 properties.insert("version".to_string(), json!("1.2.2"));
2390 assert!(!match_property(&prop, &properties).unwrap());
2391
2392 properties.insert("version".to_string(), json!("1.1.9"));
2393 assert!(!match_property(&prop, &properties).unwrap());
2394 }
2395
2396 #[test]
2397 fn test_semver_tilde_zero_versions() {
2398 let prop = Property {
2400 key: "version".to_string(),
2401 value: json!("0.2.3"),
2402 operator: "semver_tilde".to_string(),
2403 property_type: None,
2404 };
2405
2406 let mut properties = HashMap::new();
2407
2408 properties.insert("version".to_string(), json!("0.2.3"));
2409 assert!(match_property(&prop, &properties).unwrap());
2410
2411 properties.insert("version".to_string(), json!("0.2.9"));
2412 assert!(match_property(&prop, &properties).unwrap());
2413
2414 properties.insert("version".to_string(), json!("0.3.0"));
2415 assert!(!match_property(&prop, &properties).unwrap());
2416
2417 properties.insert("version".to_string(), json!("0.2.2"));
2418 assert!(!match_property(&prop, &properties).unwrap());
2419 }
2420
2421 #[test]
2424 fn test_semver_caret_major_nonzero() {
2425 let prop = Property {
2427 key: "version".to_string(),
2428 value: json!("1.2.3"),
2429 operator: "semver_caret".to_string(),
2430 property_type: None,
2431 };
2432
2433 let mut properties = HashMap::new();
2434
2435 properties.insert("version".to_string(), json!("1.2.3"));
2437 assert!(match_property(&prop, &properties).unwrap());
2438
2439 properties.insert("version".to_string(), json!("1.2.4"));
2441 assert!(match_property(&prop, &properties).unwrap());
2442
2443 properties.insert("version".to_string(), json!("1.3.0"));
2444 assert!(match_property(&prop, &properties).unwrap());
2445
2446 properties.insert("version".to_string(), json!("1.99.99"));
2447 assert!(match_property(&prop, &properties).unwrap());
2448
2449 properties.insert("version".to_string(), json!("2.0.0"));
2451 assert!(!match_property(&prop, &properties).unwrap());
2452
2453 properties.insert("version".to_string(), json!("2.0.1"));
2455 assert!(!match_property(&prop, &properties).unwrap());
2456
2457 properties.insert("version".to_string(), json!("1.2.2"));
2459 assert!(!match_property(&prop, &properties).unwrap());
2460
2461 properties.insert("version".to_string(), json!("0.9.9"));
2462 assert!(!match_property(&prop, &properties).unwrap());
2463 }
2464
2465 #[test]
2466 fn test_semver_caret_major_zero_minor_nonzero() {
2467 let prop = Property {
2469 key: "version".to_string(),
2470 value: json!("0.2.3"),
2471 operator: "semver_caret".to_string(),
2472 property_type: None,
2473 };
2474
2475 let mut properties = HashMap::new();
2476
2477 properties.insert("version".to_string(), json!("0.2.3"));
2479 assert!(match_property(&prop, &properties).unwrap());
2480
2481 properties.insert("version".to_string(), json!("0.2.4"));
2483 assert!(match_property(&prop, &properties).unwrap());
2484
2485 properties.insert("version".to_string(), json!("0.2.99"));
2486 assert!(match_property(&prop, &properties).unwrap());
2487
2488 properties.insert("version".to_string(), json!("0.3.0"));
2490 assert!(!match_property(&prop, &properties).unwrap());
2491
2492 properties.insert("version".to_string(), json!("0.3.1"));
2494 assert!(!match_property(&prop, &properties).unwrap());
2495
2496 properties.insert("version".to_string(), json!("1.0.0"));
2497 assert!(!match_property(&prop, &properties).unwrap());
2498
2499 properties.insert("version".to_string(), json!("0.2.2"));
2501 assert!(!match_property(&prop, &properties).unwrap());
2502
2503 properties.insert("version".to_string(), json!("0.1.9"));
2504 assert!(!match_property(&prop, &properties).unwrap());
2505 }
2506
2507 #[test]
2508 fn test_semver_caret_major_zero_minor_zero() {
2509 let prop = Property {
2511 key: "version".to_string(),
2512 value: json!("0.0.3"),
2513 operator: "semver_caret".to_string(),
2514 property_type: None,
2515 };
2516
2517 let mut properties = HashMap::new();
2518
2519 properties.insert("version".to_string(), json!("0.0.3"));
2521 assert!(match_property(&prop, &properties).unwrap());
2522
2523 properties.insert("version".to_string(), json!("0.0.4"));
2525 assert!(!match_property(&prop, &properties).unwrap());
2526
2527 properties.insert("version".to_string(), json!("0.0.5"));
2529 assert!(!match_property(&prop, &properties).unwrap());
2530
2531 properties.insert("version".to_string(), json!("0.1.0"));
2532 assert!(!match_property(&prop, &properties).unwrap());
2533
2534 properties.insert("version".to_string(), json!("0.0.2"));
2536 assert!(!match_property(&prop, &properties).unwrap());
2537 }
2538
2539 #[test]
2542 fn test_semver_wildcard_major() {
2543 let prop = Property {
2545 key: "version".to_string(),
2546 value: json!("1.*"),
2547 operator: "semver_wildcard".to_string(),
2548 property_type: None,
2549 };
2550
2551 let mut properties = HashMap::new();
2552
2553 properties.insert("version".to_string(), json!("1.0.0"));
2555 assert!(match_property(&prop, &properties).unwrap());
2556
2557 properties.insert("version".to_string(), json!("1.2.3"));
2559 assert!(match_property(&prop, &properties).unwrap());
2560
2561 properties.insert("version".to_string(), json!("1.99.99"));
2562 assert!(match_property(&prop, &properties).unwrap());
2563
2564 properties.insert("version".to_string(), json!("2.0.0"));
2566 assert!(!match_property(&prop, &properties).unwrap());
2567
2568 properties.insert("version".to_string(), json!("2.0.1"));
2570 assert!(!match_property(&prop, &properties).unwrap());
2571
2572 properties.insert("version".to_string(), json!("0.9.9"));
2574 assert!(!match_property(&prop, &properties).unwrap());
2575 }
2576
2577 #[test]
2578 fn test_semver_wildcard_minor() {
2579 let prop = Property {
2581 key: "version".to_string(),
2582 value: json!("1.2.*"),
2583 operator: "semver_wildcard".to_string(),
2584 property_type: None,
2585 };
2586
2587 let mut properties = HashMap::new();
2588
2589 properties.insert("version".to_string(), json!("1.2.0"));
2591 assert!(match_property(&prop, &properties).unwrap());
2592
2593 properties.insert("version".to_string(), json!("1.2.3"));
2595 assert!(match_property(&prop, &properties).unwrap());
2596
2597 properties.insert("version".to_string(), json!("1.2.99"));
2598 assert!(match_property(&prop, &properties).unwrap());
2599
2600 properties.insert("version".to_string(), json!("1.3.0"));
2602 assert!(!match_property(&prop, &properties).unwrap());
2603
2604 properties.insert("version".to_string(), json!("1.3.1"));
2606 assert!(!match_property(&prop, &properties).unwrap());
2607
2608 properties.insert("version".to_string(), json!("2.0.0"));
2609 assert!(!match_property(&prop, &properties).unwrap());
2610
2611 properties.insert("version".to_string(), json!("1.1.9"));
2613 assert!(!match_property(&prop, &properties).unwrap());
2614 }
2615
2616 #[test]
2617 fn test_semver_wildcard_zero() {
2618 let prop = Property {
2620 key: "version".to_string(),
2621 value: json!("0.*"),
2622 operator: "semver_wildcard".to_string(),
2623 property_type: None,
2624 };
2625
2626 let mut properties = HashMap::new();
2627
2628 properties.insert("version".to_string(), json!("0.0.0"));
2629 assert!(match_property(&prop, &properties).unwrap());
2630
2631 properties.insert("version".to_string(), json!("0.99.99"));
2632 assert!(match_property(&prop, &properties).unwrap());
2633
2634 properties.insert("version".to_string(), json!("1.0.0"));
2635 assert!(!match_property(&prop, &properties).unwrap());
2636 }
2637
2638 #[test]
2641 fn test_semver_invalid_property_value() {
2642 let prop = Property {
2643 key: "version".to_string(),
2644 value: json!("1.2.3"),
2645 operator: "semver_eq".to_string(),
2646 property_type: None,
2647 };
2648
2649 let mut properties = HashMap::new();
2650
2651 properties.insert("version".to_string(), json!("not-a-version"));
2653 assert!(match_property(&prop, &properties).is_err());
2654
2655 properties.insert("version".to_string(), json!(""));
2656 assert!(match_property(&prop, &properties).is_err());
2657
2658 properties.insert("version".to_string(), json!(".1.2.3"));
2659 assert!(match_property(&prop, &properties).is_err());
2660
2661 properties.insert("version".to_string(), json!("abc.def.ghi"));
2662 assert!(match_property(&prop, &properties).is_err());
2663 }
2664
2665 #[test]
2666 fn test_semver_invalid_target_value() {
2667 let mut properties = HashMap::new();
2668 properties.insert("version".to_string(), json!("1.2.3"));
2669
2670 let prop = Property {
2672 key: "version".to_string(),
2673 value: json!("not-valid"),
2674 operator: "semver_eq".to_string(),
2675 property_type: None,
2676 };
2677 assert!(match_property(&prop, &properties).is_err());
2678
2679 let prop = Property {
2680 key: "version".to_string(),
2681 value: json!(""),
2682 operator: "semver_gt".to_string(),
2683 property_type: None,
2684 };
2685 assert!(match_property(&prop, &properties).is_err());
2686 }
2687
2688 #[test]
2689 fn test_semver_invalid_wildcard_pattern() {
2690 let mut properties = HashMap::new();
2691 properties.insert("version".to_string(), json!("1.2.3"));
2692
2693 let invalid_patterns = vec![
2695 "*", "*.2.3", "1.*.3", "1.2.3.*", "abc.*", ];
2701
2702 for pattern in invalid_patterns {
2703 let prop = Property {
2704 key: "version".to_string(),
2705 value: json!(pattern),
2706 operator: "semver_wildcard".to_string(),
2707 property_type: None,
2708 };
2709 assert!(
2710 match_property(&prop, &properties).is_err(),
2711 "Pattern '{}' should be invalid",
2712 pattern
2713 );
2714 }
2715 }
2716
2717 #[test]
2718 fn test_semver_missing_property() {
2719 let prop = Property {
2720 key: "version".to_string(),
2721 value: json!("1.2.3"),
2722 operator: "semver_eq".to_string(),
2723 property_type: None,
2724 };
2725
2726 let properties = HashMap::new(); assert!(match_property(&prop, &properties).is_err());
2728 }
2729
2730 #[test]
2731 fn test_semver_null_property_value() {
2732 let prop = Property {
2733 key: "version".to_string(),
2734 value: json!("1.2.3"),
2735 operator: "semver_eq".to_string(),
2736 property_type: None,
2737 };
2738
2739 let mut properties = HashMap::new();
2740 properties.insert("version".to_string(), json!(null));
2741
2742 assert!(match_property(&prop, &properties).is_err());
2744 }
2745
2746 #[test]
2747 fn test_semver_numeric_property_value() {
2748 let prop = Property {
2750 key: "version".to_string(),
2751 value: json!("1.0.0"),
2752 operator: "semver_eq".to_string(),
2753 property_type: None,
2754 };
2755
2756 let mut properties = HashMap::new();
2757 properties.insert("version".to_string(), json!(1));
2759 assert!(match_property(&prop, &properties).unwrap());
2760 }
2761
2762 #[test]
2765 fn test_semver_four_part_versions() {
2766 let prop = Property {
2767 key: "version".to_string(),
2768 value: json!("1.2.3.4"),
2769 operator: "semver_eq".to_string(),
2770 property_type: None,
2771 };
2772
2773 let mut properties = HashMap::new();
2774
2775 properties.insert("version".to_string(), json!("1.2.3"));
2777 assert!(match_property(&prop, &properties).unwrap());
2778
2779 properties.insert("version".to_string(), json!("1.2.3.4"));
2780 assert!(match_property(&prop, &properties).unwrap());
2781
2782 properties.insert("version".to_string(), json!("1.2.3.999"));
2783 assert!(match_property(&prop, &properties).unwrap());
2784 }
2785
2786 #[test]
2787 fn test_semver_large_version_numbers() {
2788 let prop = Property {
2789 key: "version".to_string(),
2790 value: json!("1000.2000.3000"),
2791 operator: "semver_eq".to_string(),
2792 property_type: None,
2793 };
2794
2795 let mut properties = HashMap::new();
2796 properties.insert("version".to_string(), json!("1000.2000.3000"));
2797 assert!(match_property(&prop, &properties).unwrap());
2798 }
2799
2800 #[test]
2801 fn test_semver_comparison_ordering() {
2802 let cases = vec![
2804 ("0.0.1", "0.0.2", "semver_lt", true),
2805 ("0.1.0", "0.0.99", "semver_gt", true),
2806 ("1.0.0", "0.99.99", "semver_gt", true),
2807 ("1.0.0", "1.0.0", "semver_eq", true),
2808 ("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), ];
2813
2814 for (prop_val, target_val, op, expected) in cases {
2815 let prop = Property {
2816 key: "version".to_string(),
2817 value: json!(target_val),
2818 operator: op.to_string(),
2819 property_type: None,
2820 };
2821
2822 let mut properties = HashMap::new();
2823 properties.insert("version".to_string(), json!(prop_val));
2824
2825 assert_eq!(
2826 match_property(&prop, &properties).unwrap(),
2827 expected,
2828 "{} {} {} should be {}",
2829 prop_val,
2830 op,
2831 target_val,
2832 expected
2833 );
2834 }
2835 }
2836}