1use std::hash::BuildHasher;
4use std::io::Cursor;
5use std::net::IpAddr;
6use std::{collections::hash_map::HashMap, str::FromStr};
7use std::{collections::hash_set::HashSet, fmt::Display};
8
9use chrono::{DateTime, Utc};
10use ipnet::IpNet;
11use log::{trace, warn};
12use murmur3::murmur3_32;
13use rand::Rng;
14use semver::Version;
15
16use crate::api::{Constraint, ConstraintExpression};
17use crate::context::Context;
18
19pub type Strategy =
21 Box<dyn Fn(Option<HashMap<String, String>>) -> Evaluate + Sync + Send + 'static>;
22pub trait Evaluator: Fn(&Context) -> bool {
24 fn clone_boxed(&self) -> Box<dyn Evaluator + Send + Sync + 'static>;
25}
26pub type Evaluate = Box<dyn Evaluator + Send + Sync + 'static>;
27
28impl<T> Evaluator for T
29where
30 T: 'static + Clone + Sync + Send + Fn(&Context) -> bool,
31{
32 fn clone_boxed(&self) -> Box<dyn Evaluator + Send + Sync + 'static> {
33 Box::new(T::clone(self))
34 }
35}
36
37impl Clone for Box<dyn Evaluator + Send + Sync + 'static> {
38 fn clone(&self) -> Self {
39 self.as_ref().clone_boxed()
40 }
41}
42
43pub fn default<S: BuildHasher>(_: Option<HashMap<String, String, S>>) -> Evaluate {
45 Box::new(|_: &Context| -> bool { true })
46}
47
48pub fn user_with_id<S: BuildHasher>(parameters: Option<HashMap<String, String, S>>) -> Evaluate {
51 let mut uids: HashSet<String> = HashSet::new();
52 if let Some(parameters) = parameters {
53 if let Some(uids_list) = parameters.get("userIds") {
54 for uid in uids_list.split(',') {
55 uids.insert(uid.trim().into());
56 }
57 }
58 }
59 Box::new(move |context: &Context| -> bool {
60 context
61 .user_id
62 .as_ref()
63 .map(|uid| uids.contains(uid))
64 .unwrap_or(false)
65 })
66}
67
68pub fn group_and_rollout<S: BuildHasher>(
76 parameters: &Option<HashMap<String, String, S>>,
77 rollout_key: &str,
78) -> (String, u32) {
79 let parameters = if let Some(parameters) = parameters {
80 parameters
81 } else {
82 return ("".into(), 0);
83 };
84 let group = if let Some(group) = parameters.get("groupId") {
85 group.to_string()
86 } else {
87 "".into()
88 };
89
90 let mut rollout = 0;
91 if let Some(rollout_str) = parameters.get(rollout_key) {
92 if let Ok(percent) = rollout_str.parse::<u32>() {
93 rollout = percent
94 }
95 }
96 (group, rollout)
97}
98
99pub fn partial_rollout(group: &str, variable: Option<&String>, rollout: u32) -> bool {
101 let variable = if let Some(variable) = variable {
102 variable
103 } else {
104 return false;
105 };
106 match rollout {
107 0 => false,
109 100 => true,
110 rollout => {
111 if let Ok(normalised) = normalised_hash(group, variable, 100) {
112 rollout >= normalised
113 } else {
114 false
115 }
116 }
117 }
118}
119
120pub fn normalised_hash(group: &str, identifier: &str, modulus: u32) -> std::io::Result<u32> {
124 normalised_hash_internal(group, identifier, modulus, 0)
125}
126
127const VARIANT_NORMALIZATION_SEED: u32 = 86028157;
128
129pub fn normalised_variant_hash(
134 group: &str,
135 identifier: &str,
136 modulus: u32,
137) -> std::io::Result<u32> {
138 normalised_hash_internal(group, identifier, modulus, VARIANT_NORMALIZATION_SEED)
139}
140
141fn normalised_hash_internal(
142 group: &str,
143 identifier: &str,
144 modulus: u32,
145 seed: u32,
146) -> std::io::Result<u32> {
147 let mut reader = Cursor::new(format!("{}:{}", &group, &identifier));
153 murmur3_32(&mut reader, seed).map(|hash_result| hash_result % modulus + 1)
154}
155
156fn _session_id<S: BuildHasher>(
159 parameters: Option<HashMap<String, String, S>>,
160 rollout_key: &str,
161) -> Evaluate {
162 let (group, rollout) = group_and_rollout(¶meters, rollout_key);
163 Box::new(move |context: &Context| -> bool {
164 partial_rollout(&group, context.session_id.as_ref(), rollout)
165 })
166}
167
168fn _user_id<S: BuildHasher>(
171 parameters: Option<HashMap<String, String, S>>,
172 rollout_key: &str,
173) -> Evaluate {
174 let (group, rollout) = group_and_rollout(¶meters, rollout_key);
175 Box::new(move |context: &Context| -> bool {
176 partial_rollout(&group, context.user_id.as_ref(), rollout)
177 })
178}
179
180pub fn flexible_rollout<S: BuildHasher>(
185 parameters: Option<HashMap<String, String, S>>,
186) -> Evaluate {
187 let unwrapped_parameters = if let Some(parameters) = ¶meters {
188 parameters
189 } else {
190 return Box::new(|_| false);
191 };
192 match if let Some(stickiness) = unwrapped_parameters.get("stickiness") {
193 stickiness.as_str()
194 } else {
195 return Box::new(|_| false);
196 } {
197 "default" => {
198 let (group, rollout) = group_and_rollout(¶meters, "rollout");
200 Box::new(move |context: &Context| -> bool {
201 if context.user_id.is_some() {
202 partial_rollout(&group, context.user_id.as_ref(), rollout)
203 } else if context.session_id.is_some() {
204 partial_rollout(&group, context.session_id.as_ref(), rollout)
205 } else {
206 pick_random(rollout as u8)
207 }
208 })
209 }
210 "userId" => _user_id(parameters, "rollout"),
211 "sessionId" => _session_id(parameters, "rollout"),
212 "random" => _random(parameters, "rollout"),
213 _ => Box::new(|_| false),
214 }
215}
216
217pub fn user_id<S: BuildHasher>(parameters: Option<HashMap<String, String, S>>) -> Evaluate {
221 _user_id(parameters, "percentage")
222}
223
224pub fn session_id<S: BuildHasher>(parameters: Option<HashMap<String, String, S>>) -> Evaluate {
228 _session_id(parameters, "percentage")
229}
230
231fn pick_random(pct: u8) -> bool {
233 match pct {
234 0 => false,
235 100 => true,
236 pct => {
237 let mut rng = rand::rng();
238 let picked = rng.random_range(0..100);
240 pct > picked
241 }
242 }
243}
244
245pub fn _random<S: BuildHasher>(
248 parameters: Option<HashMap<String, String, S>>,
249 rollout_key: &str,
250) -> Evaluate {
251 let mut pct = 0;
252 if let Some(parameters) = parameters {
253 if let Some(pct_str) = parameters.get(rollout_key) {
254 if let Ok(percent) = pct_str.parse::<u8>() {
255 pct = percent
256 }
257 }
258 }
259 Box::new(move |_: &Context| -> bool { pick_random(pct) })
260}
261
262pub fn random<S: BuildHasher>(parameters: Option<HashMap<String, String, S>>) -> Evaluate {
265 _random(parameters, "percentage")
266}
267
268pub fn remote_address<S: BuildHasher>(parameters: Option<HashMap<String, String, S>>) -> Evaluate {
271 let mut ips: Vec<IpNet> = Vec::new();
274
275 if let Some(parameters) = parameters {
276 if let Some(ips_str) = parameters.get("IPs") {
277 for ip_str in ips_str.split(',') {
278 let ip_parsed = _parse_ip(ip_str.trim());
279 if let Ok(ip) = ip_parsed {
280 ips.push(ip)
281 }
282 }
283 }
284 }
285
286 Box::new(move |context: &Context| -> bool {
287 if let Some(remote_address) = &context.remote_address {
288 for ip in &ips {
289 if ip.contains(&remote_address.0) {
290 return true;
291 }
292 }
293 }
294 false
295 })
296}
297
298pub fn hostname<S: BuildHasher>(parameters: Option<HashMap<String, String, S>>) -> Evaluate {
301 let mut result = false;
302 hostname::get().ok().and_then(|this_hostname| {
303 parameters.map(|parameters| {
304 parameters.get("hostNames").map(|hostnames: &String| {
305 for hostname in hostnames.split(',') {
306 if this_hostname == hostname.trim() {
307 result = true;
308 }
309 }
310 false
311 })
312 })
313 });
314
315 Box::new(move |_: &Context| -> bool { result })
316}
317
318fn lower_case_if<S: Display>(case_insensitive: bool) -> impl Fn(S) -> String {
319 move |s| {
320 if case_insensitive {
321 s.to_string().to_lowercase()
322 } else {
323 s.to_string()
324 }
325 }
326}
327
328fn handle_parsable_op<T, C, F>(getter: F, compare_fn: C) -> Evaluate
329where
330 T: FromStr,
331 C: Fn(T) -> bool + Clone + Sync + Send + 'static,
332 F: Fn(&Context) -> Option<&String> + Clone + Sync + Send + 'static,
333{
334 Box::new(move |context: &Context| {
335 getter(context)
336 .and_then(|v| v.parse::<T>().ok())
337 .map(&compare_fn)
338 .unwrap_or(false)
339 })
340}
341
342fn handle_str_op<T, C, F>(
343 values: Vec<String>,
344 getter: F,
345 case_insensitive: bool,
346 compare_fn: C,
347) -> Evaluate
348where
349 T: Display,
350 C: Fn(&String, &String) -> bool + Clone + Sync + Send + 'static,
351 F: Fn(&Context) -> Option<&T> + Clone + Sync + Send + 'static,
352{
353 let as_vec: Vec<String> = values.iter().map(lower_case_if(case_insensitive)).collect();
354 Box::new(move |context: &Context| {
355 getter(context)
356 .map(lower_case_if(case_insensitive))
357 .map(|v| as_vec.iter().any(|entry| compare_fn(&v, entry)))
358 .unwrap_or(false)
359 })
360}
361
362fn _compile_constraint_string<F, B>(
364 expression: ConstraintExpression,
365 apply_invert: B,
366 case_insensitive: bool,
367 getter: F,
368) -> Evaluate
369where
370 F: Fn(&Context) -> Option<&String> + Clone + Sync + Send + 'static,
371 B: Fn(bool) -> bool + Sync + Send + Clone + 'static,
372{
373 let compiled_fn: Box<dyn Evaluator + Send + Sync + 'static> = match expression {
374 ConstraintExpression::In { values } => {
375 let as_set: HashSet<String> = values.iter().cloned().collect();
376
377 Box::new(move |context: &Context| {
378 getter(context).map(|v| as_set.contains(v)).unwrap_or(false)
379 })
380 }
381 ConstraintExpression::NotIn { values } => {
382 if values.is_empty() {
383 Box::new(|_| true)
384 } else {
385 let as_set: HashSet<String> = values.iter().cloned().collect();
386 Box::new(move |context: &Context| {
387 getter(context).map(|v| !as_set.contains(v)).unwrap_or(true)
388 })
389 }
390 }
391 ConstraintExpression::StrContains { values } => {
392 handle_str_op(values, getter, case_insensitive, |v, entry| {
393 v.contains(entry)
394 })
395 }
396 ConstraintExpression::StrStartsWith { values } => {
397 handle_str_op(values, getter, case_insensitive, |v, entry| {
398 v.starts_with(entry)
399 })
400 }
401 ConstraintExpression::StrEndsWith { values } => {
402 handle_str_op(values, getter, case_insensitive, |v, entry| {
403 v.ends_with(entry)
404 })
405 }
406 ConstraintExpression::NumEq { value } => {
407 handle_parsable_op(getter, move |v: f64| v == value)
408 }
409 ConstraintExpression::NumGT { value } => {
410 handle_parsable_op(getter, move |v: f64| v > value)
411 }
412 ConstraintExpression::NumGTE { value } => {
413 handle_parsable_op(getter, move |v: f64| v >= value)
414 }
415 ConstraintExpression::NumLT { value } => {
416 handle_parsable_op(getter, move |v: f64| v < value)
417 }
418 ConstraintExpression::NumLTE { value } => {
419 handle_parsable_op(getter, move |v: f64| v <= value)
420 }
421 ConstraintExpression::SemverEq { value } => {
422 handle_parsable_op(getter, move |v: Version| v == value)
423 }
424 ConstraintExpression::SemverGT { value } => {
425 handle_parsable_op(getter, move |v: Version| v > value)
426 }
427 ConstraintExpression::SemverLT { value } => {
428 handle_parsable_op(getter, move |v: Version| v < value)
429 }
430 _ => Box::new(|_| false),
431 };
432
433 Box::new(move |context: &Context| apply_invert(compiled_fn(context)))
434}
435
436fn _compile_constraint_date<F, B>(
438 expression: ConstraintExpression,
439 apply_invert: B,
440 getter: F,
441) -> Evaluate
442where
443 F: Fn(&Context) -> Option<&DateTime<Utc>> + Clone + Sync + Send + 'static,
444 B: Fn(bool) -> bool + Sync + Send + Clone + 'static,
445{
446 let compiled_fn: Box<dyn Evaluator + Send + Sync + 'static> = match expression {
447 ConstraintExpression::DateAfter { value } => {
448 Box::new(move |context: &Context| getter(context).map(|v| *v > value).unwrap_or(false))
449 }
450 ConstraintExpression::DateBefore { value } => {
451 Box::new(move |context: &Context| getter(context).map(|v| *v < value).unwrap_or(false))
452 }
453 _ => Box::new(|_| false),
454 };
455 Box::new(move |context: &Context| apply_invert(compiled_fn(context)))
456}
457
458fn _ip_to_vec(ips: &[String]) -> Vec<IpNet> {
459 let mut result = Vec::new();
460 for ip_str in ips {
461 let ip_parsed = _parse_ip(ip_str.trim());
462 if let Ok(ip) = ip_parsed {
463 result.push(ip);
464 } else {
465 warn!("Could not parse IP address {ip_str:?}");
466 }
467 }
468 result
469}
470
471fn _compile_constraint_host<F, B>(
473 expression: ConstraintExpression,
474 apply_invert: B,
475 case_insensitive: bool,
476 getter: F,
477) -> Evaluate
478where
479 F: Fn(&Context) -> Option<&crate::context::IPAddress> + Clone + Sync + Send + 'static,
480 B: Fn(bool) -> bool + Sync + Send + Clone + 'static,
481{
482 let compiled_fn: Box<dyn Evaluator + Send + Sync + 'static> = match expression {
483 ConstraintExpression::In { values } => {
484 let ips = _ip_to_vec(&values[..]);
485 Box::new(move |context: &Context| {
486 getter(context)
487 .map(|remote_address| {
488 for ip in &ips {
489 if ip.contains(&remote_address.0) {
490 return true;
491 }
492 }
493 false
494 })
495 .unwrap_or(false)
496 })
497 }
498 ConstraintExpression::NotIn { values } => {
499 if values.is_empty() {
500 Box::new(|_| false)
501 } else {
502 let ips = _ip_to_vec(&values[..]);
503 Box::new(move |context: &Context| {
504 getter(context)
505 .map(|remote_address| {
506 if ips.is_empty() {
507 return false;
508 }
509 for ip in &ips {
510 if ip.contains(&remote_address.0) {
511 return false;
512 }
513 }
514 true
515 })
516 .unwrap_or(true)
517 })
518 }
519 }
520 ConstraintExpression::StrContains { values } => handle_str_op(
521 values,
522 move |ctx: &Context| getter(ctx).map(|v| &v.0),
523 case_insensitive,
524 |v, entry| v.contains(entry),
525 ),
526 ConstraintExpression::StrStartsWith { values } => handle_str_op(
527 values,
528 move |ctx: &Context| getter(ctx).map(|v| &v.0),
529 case_insensitive,
530 |v, entry| v.starts_with(entry),
531 ),
532 ConstraintExpression::StrEndsWith { values } => handle_str_op(
533 values,
534 move |ctx: &Context| getter(ctx).map(|v| &v.0),
535 case_insensitive,
536 |v, entry| v.ends_with(entry),
537 ),
538 _ => Box::new(|_| false),
539 };
540 Box::new(move |context: &Context| apply_invert(compiled_fn(context)))
541}
542
543fn _apply_invert(inverted: bool) -> impl Fn(bool) -> bool + Clone {
544 move |state| {
545 if inverted {
546 !state
547 } else {
548 state
549 }
550 }
551}
552
553fn _compile_constraints(constraints: Vec<Constraint>) -> Vec<Evaluate> {
554 constraints
555 .into_iter()
556 .map(|constraint| {
557 let (context_name, expression, inverted, case_insensitive) = (
558 constraint.context_name,
559 constraint.expression,
560 constraint.inverted,
561 constraint.case_insensitive,
562 );
563 let apply_invert = _apply_invert(inverted);
564
565 match context_name.as_str() {
566 "appName" => _compile_constraint_string(
567 expression,
568 apply_invert,
569 case_insensitive,
570 |context| Some(&context.app_name),
571 ),
572 "environment" => _compile_constraint_string(
573 expression,
574 apply_invert,
575 case_insensitive,
576 |context| Some(&context.environment),
577 ),
578 "remoteAddress" => _compile_constraint_host(
579 expression,
580 apply_invert,
581 case_insensitive,
582 |context| context.remote_address.as_ref(),
583 ),
584 "sessionId" => _compile_constraint_string(
585 expression,
586 apply_invert,
587 case_insensitive,
588 |context| context.session_id.as_ref(),
589 ),
590 "userId" => _compile_constraint_string(
591 expression,
592 apply_invert,
593 case_insensitive,
594 |context| context.user_id.as_ref(),
595 ),
596 "currentTime" => _compile_constraint_date(expression, apply_invert, |context| {
597 context.current_time.as_ref()
598 }),
599 _ => _compile_constraint_string(
600 expression,
601 apply_invert,
602 case_insensitive,
603 move |context| context.properties.get(&context_name),
604 ),
605 }
606 })
607 .collect()
608}
609
610pub fn constrain<S: Fn(Option<HashMap<String, String>>) -> Evaluate + Sync + Send + 'static>(
613 constraints: Option<Vec<Constraint>>,
614 strategy: &S,
615 parameters: Option<HashMap<String, String>>,
616) -> Evaluate {
617 let compiled_strategy = strategy(parameters);
618 match constraints {
619 None => {
620 trace!("constrain: no constraints, bypassing");
621 compiled_strategy
622 }
623 Some(constraints) => {
624 if constraints.is_empty() {
625 trace!("constrain: empty constraints list, bypassing");
626 compiled_strategy
627 } else {
628 trace!("constrain: compiling constraints list {constraints:?}");
629 let constraints = _compile_constraints(constraints);
630 Box::new(move |context| {
632 for constraint in &constraints {
634 if !constraint(context) {
635 return false;
636 }
637 }
638 compiled_strategy(context)
639 })
640 }
641 }
642 }
643}
644
645fn _parse_ip(ip: &str) -> Result<IpNet, std::net::AddrParseError> {
646 ip.parse::<IpNet>()
647 .or_else(|_| ip.parse::<IpAddr>().map(|addr| addr.into()))
648}
649
650#[cfg(test)]
651mod tests {
652 use std::default::Default;
653 use std::{collections::hash_map::HashMap, str::FromStr};
654
655 use chrono::{DateTime, FixedOffset, TimeDelta, Utc};
656 use maplit::hashmap;
657 use semver::Version;
658
659 use crate::api::{Constraint, ConstraintExpression};
660 use crate::context::{Context, IPAddress};
661
662 fn parse_ip(addr: &str) -> Option<IPAddress> {
663 Some(IPAddress(addr.parse().unwrap()))
664 }
665
666 fn default_constraint() -> Constraint {
667 Constraint {
668 context_name: "".into(),
669 case_insensitive: false,
670 inverted: false,
671 expression: ConstraintExpression::In { values: vec![] },
672 }
673 }
674
675 #[test]
676 fn test_constrain_general() {
677 let context = Context::default();
679 assert!(super::constrain(None, &super::default, None)(&context));
680
681 let context = Context::default();
683 assert!(super::constrain(Some(vec![]), &super::default, None)(
684 &context
685 ));
686 }
687
688 #[test]
689 fn test_constrain_with_in_constraints() {
690 let context = Context {
692 environment: "development".into(),
693 ..Default::default()
694 };
695 assert!(!super::constrain(
696 Some(vec![Constraint {
697 context_name: "".into(),
698 expression: ConstraintExpression::In { values: vec![] },
699 ..default_constraint()
700 }]),
701 &super::default,
702 None
703 )(&context));
704
705 let context = Context::default();
707 assert!(super::constrain(
708 Some(vec![Constraint {
709 context_name: "customFieldMissing".into(),
710 expression: ConstraintExpression::NotIn {
711 values: vec!["s1".into()]
712 },
713 ..default_constraint()
714 }]),
715 &super::default,
716 None
717 )(&context));
718
719 let context = Context {
721 environment: "production".into(),
722 ..Default::default()
723 };
724 assert!(!super::constrain(
725 Some(vec![Constraint {
726 context_name: "environment".into(),
727 expression: ConstraintExpression::In {
728 values: vec!["development".into()]
729 },
730 ..default_constraint()
731 }]),
732 &super::default,
733 None
734 )(&context));
735
736 let context = Context {
738 environment: "development".into(),
739 ..Default::default()
740 };
741 assert!(!super::constrain(
742 Some(vec![Constraint {
743 context_name: "environment".into(),
744 expression: ConstraintExpression::NotIn {
745 values: vec!["development".into()]
746 },
747 ..default_constraint()
748 }]),
749 &super::default,
750 None
751 )(&context));
752
753 let context = Context {
755 environment: "development".into(),
756 ..Default::default()
757 };
758 assert!(super::constrain(
759 Some(vec![Constraint {
760 context_name: "environment".into(),
761 expression: ConstraintExpression::In {
762 values: vec!["development".into()]
763 },
764 ..default_constraint()
765 }]),
766 &super::default,
767 None
768 )(&context));
769
770 assert!(!super::constrain(
772 Some(vec![Constraint {
773 context_name: "environment".into(),
774 expression: ConstraintExpression::In {
775 values: vec!["development".into()]
776 },
777 inverted: true,
778 ..default_constraint()
779 }]),
780 &super::default,
781 None
782 )(&context));
783
784 let context = Context {
786 environment: "development".into(),
787 ..Default::default()
788 };
789 assert!(super::constrain(
790 Some(vec![Constraint {
791 context_name: "environment".into(),
792 expression: ConstraintExpression::In {
793 values: vec!["staging".into(), "development".into()]
794 },
795 ..default_constraint()
796 }]),
797 &super::default,
798 None
799 )(&context));
800
801 let context = Context {
803 environment: "production".into(),
804 ..Default::default()
805 };
806 assert!(super::constrain(
807 Some(vec![Constraint {
808 context_name: "environment".into(),
809 expression: ConstraintExpression::NotIn {
810 values: vec!["staging".into(), "development".into()]
811 },
812 ..default_constraint()
813 }]),
814 &super::default,
815 None
816 )(&context));
817
818 assert!(!super::constrain(
820 Some(vec![Constraint {
821 context_name: "environment".into(),
822 expression: ConstraintExpression::NotIn {
823 values: vec!["staging".into(), "development".into()]
824 },
825 inverted: true,
826 ..default_constraint()
827 }]),
828 &super::default,
829 None
830 )(&context));
831
832 let context = Context {
836 user_id: Some("fred".into()),
837 ..Default::default()
838 };
839 assert!(super::constrain(
840 Some(vec![Constraint {
841 context_name: "userId".into(),
842 expression: ConstraintExpression::In {
843 values: vec!["fred".into()]
844 },
845 ..default_constraint()
846 }]),
847 &super::default,
848 None
849 )(&context));
850
851 let context = Context {
853 session_id: Some("qwerty".into()),
854 ..Default::default()
855 };
856 assert!(super::constrain(
857 Some(vec![Constraint {
858 context_name: "sessionId".into(),
859 expression: ConstraintExpression::In {
860 values: vec!["qwerty".into()]
861 },
862 ..default_constraint()
863 }]),
864 &super::default,
865 None
866 )(&context));
867
868 let context = Context {
870 remote_address: parse_ip("10.20.30.40"),
871 ..Default::default()
872 };
873 assert!(super::constrain(
874 Some(vec![Constraint {
875 context_name: "remoteAddress".into(),
876 expression: ConstraintExpression::In {
877 values: vec!["10.0.0.0/8".into()]
878 },
879 ..default_constraint()
880 }]),
881 &super::default,
882 None
883 )(&context));
884 let context = Context {
885 remote_address: parse_ip("1.2.3.4"),
886 ..Default::default()
887 };
888 assert!(super::constrain(
889 Some(vec![Constraint {
890 context_name: "remoteAddress".into(),
891 expression: ConstraintExpression::NotIn {
892 values: vec!["10.0.0.0/8".into()]
893 },
894 ..default_constraint()
895 }]),
896 &super::default,
897 None
898 )(&context));
899
900 let context = Context {
902 environment: "development".into(),
903 ..Default::default()
904 };
905 assert!(super::constrain(
907 Some(vec![
908 Constraint {
909 context_name: "environment".into(),
910 expression: ConstraintExpression::In {
911 values: vec!["development".into()]
912 },
913 ..default_constraint()
914 },
915 Constraint {
916 context_name: "environment".into(),
917 expression: ConstraintExpression::In {
918 values: vec!["development".into()]
919 },
920 ..default_constraint()
921 },
922 ]),
923 &super::default,
924 None
925 )(&context));
926
927 assert!(!super::constrain(
928 Some(vec![
929 Constraint {
930 context_name: "environment".into(),
931 expression: ConstraintExpression::In {
932 values: vec!["development".into()]
933 },
934 ..default_constraint()
935 },
936 Constraint {
937 context_name: "environment".into(),
938 expression: ConstraintExpression::In { values: vec![] },
939 ..default_constraint()
940 }
941 ]),
942 &super::default,
943 None
944 )(&context));
945 }
946
947 #[test]
948 fn test_constrain_with_date_constraints() {
949 let now = Utc::now();
950 let context = Context {
951 current_time: Some(now),
952 ..Default::default()
953 };
954 assert!(!super::constrain(
955 Some(vec![Constraint {
956 context_name: "currentTime".into(),
957 expression: ConstraintExpression::DateBefore {
958 value: Utc::now() - TimeDelta::seconds(30)
959 },
960 ..default_constraint()
961 }]),
962 &super::default,
963 None
964 )(&context));
965
966 assert!(super::constrain(
967 Some(vec![Constraint {
968 context_name: "currentTime".into(),
969 expression: ConstraintExpression::DateAfter {
970 value: Utc::now() - TimeDelta::seconds(30)
971 },
972 ..default_constraint()
973 }]),
974 &super::default,
975 None
976 )(&context));
977
978 assert!(!super::constrain(
980 Some(vec![Constraint {
981 context_name: "currentTime".into(),
982 expression: ConstraintExpression::DateAfter {
983 value: Utc::now() - TimeDelta::seconds(30)
984 },
985 inverted: true,
986 ..default_constraint()
987 }]),
988 &super::default,
989 None
990 )(&context));
991
992 let context = Context {
993 current_time: DateTime::<FixedOffset>::parse_from_rfc3339("2024-07-18T17:18:25.844Z")
994 .ok()
995 .map(|date| date.to_utc()),
996 ..Default::default()
997 };
998
999 assert!(super::constrain(
1000 Some(vec![Constraint {
1001 context_name: "currentTime".into(),
1002 expression: ConstraintExpression::DateBefore { value: Utc::now() },
1003 ..default_constraint()
1004 }]),
1005 &super::default,
1006 None
1007 )(&context));
1008
1009 assert!(!super::constrain(
1011 Some(vec![Constraint {
1012 context_name: "currentTime".into(),
1013 expression: ConstraintExpression::DateBefore { value: Utc::now() },
1014 inverted: true,
1015 ..default_constraint()
1016 }]),
1017 &super::default,
1018 None
1019 )(&context));
1020
1021 assert!(!super::constrain(
1022 Some(vec![Constraint {
1023 context_name: "currentTime".into(),
1024 expression: ConstraintExpression::DateAfter { value: Utc::now() },
1025 ..default_constraint()
1026 }]),
1027 &super::default,
1028 None
1029 )(&context));
1030
1031 assert!(!super::constrain(
1033 Some(vec![Constraint {
1034 context_name: "environment".into(),
1035 expression: ConstraintExpression::DateBefore { value: Utc::now() },
1036 ..default_constraint()
1037 }]),
1038 &super::default,
1039 None
1040 )(&context));
1041 }
1042
1043 #[test]
1044 fn test_constrain_with_semver_constraints() {
1045 let context = Context {
1046 properties: hashmap! {
1047 "version".into() => "1.2.3-rc.2".into()
1048 },
1049 ..Default::default()
1050 };
1051 assert!(super::constrain(
1052 Some(vec![Constraint {
1053 context_name: "version".into(),
1054 expression: ConstraintExpression::SemverLT {
1055 value: Version::from_str("1.2.3").unwrap()
1056 },
1057 ..default_constraint()
1058 }]),
1059 &super::default,
1060 None
1061 )(&context));
1062
1063 assert!(super::constrain(
1064 Some(vec![Constraint {
1065 context_name: "version".into(),
1066 expression: ConstraintExpression::SemverGT {
1067 value: Version::from_str("1.2.2").unwrap()
1068 },
1069 ..default_constraint()
1070 }]),
1071 &super::default,
1072 None
1073 )(&context));
1074
1075 assert!(super::constrain(
1076 Some(vec![Constraint {
1077 context_name: "version".into(),
1078 expression: ConstraintExpression::SemverEq {
1079 value: Version::from_str("1.2.3-rc.2").unwrap()
1080 },
1081 ..default_constraint()
1082 }]),
1083 &super::default,
1084 None
1085 )(&context));
1086
1087 let context = Context {
1088 properties: hashmap! {
1089 "app_version".into() => "1.0.0-alpha.1".into()
1090 },
1091 ..Default::default()
1092 };
1093
1094 assert!(!super::constrain(
1095 Some(vec![Constraint {
1096 context_name: "app_version".into(),
1097 expression: ConstraintExpression::SemverLT {
1098 value: Version::from_str("0.155.0").unwrap()
1099 },
1100 ..default_constraint()
1101 }]),
1102 &super::default,
1103 None
1104 )(&context));
1105
1106 assert!(super::constrain(
1108 Some(vec![Constraint {
1109 context_name: "app_version".into(),
1110 expression: ConstraintExpression::SemverLT {
1111 value: Version::from_str("0.155.0").unwrap()
1112 },
1113 inverted: true,
1114 ..default_constraint()
1115 }]),
1116 &super::default,
1117 None
1118 )(&context));
1119
1120 assert!(!super::constrain(
1121 Some(vec![Constraint {
1122 context_name: "app_version".into(),
1123 expression: ConstraintExpression::SemverGT {
1124 value: Version::from_str("1.0.0-beta.1").unwrap()
1125 },
1126 ..default_constraint()
1127 }]),
1128 &super::default,
1129 None
1130 )(&context));
1131
1132 assert!(super::constrain(
1134 Some(vec![Constraint {
1135 context_name: "app_version".into(),
1136 expression: ConstraintExpression::SemverGT {
1137 value: Version::from_str("1.0.0-beta.1").unwrap()
1138 },
1139 inverted: true,
1140 ..default_constraint()
1141 }]),
1142 &super::default,
1143 None
1144 )(&context));
1145
1146 assert!(!super::constrain(
1147 Some(vec![Constraint {
1148 context_name: "app_version".into(),
1149 expression: ConstraintExpression::SemverEq {
1150 value: Version::from_str("1.0.0-beta.10").unwrap()
1151 },
1152 ..default_constraint()
1153 }]),
1154 &super::default,
1155 None
1156 )(&context));
1157
1158 assert!(super::constrain(
1160 Some(vec![Constraint {
1161 context_name: "app_version".into(),
1162 expression: ConstraintExpression::SemverEq {
1163 value: Version::from_str("1.0.0-beta.10").unwrap()
1164 },
1165 inverted: true,
1166 ..default_constraint()
1167 }]),
1168 &super::default,
1169 None
1170 )(&context));
1171 }
1172
1173 #[test]
1174 fn test_constrain_with_str_constraints() {
1175 let context = Context {
1176 app_name: "gondola".into(),
1177 ..Default::default()
1178 };
1179
1180 assert!(super::constrain(
1182 Some(vec![Constraint {
1183 context_name: "appName".into(),
1184 expression: ConstraintExpression::StrContains {
1185 values: vec!["ondo".into(), "gigabad".into()]
1186 },
1187 ..default_constraint()
1188 }]),
1189 &super::default,
1190 None
1191 )(&context));
1192
1193 assert!(!super::constrain(
1194 Some(vec![Constraint {
1195 context_name: "appName".into(),
1196 expression: ConstraintExpression::StrContains {
1197 values: vec!["Ondo".into(), "gigabad".into()]
1198 },
1199 ..default_constraint()
1200 }]),
1201 &super::default,
1202 None
1203 )(&context));
1204
1205 assert!(super::constrain(
1207 Some(vec![Constraint {
1208 context_name: "appName".into(),
1209 expression: ConstraintExpression::StrContains {
1210 values: vec!["Ondo".into(), "gigabad".into()]
1211 },
1212 inverted: true,
1213 ..default_constraint()
1214 }]),
1215 &super::default,
1216 None
1217 )(&context));
1218
1219 assert!(super::constrain(
1221 Some(vec![Constraint {
1222 context_name: "appName".into(),
1223 expression: ConstraintExpression::StrContains {
1224 values: vec!["Ondo".into(), "gigabad".into()]
1225 },
1226 case_insensitive: true,
1227 ..default_constraint()
1228 }]),
1229 &super::default,
1230 None
1231 )(&context));
1232
1233 assert!(super::constrain(
1234 Some(vec![Constraint {
1235 context_name: "appName".into(),
1236 expression: ConstraintExpression::StrStartsWith {
1237 values: vec!["and".into(), "gon".into()]
1238 },
1239 ..default_constraint()
1240 }]),
1241 &super::default,
1242 None
1243 )(&context));
1244
1245 assert!(!super::constrain(
1246 Some(vec![Constraint {
1247 context_name: "appName".into(),
1248 expression: ConstraintExpression::StrStartsWith {
1249 values: vec!["and".into(), "Gon".into()]
1250 },
1251 ..default_constraint()
1252 }]),
1253 &super::default,
1254 None
1255 )(&context));
1256
1257 assert!(super::constrain(
1259 Some(vec![Constraint {
1260 context_name: "appName".into(),
1261 expression: ConstraintExpression::StrStartsWith {
1262 values: vec!["and".into(), "Gon".into()]
1263 },
1264 inverted: true,
1265 ..default_constraint()
1266 }]),
1267 &super::default,
1268 None
1269 )(&context));
1270
1271 assert!(super::constrain(
1273 Some(vec![Constraint {
1274 context_name: "appName".into(),
1275 expression: ConstraintExpression::StrStartsWith {
1276 values: vec!["and".into(), "Gon".into()]
1277 },
1278 case_insensitive: true,
1279 ..default_constraint()
1280 }]),
1281 &super::default,
1282 None
1283 )(&context));
1284
1285 assert!(super::constrain(
1286 Some(vec![Constraint {
1287 context_name: "appName".into(),
1288 expression: ConstraintExpression::StrEndsWith {
1289 values: vec!["ola".into(), "oga".into()]
1290 },
1291 ..default_constraint()
1292 }]),
1293 &super::default,
1294 None
1295 )(&context));
1296
1297 assert!(!super::constrain(
1298 Some(vec![Constraint {
1299 context_name: "appName".into(),
1300 expression: ConstraintExpression::StrEndsWith {
1301 values: vec!["Ola".into(), "Oga".into()]
1302 },
1303 ..default_constraint()
1304 }]),
1305 &super::default,
1306 None
1307 )(&context));
1308
1309 assert!(super::constrain(
1311 Some(vec![Constraint {
1312 context_name: "appName".into(),
1313 expression: ConstraintExpression::StrEndsWith {
1314 values: vec!["Ola".into(), "Oga".into()]
1315 },
1316 inverted: true,
1317 ..default_constraint()
1318 }]),
1319 &super::default,
1320 None
1321 )(&context));
1322
1323 assert!(super::constrain(
1325 Some(vec![Constraint {
1326 context_name: "appName".into(),
1327 expression: ConstraintExpression::StrEndsWith {
1328 values: vec!["Ola".into(), "Oga".into()]
1329 },
1330 case_insensitive: true,
1331 ..default_constraint()
1332 }]),
1333 &super::default,
1334 None
1335 )(&context));
1336 }
1337
1338 #[test]
1339 fn test_constrain_with_num_constraints() {
1340 let context = Context {
1341 properties: hashmap! {
1342 "times".into() => "30".into()
1343 },
1344 ..Default::default()
1345 };
1346
1347 assert!(super::constrain(
1348 Some(vec![Constraint {
1349 context_name: "times".into(),
1350 expression: ConstraintExpression::NumEq { value: 30.0 },
1351 ..default_constraint()
1352 }]),
1353 &super::default,
1354 None
1355 )(&context));
1356
1357 assert!(super::constrain(
1358 Some(vec![Constraint {
1359 context_name: "times".into(),
1360 expression: ConstraintExpression::NumLT { value: 31.0 },
1361 ..default_constraint()
1362 }]),
1363 &super::default,
1364 None
1365 )(&context));
1366
1367 assert!(super::constrain(
1368 Some(vec![Constraint {
1369 context_name: "times".into(),
1370 expression: ConstraintExpression::NumLTE { value: 40.0 },
1371 ..default_constraint()
1372 }]),
1373 &super::default,
1374 None
1375 )(&context));
1376
1377 assert!(super::constrain(
1378 Some(vec![Constraint {
1379 context_name: "times".into(),
1380 expression: ConstraintExpression::NumGT { value: 29.0 },
1381 ..default_constraint()
1382 }]),
1383 &super::default,
1384 None
1385 )(&context));
1386
1387 assert!(super::constrain(
1388 Some(vec![Constraint {
1389 context_name: "times".into(),
1390 expression: ConstraintExpression::NumGTE { value: 30.0 },
1391 ..default_constraint()
1392 }]),
1393 &super::default,
1394 None
1395 )(&context));
1396
1397 assert!(!super::constrain(
1399 Some(vec![Constraint {
1400 context_name: "times".into(),
1401 expression: ConstraintExpression::NumEq { value: 30.0 },
1402 inverted: true,
1403 ..default_constraint()
1404 }]),
1405 &super::default,
1406 None
1407 )(&context));
1408
1409 assert!(!super::constrain(
1410 Some(vec![Constraint {
1411 context_name: "times".into(),
1412 expression: ConstraintExpression::NumLT { value: 31.0 },
1413 inverted: true,
1414 ..default_constraint()
1415 }]),
1416 &super::default,
1417 None
1418 )(&context));
1419
1420 assert!(!super::constrain(
1421 Some(vec![Constraint {
1422 context_name: "times".into(),
1423 expression: ConstraintExpression::NumLTE { value: 40.0 },
1424 inverted: true,
1425 ..default_constraint()
1426 }]),
1427 &super::default,
1428 None
1429 )(&context));
1430
1431 assert!(!super::constrain(
1432 Some(vec![Constraint {
1433 context_name: "times".into(),
1434 expression: ConstraintExpression::NumGT { value: 29.0 },
1435 inverted: true,
1436 ..default_constraint()
1437 }]),
1438 &super::default,
1439 None
1440 )(&context));
1441
1442 assert!(!super::constrain(
1443 Some(vec![Constraint {
1444 context_name: "times".into(),
1445 expression: ConstraintExpression::NumGTE { value: 30.0 },
1446 inverted: true,
1447 ..default_constraint()
1448 }]),
1449 &super::default,
1450 None
1451 )(&context));
1452 }
1453
1454 #[test]
1455 fn test_user_with_id() {
1456 let params: HashMap<String, String> = hashmap! {
1457 "userIds".into() => "fred,barney".into(),
1458 };
1459 assert!(super::user_with_id(Some(params.clone()))(&Context {
1460 user_id: Some("fred".into()),
1461 ..Default::default()
1462 }));
1463 assert!(super::user_with_id(Some(params.clone()))(&Context {
1464 user_id: Some("barney".into()),
1465 ..Default::default()
1466 }));
1467 assert!(!super::user_with_id(Some(params))(&Context {
1468 user_id: Some("betty".into()),
1469 ..Default::default()
1470 }));
1471 }
1472
1473 #[test]
1474 fn test_flexible_rollout() {
1475 let params: HashMap<String, String> = hashmap! {
1477 "stickiness".into() => "random".into(),
1478 "rollout".into() => "0".into(),
1479 };
1480 let c: Context = Default::default();
1481 assert!(!super::flexible_rollout(Some(params))(&c));
1482
1483 let params: HashMap<String, String> = hashmap! {
1484 "stickiness".into() => "random".into(),
1485 "rollout".into() => "100".into(),
1486 };
1487 let c: Context = Default::default();
1488 assert!(super::flexible_rollout(Some(params))(&c));
1489 let params: HashMap<String, String> = hashmap! {
1490 "stickiness".into() => "random".into(),
1491 "rollout".into() => "0".into(),
1492 };
1493 let c: Context = Default::default();
1494 assert!(!super::flexible_rollout(Some(params))(&c));
1495
1496 let params: HashMap<String, String> = hashmap! {
1500 "stickiness".into() => "sessionId".into(),
1501 "groupId".into() => "group1".into(),
1502 "rollout".into() => "0".into(),
1503 };
1504 let c: Context = Context {
1505 session_id: Some("session1".into()),
1506 ..Default::default()
1507 };
1508 assert!(!super::flexible_rollout(Some(params))(&c));
1509 let params: HashMap<String, String> = hashmap! {
1510 "stickiness".into() => "sessionId".into(),
1511 "groupId".into() => "group1".into(),
1512 "rollout".into() => "100".into(),
1513 };
1514 let c: Context = Context {
1515 session_id: Some("session1".into()),
1516 ..Default::default()
1517 };
1518 assert!(super::flexible_rollout(Some(params))(&c));
1519 let params: HashMap<String, String> = hashmap! {
1521 "stickiness".into() => "sessionId".into(),
1522 "groupId".into() => "group1".into(),
1523 "rollout".into() => "50".into(),
1524 };
1525 let c: Context = Context {
1526 session_id: Some("session1".into()),
1527 ..Default::default()
1528 };
1529 assert!(super::flexible_rollout(Some(params.clone()))(&c));
1530 let c: Context = Context {
1531 session_id: Some("session2".into()),
1532 ..Default::default()
1533 };
1534 assert!(!super::flexible_rollout(Some(params))(&c));
1535 let params: HashMap<String, String> = hashmap! {
1537 "stickiness".into() => "sessionId".into(),
1538 "groupId".into() => "group3".into(),
1539 "rollout".into() => "50".into(),
1540 };
1541 let c: Context = Context {
1542 session_id: Some("session1".into()),
1543 ..Default::default()
1544 };
1545 assert!(!super::flexible_rollout(Some(params.clone()))(&c));
1546 let c: Context = Context {
1547 session_id: Some("session2".into()),
1548 ..Default::default()
1549 };
1550 assert!(super::flexible_rollout(Some(params))(&c));
1551
1552 let params: HashMap<String, String> = hashmap! {
1554 "stickiness".into() => "userId".into(),
1555 "groupId".into() => "group1".into(),
1556 "rollout".into() => "0".into(),
1557 };
1558 let c: Context = Context {
1559 user_id: Some("user1".into()),
1560 ..Default::default()
1561 };
1562 assert!(!super::flexible_rollout(Some(params))(&c));
1563 let params: HashMap<String, String> = hashmap! {
1564 "stickiness".into() => "userId".into(),
1565 "groupId".into() => "group1".into(),
1566 "rollout".into() => "100".into(),
1567 };
1568 let c: Context = Context {
1569 user_id: Some("user1".into()),
1570 ..Default::default()
1571 };
1572 assert!(super::flexible_rollout(Some(params))(&c));
1573 let params: HashMap<String, String> = hashmap! {
1575 "stickiness".into() => "userId".into(),
1576 "groupId".into() => "group1".into(),
1577 "rollout".into() => "50".into(),
1578 };
1579 let c: Context = Context {
1580 user_id: Some("user1".into()),
1581 ..Default::default()
1582 };
1583 assert!(super::flexible_rollout(Some(params.clone()))(&c));
1584 let c: Context = Context {
1585 user_id: Some("user3".into()),
1586 ..Default::default()
1587 };
1588 assert!(!super::flexible_rollout(Some(params))(&c));
1589 let params: HashMap<String, String> = hashmap! {
1591 "stickiness".into() => "userId".into(),
1592 "groupId".into() => "group2".into(),
1593 "rollout".into() => "50".into(),
1594 };
1595 let c: Context = Context {
1596 user_id: Some("user3".into()),
1597 ..Default::default()
1598 };
1599 assert!(!super::flexible_rollout(Some(params.clone()))(&c));
1600 let c: Context = Context {
1601 user_id: Some("user1".into()),
1602 ..Default::default()
1603 };
1604 assert!(super::flexible_rollout(Some(params))(&c));
1605 }
1606
1607 #[test]
1608 fn test_random() {
1609 let params: HashMap<String, String> = hashmap! {
1610 "percentage".into() => "0".into()
1611 };
1612 let c: Context = Default::default();
1613 assert!(!super::random(Some(params))(&c));
1614 let params: HashMap<String, String> = hashmap! {
1615 "percentage".into() => "100".into()
1616 };
1617 let c: Context = Default::default();
1618 assert!(super::random(Some(params))(&c));
1619 }
1620
1621 #[test]
1622 fn test_remote_address() {
1623 let params: HashMap<String, String> = hashmap! {
1624 "IPs".into() => "1.2.0.0/8,2.3.4.5,2222:FF:0:1234::/64".into()
1625 };
1626 let c: Context = Context {
1627 remote_address: parse_ip("1.2.3.4"),
1628 ..Default::default()
1629 };
1630 assert!(super::remote_address(Some(params.clone()))(&c));
1631 let c: Context = Context {
1632 remote_address: parse_ip("2.3.4.5"),
1633 ..Default::default()
1634 };
1635 assert!(super::remote_address(Some(params.clone()))(&c));
1636 let c: Context = Context {
1637 remote_address: parse_ip("2222:FF:0:1234::FDEC"),
1638 ..Default::default()
1639 };
1640 assert!(super::remote_address(Some(params.clone()))(&c));
1641 let c: Context = Context {
1642 remote_address: parse_ip("2.3.4.4"),
1643 ..Default::default()
1644 };
1645 assert!(!super::remote_address(Some(params))(&c));
1646 }
1647
1648 #[test]
1649 fn test_hostname() {
1650 let c: Context = Default::default();
1651 let this_hostname = hostname::get().unwrap().into_string().unwrap();
1652 let params: HashMap<String, String> = hashmap! {
1653 "hostNames".into() => format!("foo,{},bar", this_hostname)
1654 };
1655 assert!(super::hostname(Some(params))(&c));
1656 let params: HashMap<String, String> = hashmap! {
1657 "hostNames".into() => "foo,bar".into()
1658 };
1659 assert!(!super::hostname(Some(params))(&c));
1660 }
1661
1662 #[test]
1663 fn normalised_hash() {
1664 assert!(50 > super::normalised_hash("AB12A", "122", 100).unwrap());
1665 }
1666
1667 #[test]
1668 fn test_normalized_hash() {
1669 assert_eq!(73, super::normalised_hash("gr1", "123", 100).unwrap());
1670 assert_eq!(25, super::normalised_hash("groupX", "999", 100).unwrap());
1671 }
1672
1673 #[test]
1674 fn test_normalised_variant_hash() {
1675 assert_eq!(
1676 96,
1677 super::normalised_variant_hash("gr1", "123", 100).unwrap()
1678 );
1679 assert_eq!(
1680 60,
1681 super::normalised_variant_hash("groupX", "999", 100).unwrap()
1682 );
1683 }
1684}