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