1#![forbid(unsafe_code)]
2use std::{
20 collections::{BTreeMap, HashMap},
21 future::Future,
22 pin::Pin,
23 sync::Arc,
24 time::Duration,
25};
26
27pub trait Specification<T: ?Sized> {
33 fn is_satisfied_by(&self, candidate: &T) -> bool;
35
36 fn and<Other>(self, other: Other) -> And<Self, Other>
41 where
42 Self: Sized,
43 Other: Specification<T>,
44 {
45 And {
46 left: self,
47 right: other,
48 }
49 }
50
51 fn or<Other>(self, other: Other) -> Or<Self, Other>
56 where
57 Self: Sized,
58 Other: Specification<T>,
59 {
60 Or {
61 left: self,
62 right: other,
63 }
64 }
65
66 fn not(self) -> Not<Self>
68 where
69 Self: Sized,
70 {
71 Not { inner: self }
72 }
73}
74
75impl<T: ?Sized, Predicate> Specification<T> for Predicate
76where
77 Predicate: Fn(&T) -> bool,
78{
79 fn is_satisfied_by(&self, candidate: &T) -> bool {
80 self(candidate)
81 }
82}
83
84pub trait AsyncSpecification<T: ?Sized + Sync>: Sync {
98 type Error: Send;
100
101 fn is_satisfied_by<'candidate>(
103 &'candidate self,
104 candidate: &'candidate T,
105 ) -> impl Future<Output = Result<bool, Self::Error>> + Send + 'candidate;
106
107 fn and<Other>(self, other: Other) -> AsyncAnd<Self, Other>
109 where
110 Self: Sized,
111 Other: AsyncSpecification<T, Error = Self::Error>,
112 {
113 AsyncAnd {
114 left: self,
115 right: other,
116 }
117 }
118
119 fn or<Other>(self, other: Other) -> AsyncOr<Self, Other>
121 where
122 Self: Sized,
123 Other: AsyncSpecification<T, Error = Self::Error>,
124 {
125 AsyncOr {
126 left: self,
127 right: other,
128 }
129 }
130
131 fn not(self) -> AsyncNot<Self>
133 where
134 Self: Sized,
135 {
136 AsyncNot { inner: self }
137 }
138}
139
140pub struct AsyncAnd<Left, Right> {
142 left: Left,
143 right: Right,
144}
145
146impl<T: ?Sized + Sync, Left, Right> AsyncSpecification<T> for AsyncAnd<Left, Right>
147where
148 Left: AsyncSpecification<T>,
149 Right: AsyncSpecification<T, Error = Left::Error>,
150{
151 type Error = Left::Error;
152
153 #[allow(clippy::manual_async_fn)]
154 fn is_satisfied_by<'candidate>(
155 &'candidate self,
156 candidate: &'candidate T,
157 ) -> impl Future<Output = Result<bool, Self::Error>> + Send + 'candidate {
158 async move {
159 if !self.left.is_satisfied_by(candidate).await? {
160 return Ok(false);
161 }
162 self.right.is_satisfied_by(candidate).await
163 }
164 }
165}
166
167pub struct AsyncOr<Left, Right> {
169 left: Left,
170 right: Right,
171}
172
173impl<T: ?Sized + Sync, Left, Right> AsyncSpecification<T> for AsyncOr<Left, Right>
174where
175 Left: AsyncSpecification<T>,
176 Right: AsyncSpecification<T, Error = Left::Error>,
177{
178 type Error = Left::Error;
179
180 #[allow(clippy::manual_async_fn)]
181 fn is_satisfied_by<'candidate>(
182 &'candidate self,
183 candidate: &'candidate T,
184 ) -> impl Future<Output = Result<bool, Self::Error>> + Send + 'candidate {
185 async move {
186 if self.left.is_satisfied_by(candidate).await? {
187 return Ok(true);
188 }
189 self.right.is_satisfied_by(candidate).await
190 }
191 }
192}
193
194pub struct AsyncNot<Inner> {
196 inner: Inner,
197}
198
199impl<T: ?Sized + Sync, Inner> AsyncSpecification<T> for AsyncNot<Inner>
200where
201 Inner: AsyncSpecification<T>,
202{
203 type Error = Inner::Error;
204
205 #[allow(clippy::manual_async_fn)]
206 fn is_satisfied_by<'candidate>(
207 &'candidate self,
208 candidate: &'candidate T,
209 ) -> impl Future<Output = Result<bool, Self::Error>> + Send + 'candidate {
210 async move {
211 self.inner
212 .is_satisfied_by(candidate)
213 .await
214 .map(|result| !result)
215 }
216 }
217}
218
219pub type BoxFuture<'future, Error> =
221 Pin<Box<dyn Future<Output = Result<bool, Error>> + Send + 'future>>;
222
223trait DynAsyncSpecification<T: ?Sized + Sync, Error>: Send + Sync {
224 fn evaluate<'candidate>(
225 &'candidate self,
226 candidate: &'candidate T,
227 ) -> BoxFuture<'candidate, Error>;
228}
229
230impl<T: ?Sized + Sync, Error, Concrete> DynAsyncSpecification<T, Error> for Concrete
231where
232 Concrete: AsyncSpecification<T, Error = Error> + Send + Sync,
233{
234 fn evaluate<'candidate>(
235 &'candidate self,
236 candidate: &'candidate T,
237 ) -> BoxFuture<'candidate, Error> {
238 Box::pin(self.is_satisfied_by(candidate))
239 }
240}
241
242pub struct BoxedAsyncSpecification<T: ?Sized + Sync, Error: Send> {
248 inner: Box<dyn DynAsyncSpecification<T, Error>>,
249}
250
251impl<T: ?Sized + Sync, Error: Send> BoxedAsyncSpecification<T, Error> {
252 pub fn new<Concrete>(specification: Concrete) -> Self
254 where
255 Concrete: AsyncSpecification<T, Error = Error> + Send + Sync + 'static,
256 {
257 Self {
258 inner: Box::new(specification),
259 }
260 }
261}
262
263impl<T: ?Sized + Sync, Error: Send> AsyncSpecification<T> for BoxedAsyncSpecification<T, Error> {
264 type Error = Error;
265
266 #[allow(clippy::manual_async_fn)]
267 fn is_satisfied_by<'candidate>(
268 &'candidate self,
269 candidate: &'candidate T,
270 ) -> impl Future<Output = Result<bool, Self::Error>> + Send + 'candidate {
271 self.inner.evaluate(candidate)
272 }
273}
274
275pub struct And<Left, Right> {
281 left: Left,
282 right: Right,
283}
284
285impl<T: ?Sized, Left, Right> Specification<T> for And<Left, Right>
286where
287 Left: Specification<T>,
288 Right: Specification<T>,
289{
290 fn is_satisfied_by(&self, candidate: &T) -> bool {
291 self.left.is_satisfied_by(candidate) && self.right.is_satisfied_by(candidate)
292 }
293}
294
295pub struct Or<Left, Right> {
301 left: Left,
302 right: Right,
303}
304
305impl<T: ?Sized, Left, Right> Specification<T> for Or<Left, Right>
306where
307 Left: Specification<T>,
308 Right: Specification<T>,
309{
310 fn is_satisfied_by(&self, candidate: &T) -> bool {
311 self.left.is_satisfied_by(candidate) || self.right.is_satisfied_by(candidate)
312 }
313}
314
315pub struct Not<Inner> {
319 inner: Inner,
320}
321
322impl<T: ?Sized, Inner> Specification<T> for Not<Inner>
323where
324 Inner: Specification<T>,
325{
326 fn is_satisfied_by(&self, candidate: &T) -> bool {
327 !self.inner.is_satisfied_by(candidate)
328 }
329}
330
331#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
333pub struct Always;
334
335impl<T: ?Sized> Specification<T> for Always {
336 fn is_satisfied_by(&self, _: &T) -> bool {
337 true
338 }
339}
340
341#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
343pub struct Never;
344
345impl<T: ?Sized> Specification<T> for Never {
346 fn is_satisfied_by(&self, _: &T) -> bool {
347 false
348 }
349}
350
351pub struct AllOf<'specifications, SpecificationType> {
356 specifications: &'specifications [SpecificationType],
357}
358
359impl<'specifications, SpecificationType> AllOf<'specifications, SpecificationType> {
360 pub const fn new(specifications: &'specifications [SpecificationType]) -> Self {
362 Self { specifications }
363 }
364}
365
366impl<T: ?Sized, SpecificationType> Specification<T> for AllOf<'_, SpecificationType>
367where
368 SpecificationType: Specification<T>,
369{
370 fn is_satisfied_by(&self, candidate: &T) -> bool {
371 self.specifications
372 .iter()
373 .all(|specification| specification.is_satisfied_by(candidate))
374 }
375}
376
377pub struct AnyOf<'specifications, SpecificationType> {
382 specifications: &'specifications [SpecificationType],
383}
384
385impl<'specifications, SpecificationType> AnyOf<'specifications, SpecificationType> {
386 pub const fn new(specifications: &'specifications [SpecificationType]) -> Self {
388 Self { specifications }
389 }
390}
391
392impl<T: ?Sized, SpecificationType> Specification<T> for AnyOf<'_, SpecificationType>
393where
394 SpecificationType: Specification<T>,
395{
396 fn is_satisfied_by(&self, candidate: &T) -> bool {
397 self.specifications
398 .iter()
399 .any(|specification| specification.is_satisfied_by(candidate))
400 }
401}
402
403pub struct BoxedSpecification<T: ?Sized> {
408 inner: Box<dyn Specification<T>>,
409}
410
411impl<T: ?Sized> BoxedSpecification<T> {
412 pub fn new<Concrete>(specification: Concrete) -> Self
414 where
415 Concrete: Specification<T> + 'static,
416 {
417 Self {
418 inner: Box::new(specification),
419 }
420 }
421}
422
423impl<T: ?Sized> Specification<T> for BoxedSpecification<T> {
424 fn is_satisfied_by(&self, candidate: &T) -> bool {
425 self.inner.is_satisfied_by(candidate)
426 }
427}
428
429pub struct SharedSpecification<T: ?Sized> {
446 inner: Arc<dyn Specification<T> + Send + Sync>,
447}
448
449impl<T: ?Sized> Clone for SharedSpecification<T> {
450 fn clone(&self) -> Self {
451 Self {
452 inner: Arc::clone(&self.inner),
453 }
454 }
455}
456
457impl<T: ?Sized> SharedSpecification<T> {
458 pub fn new<Concrete>(specification: Concrete) -> Self
460 where
461 Concrete: Specification<T> + Send + Sync + 'static,
462 {
463 Self {
464 inner: Arc::new(specification),
465 }
466 }
467}
468
469impl<T: ?Sized> Specification<T> for SharedSpecification<T> {
470 fn is_satisfied_by(&self, candidate: &T) -> bool {
471 self.inner.is_satisfied_by(candidate)
472 }
473}
474
475pub trait DecisionSpecification<T: ?Sized> {
477 type Decision;
479
480 fn decide(&self, candidate: &T) -> Option<&Self::Decision>;
482}
483
484pub struct FirstMatch<T: ?Sized, Decision> {
489 rules: Vec<(BoxedSpecification<T>, Decision)>,
490}
491
492impl<T: ?Sized, Decision> FirstMatch<T, Decision> {
493 pub const fn new() -> Self {
495 Self { rules: Vec::new() }
496 }
497
498 pub fn push<Concrete>(&mut self, specification: Concrete, decision: Decision)
500 where
501 Concrete: Specification<T> + 'static,
502 {
503 self.rules
504 .push((BoxedSpecification::new(specification), decision));
505 }
506
507 pub fn decide_or<'decision>(
509 &'decision self,
510 candidate: &T,
511 fallback: &'decision Decision,
512 ) -> &'decision Decision {
513 self.decide(candidate).unwrap_or(fallback)
514 }
515}
516
517impl<T: ?Sized, Decision> Default for FirstMatch<T, Decision> {
518 fn default() -> Self {
519 Self::new()
520 }
521}
522
523impl<T: ?Sized, Decision> DecisionSpecification<T> for FirstMatch<T, Decision> {
524 type Decision = Decision;
525
526 fn decide(&self, candidate: &T) -> Option<&Decision> {
527 self.rules.iter().find_map(|(specification, decision)| {
528 specification.is_satisfied_by(candidate).then_some(decision)
529 })
530 }
531}
532
533pub struct IndexedFirstMatch<T: ?Sized, Decision> {
567 key_of: Arc<dyn for<'candidate> Fn(&'candidate T) -> &'candidate str + Send + Sync>,
568 rules: Vec<(SharedSpecification<T>, Decision)>,
569 buckets: HashMap<String, Vec<usize>>,
570 unkeyed: Vec<usize>,
571}
572
573impl<T: ?Sized, Decision> IndexedFirstMatch<T, Decision> {
574 pub fn new<Projection>(key_of: Projection) -> Self
578 where
579 Projection: for<'candidate> Fn(&'candidate T) -> &'candidate str + Send + Sync + 'static,
580 {
581 Self {
582 key_of: Arc::new(key_of),
583 rules: Vec::new(),
584 buckets: HashMap::new(),
585 unkeyed: Vec::new(),
586 }
587 }
588
589 pub fn push_keyed<Concrete>(
595 &mut self,
596 key: impl Into<String>,
597 specification: Concrete,
598 decision: Decision,
599 ) where
600 Concrete: Specification<T> + Send + Sync + 'static,
601 {
602 let index = self.rules.len();
603 self.rules
604 .push((SharedSpecification::new(specification), decision));
605 self.buckets.entry(key.into()).or_default().push(index);
606 }
607
608 pub fn push_unkeyed<Concrete>(&mut self, specification: Concrete, decision: Decision)
610 where
611 Concrete: Specification<T> + Send + Sync + 'static,
612 {
613 let index = self.rules.len();
614 self.rules
615 .push((SharedSpecification::new(specification), decision));
616 self.unkeyed.push(index);
617 }
618
619 pub fn may_match(&self, key: &str) -> bool {
624 !self.unkeyed.is_empty() || self.buckets.contains_key(key)
625 }
626}
627
628impl<T: ?Sized, Decision> DecisionSpecification<T> for IndexedFirstMatch<T, Decision> {
629 type Decision = Decision;
630
631 fn decide(&self, candidate: &T) -> Option<&Decision> {
632 let key = (self.key_of)(candidate);
633 let keyed = self.buckets.get(key).map(Vec::as_slice).unwrap_or(&[]);
634 let mut keyed_index = 0;
635 let mut unkeyed_index = 0;
636
637 while keyed_index < keyed.len() || unkeyed_index < self.unkeyed.len() {
638 let rule_index = match (keyed.get(keyed_index), self.unkeyed.get(unkeyed_index)) {
639 (Some(&keyed_rule), Some(&unkeyed_rule)) if keyed_rule < unkeyed_rule => {
640 keyed_index += 1;
641 keyed_rule
642 }
643 (Some(_), Some(&unkeyed_rule)) => {
644 unkeyed_index += 1;
645 unkeyed_rule
646 }
647 (Some(&keyed_rule), None) => {
648 keyed_index += 1;
649 keyed_rule
650 }
651 (None, Some(&unkeyed_rule)) => {
652 unkeyed_index += 1;
653 unkeyed_rule
654 }
655 (None, None) => unreachable!("loop condition guarantees an available rule"),
656 };
657
658 let (specification, decision) = &self.rules[rule_index];
659 if specification.is_satisfied_by(candidate) {
660 return Some(decision);
661 }
662 }
663 None
664 }
665}
666
667pub struct EvaluationContext<UserData> {
675 user_data: UserData,
676 counters: BTreeMap<String, u64>,
677 flags: BTreeMap<String, bool>,
678 timestamps: BTreeMap<String, Duration>,
679}
680
681impl<UserData> EvaluationContext<UserData> {
682 pub fn new(user_data: UserData) -> Self {
684 Self {
685 user_data,
686 counters: BTreeMap::new(),
687 flags: BTreeMap::new(),
688 timestamps: BTreeMap::new(),
689 }
690 }
691
692 pub fn with_counter(mut self, key: impl Into<String>, value: u64) -> Self {
694 self.counters.insert(key.into(), value);
695 self
696 }
697
698 pub fn with_flag(mut self, key: impl Into<String>, value: bool) -> Self {
700 self.flags.insert(key.into(), value);
701 self
702 }
703
704 pub fn with_timestamp(mut self, key: impl Into<String>, value: Duration) -> Self {
706 self.timestamps.insert(key.into(), value);
707 self
708 }
709
710 pub fn user_data(&self) -> &UserData {
712 &self.user_data
713 }
714
715 pub fn counter(&self, key: &str) -> u64 {
717 self.counters.get(key).copied().unwrap_or_default()
718 }
719
720 pub fn flag(&self, key: &str) -> bool {
722 self.flags.get(key).copied().unwrap_or_default()
723 }
724
725 pub fn timestamp(&self, key: &str) -> Option<Duration> {
727 self.timestamps.get(key).copied()
728 }
729}
730
731pub trait Clock {
737 fn now(&self) -> Duration;
739}
740
741#[derive(Clone, Copy, Debug, Eq, PartialEq)]
743pub struct FixedClock(Duration);
744
745impl FixedClock {
746 pub const fn new(now: Duration) -> Self {
748 Self(now)
749 }
750}
751
752impl Clock for FixedClock {
753 fn now(&self) -> Duration {
754 self.0
755 }
756}
757
758pub struct MaxCount {
760 key: String,
761 maximum_count: u64,
762}
763
764impl MaxCount {
765 pub fn new(key: impl Into<String>, maximum_count: u64) -> Self {
767 Self {
768 key: key.into(),
769 maximum_count,
770 }
771 }
772}
773
774impl<UserData> Specification<EvaluationContext<UserData>> for MaxCount {
775 fn is_satisfied_by(&self, context: &EvaluationContext<UserData>) -> bool {
776 context.counter(&self.key) < self.maximum_count
777 }
778}
779
780pub struct Flag {
782 key: String,
783}
784
785impl Flag {
786 pub fn new(key: impl Into<String>) -> Self {
788 Self { key: key.into() }
789 }
790}
791
792impl<UserData> Specification<EvaluationContext<UserData>> for Flag {
793 fn is_satisfied_by(&self, context: &EvaluationContext<UserData>) -> bool {
794 context.flag(&self.key)
795 }
796}
797
798pub struct Cooldown<ClockType> {
803 key: String,
804 duration: Duration,
805 clock: ClockType,
806}
807
808impl<ClockType> Cooldown<ClockType> {
809 pub fn new(key: impl Into<String>, duration: Duration, clock: ClockType) -> Self {
811 Self {
812 key: key.into(),
813 duration,
814 clock,
815 }
816 }
817}
818
819impl<UserData, ClockType> Specification<EvaluationContext<UserData>> for Cooldown<ClockType>
820where
821 ClockType: Clock,
822{
823 fn is_satisfied_by(&self, context: &EvaluationContext<UserData>) -> bool {
824 context.timestamp(&self.key).is_none_or(|timestamp| {
825 let now = self.clock.now();
826 now >= timestamp && now - timestamp >= self.duration
827 })
828 }
829}
830
831#[cfg(test)]
832mod tests {
833 use std::cell::Cell;
834 use std::future::{Future, ready};
835 use std::rc::Rc;
836 use std::sync::{
837 Arc,
838 atomic::{AtomicUsize, Ordering},
839 };
840 use std::task::{Context, Poll, Waker};
841 use std::thread;
842 use std::time::Duration;
843
844 use super::{
845 AllOf, Always, AnyOf, AsyncSpecification, BoxedAsyncSpecification, BoxedSpecification,
846 Clock, Cooldown, DecisionSpecification, EvaluationContext, FirstMatch, FixedClock, Flag,
847 MaxCount, Never, SharedSpecification, Specification,
848 };
849
850 fn assert_send_sync<T: Send + Sync>() {}
851
852 struct CountingSpecification {
853 result: bool,
854 evaluations: Rc<Cell<u8>>,
855 }
856
857 impl CountingSpecification {
858 fn new(result: bool) -> Self {
859 Self {
860 result,
861 evaluations: Rc::new(Cell::new(0)),
862 }
863 }
864
865 fn counter(&self) -> Rc<Cell<u8>> {
866 Rc::clone(&self.evaluations)
867 }
868 }
869
870 impl Specification<i32> for CountingSpecification {
871 fn is_satisfied_by(&self, _: &i32) -> bool {
872 self.evaluations.set(self.evaluations.get() + 1);
873 self.result
874 }
875 }
876
877 #[derive(Clone)]
878 struct ImmediateAsyncSpecification {
879 result: Result<bool, &'static str>,
880 evaluations: Arc<AtomicUsize>,
881 }
882
883 impl ImmediateAsyncSpecification {
884 fn new(result: Result<bool, &'static str>) -> Self {
885 Self {
886 result,
887 evaluations: Arc::new(AtomicUsize::new(0)),
888 }
889 }
890
891 fn counter(&self) -> Arc<AtomicUsize> {
892 Arc::clone(&self.evaluations)
893 }
894 }
895
896 impl AsyncSpecification<i32> for ImmediateAsyncSpecification {
897 type Error = &'static str;
898
899 fn is_satisfied_by<'candidate>(
900 &'candidate self,
901 _: &'candidate i32,
902 ) -> impl Future<Output = Result<bool, Self::Error>> + Send + 'candidate {
903 self.evaluations.fetch_add(1, Ordering::Relaxed);
904 ready(self.result)
905 }
906 }
907
908 fn block_on<F: Future>(future: F) -> F::Output {
909 let waker = Waker::noop();
910 let mut context = Context::from_waker(waker);
911 let mut future = std::pin::pin!(future);
912
913 loop {
914 match future.as_mut().poll(&mut context) {
915 Poll::Ready(output) => return output,
916 Poll::Pending => std::thread::yield_now(),
917 }
918 }
919 }
920
921 #[test]
922 fn closures_are_specifications() {
923 let is_positive = |number: &i32| *number > 0;
924
925 assert!(is_positive.is_satisfied_by(&1));
926 assert!(!is_positive.is_satisfied_by(&0));
927 }
928
929 #[test]
930 fn and_requires_both_rules_to_succeed() {
931 let between = (|number: &i32| *number > 5).and(|number: &i32| *number < 10);
932
933 assert!(between.is_satisfied_by(&7));
934 assert!(!between.is_satisfied_by(&5));
935 assert!(!between.is_satisfied_by(&10));
936 }
937
938 #[test]
939 fn and_short_circuits_after_a_false_left_side() {
940 let left = CountingSpecification::new(false);
941 let right = CountingSpecification::new(true);
942 let left_evaluations = left.counter();
943 let right_evaluations = right.counter();
944 let specification = left.and(right);
945
946 assert!(!specification.is_satisfied_by(&0));
947 assert_eq!(left_evaluations.get(), 1);
948 assert_eq!(right_evaluations.get(), 0);
949 }
950
951 #[test]
952 fn or_accepts_either_rule() {
953 let outside = (|number: &i32| *number < 0).or(|number: &i32| *number > 10);
954
955 assert!(outside.is_satisfied_by(&-1));
956 assert!(outside.is_satisfied_by(&11));
957 assert!(!outside.is_satisfied_by(&5));
958 }
959
960 #[test]
961 fn or_short_circuits_after_a_true_left_side() {
962 let left = CountingSpecification::new(true);
963 let right = CountingSpecification::new(false);
964 let left_evaluations = left.counter();
965 let right_evaluations = right.counter();
966 let specification = left.or(right);
967
968 assert!(specification.is_satisfied_by(&0));
969 assert_eq!(left_evaluations.get(), 1);
970 assert_eq!(right_evaluations.get(), 0);
971 }
972
973 #[test]
974 fn not_negates_a_rule() {
975 let is_even = |number: &i32| *number % 2 == 0;
976 let is_odd = is_even.not();
977
978 assert!(is_odd.is_satisfied_by(&3));
979 assert!(!is_odd.is_satisfied_by(&4));
980 }
981
982 #[test]
983 fn constants_have_expected_results() {
984 assert!(Always.is_satisfied_by(&"candidate"));
985 assert!(!Never.is_satisfied_by(&"candidate"));
986 }
987
988 #[test]
989 fn all_of_uses_the_and_identity_and_short_circuits() {
990 let empty: [CountingSpecification; 0] = [];
991 let failing = CountingSpecification::new(false);
992 let skipped = CountingSpecification::new(true);
993 let failing_evaluations = failing.counter();
994 let skipped_evaluations = skipped.counter();
995 let specifications = [failing, skipped];
996
997 assert!(AllOf::new(&empty).is_satisfied_by(&0));
998 assert!(!AllOf::new(&specifications).is_satisfied_by(&0));
999 assert_eq!(failing_evaluations.get(), 1);
1000 assert_eq!(skipped_evaluations.get(), 0);
1001 }
1002
1003 #[test]
1004 fn any_of_uses_the_or_identity_and_short_circuits() {
1005 let empty: [CountingSpecification; 0] = [];
1006 let succeeding = CountingSpecification::new(true);
1007 let skipped = CountingSpecification::new(false);
1008 let succeeding_evaluations = succeeding.counter();
1009 let skipped_evaluations = skipped.counter();
1010 let specifications = [succeeding, skipped];
1011
1012 assert!(!AnyOf::new(&empty).is_satisfied_by(&0));
1013 assert!(AnyOf::new(&specifications).is_satisfied_by(&0));
1014 assert_eq!(succeeding_evaluations.get(), 1);
1015 assert_eq!(skipped_evaluations.get(), 0);
1016 }
1017
1018 #[test]
1019 fn boxed_specifications_store_heterogeneous_rules() {
1020 let specifications: Vec<BoxedSpecification<i32>> = vec![
1021 BoxedSpecification::new(|number: &i32| *number > 0),
1022 BoxedSpecification::new(Always),
1023 ];
1024
1025 assert!(specifications[0].is_satisfied_by(&1));
1026 assert!(specifications[1].is_satisfied_by(&-1));
1027 }
1028
1029 #[test]
1030 fn shared_specifications_are_cloneable() {
1031 let specification = SharedSpecification::new(|number: &i32| *number % 2 == 0);
1032 let shared = specification.clone();
1033
1034 assert!(shared.is_satisfied_by(&2));
1035 assert!(!specification.is_satisfied_by(&3));
1036 }
1037
1038 #[test]
1039 fn shared_api_and_immutable_context_expose_expected_auto_traits() {
1040 assert_send_sync::<SharedSpecification<i32>>();
1041 assert_send_sync::<EvaluationContext<String>>();
1042 assert_send_sync::<Cooldown<FixedClock>>();
1043 }
1044
1045 #[test]
1046 fn shared_specification_can_be_evaluated_concurrently() {
1047 let specification = SharedSpecification::new(|number: &u64| *number % 2 == 0);
1048 let handles = (0..8)
1049 .map(|_| {
1050 let specification = specification.clone();
1051 thread::spawn(move || {
1052 (0..1_000_u64)
1053 .filter(|number| specification.is_satisfied_by(number))
1054 .count()
1055 })
1056 })
1057 .collect::<Vec<_>>();
1058
1059 for handle in handles {
1060 assert_eq!(handle.join().expect("worker thread should finish"), 500);
1061 }
1062 }
1063
1064 #[test]
1065 fn async_and_short_circuits_and_propagates_errors() {
1066 let left = ImmediateAsyncSpecification::new(Ok(false));
1067 let right = ImmediateAsyncSpecification::new(Err("right"));
1068 let right_evaluations = right.counter();
1069 let specification = left.and(right);
1070
1071 assert_eq!(block_on(specification.is_satisfied_by(&0)), Ok(false));
1072 assert_eq!(right_evaluations.load(Ordering::Relaxed), 0);
1073
1074 let failing_left = ImmediateAsyncSpecification::new(Err("left"));
1075 let successful_right = ImmediateAsyncSpecification::new(Ok(true));
1076 let successful_right_evaluations = successful_right.counter();
1077 let specification = failing_left.and(successful_right);
1078
1079 assert_eq!(block_on(specification.is_satisfied_by(&0)), Err("left"));
1080 assert_eq!(successful_right_evaluations.load(Ordering::Relaxed), 0);
1081 }
1082
1083 #[test]
1084 fn async_or_short_circuits_and_async_not_negates() {
1085 let left = ImmediateAsyncSpecification::new(Ok(true));
1086 let right = ImmediateAsyncSpecification::new(Err("right"));
1087 let right_evaluations = right.counter();
1088 let specification = left.or(right);
1089
1090 assert_eq!(block_on(specification.is_satisfied_by(&0)), Ok(true));
1091 assert_eq!(right_evaluations.load(Ordering::Relaxed), 0);
1092
1093 let specification = ImmediateAsyncSpecification::new(Ok(false)).not();
1094 assert_eq!(block_on(specification.is_satisfied_by(&0)), Ok(true));
1095
1096 let specification = ImmediateAsyncSpecification::new(Err("failure")).not();
1097 assert_eq!(block_on(specification.is_satisfied_by(&0)), Err("failure"));
1098 }
1099
1100 #[test]
1101 fn boxed_async_specification_is_an_explicit_dynamic_boundary() {
1102 assert_send_sync::<BoxedAsyncSpecification<i32, &'static str>>();
1103 let specification =
1104 BoxedAsyncSpecification::new(ImmediateAsyncSpecification::new(Ok(true)));
1105
1106 assert_eq!(block_on(specification.is_satisfied_by(&0)), Ok(true));
1107 }
1108
1109 #[derive(Clone)]
1110 struct CountingClock {
1111 now: Duration,
1112 reads: Arc<AtomicUsize>,
1113 }
1114
1115 impl Clock for CountingClock {
1116 fn now(&self) -> Duration {
1117 self.reads.fetch_add(1, Ordering::Relaxed);
1118 self.now
1119 }
1120 }
1121
1122 #[test]
1123 fn injected_thread_safe_clock_can_back_a_shared_cooldown() {
1124 let reads = Arc::new(AtomicUsize::new(0));
1125 let clock = CountingClock {
1126 now: Duration::from_secs(100),
1127 reads: Arc::clone(&reads),
1128 };
1129 let specification =
1130 SharedSpecification::new(Cooldown::new("last_login", Duration::from_secs(60), clock));
1131 let handles = (0..4)
1132 .map(|_| {
1133 let specification = specification.clone();
1134 let context = EvaluationContext::new(())
1135 .with_timestamp("last_login", Duration::from_secs(40));
1136 thread::spawn(move || specification.is_satisfied_by(&context))
1137 })
1138 .collect::<Vec<_>>();
1139
1140 for handle in handles {
1141 assert!(handle.join().expect("worker thread should finish"));
1142 }
1143 assert_eq!(reads.load(Ordering::Relaxed), 4);
1144 }
1145
1146 #[test]
1147 fn first_match_returns_the_first_typed_decision_and_short_circuits() {
1148 let first = CountingSpecification::new(true);
1149 let later = CountingSpecification::new(true);
1150 let first_evaluations = first.counter();
1151 let later_evaluations = later.counter();
1152 let mut decisions = FirstMatch::new();
1153 decisions.push(first, "first");
1154 decisions.push(later, "later");
1155
1156 assert_eq!(decisions.decide(&0), Some(&"first"));
1157 assert_eq!(first_evaluations.get(), 1);
1158 assert_eq!(later_evaluations.get(), 0);
1159 }
1160
1161 #[test]
1162 fn first_match_exposes_no_match_and_explicit_fallback() {
1163 let mut decisions = FirstMatch::new();
1164 decisions.push(|number: &i32| *number > 10, "large");
1165 let fallback = "default";
1166
1167 assert_eq!(decisions.decide(&5), None);
1168 assert_eq!(decisions.decide_or(&5, &fallback), &fallback);
1169 }
1170
1171 #[test]
1172 fn context_preserves_typed_user_data_and_default_values() {
1173 let context = EvaluationContext::new("user").with_counter("visits", 3);
1174
1175 assert_eq!(context.user_data(), &"user");
1176 assert_eq!(context.counter("visits"), 3);
1177 assert_eq!(context.counter("missing"), 0);
1178 assert!(!context.flag("missing"));
1179 assert_eq!(context.timestamp("missing"), None);
1180 }
1181
1182 #[test]
1183 fn max_count_uses_a_strict_boundary() {
1184 let specification = MaxCount::new("attempts", 3);
1185
1186 assert!(
1187 specification.is_satisfied_by(&EvaluationContext::new(()).with_counter("attempts", 2))
1188 );
1189 assert!(
1190 !specification.is_satisfied_by(&EvaluationContext::new(()).with_counter("attempts", 3))
1191 );
1192 assert!(specification.is_satisfied_by(&EvaluationContext::new(())));
1193 }
1194
1195 #[test]
1196 fn flag_requires_a_true_context_value() {
1197 let specification = Flag::new("premium");
1198
1199 assert!(
1200 specification.is_satisfied_by(&EvaluationContext::new(()).with_flag("premium", true))
1201 );
1202 assert!(
1203 !specification.is_satisfied_by(&EvaluationContext::new(()).with_flag("premium", false))
1204 );
1205 assert!(!specification.is_satisfied_by(&EvaluationContext::new(())));
1206 }
1207
1208 #[test]
1209 fn cooldown_uses_an_injected_clock_and_inclusive_boundary() {
1210 let specification = Cooldown::new(
1211 "last_login",
1212 Duration::from_secs(60),
1213 FixedClock::new(Duration::from_secs(100)),
1214 );
1215
1216 assert!(specification.is_satisfied_by(&EvaluationContext::new(())));
1217 assert!(specification.is_satisfied_by(
1218 &EvaluationContext::new(()).with_timestamp("last_login", Duration::from_secs(40))
1219 ));
1220 assert!(!specification.is_satisfied_by(
1221 &EvaluationContext::new(()).with_timestamp("last_login", Duration::from_secs(41))
1222 ));
1223 }
1224
1225 #[test]
1226 fn cooldown_rejects_future_timestamps_even_at_zero_duration() {
1227 let specification = Cooldown::new(
1228 "last_login",
1229 Duration::ZERO,
1230 FixedClock::new(Duration::from_secs(100)),
1231 );
1232
1233 assert!(!specification.is_satisfied_by(
1234 &EvaluationContext::new(()).with_timestamp("last_login", Duration::from_secs(101))
1235 ));
1236 }
1237}