1#![forbid(unsafe_code)]
2use std::{collections::BTreeMap, future::Future, pin::Pin, sync::Arc, time::Duration};
20
21pub trait Specification<T: ?Sized> {
27 fn is_satisfied_by(&self, candidate: &T) -> bool;
29
30 fn and<Other>(self, other: Other) -> And<Self, Other>
35 where
36 Self: Sized,
37 Other: Specification<T>,
38 {
39 And {
40 left: self,
41 right: other,
42 }
43 }
44
45 fn or<Other>(self, other: Other) -> Or<Self, Other>
50 where
51 Self: Sized,
52 Other: Specification<T>,
53 {
54 Or {
55 left: self,
56 right: other,
57 }
58 }
59
60 fn not(self) -> Not<Self>
62 where
63 Self: Sized,
64 {
65 Not { inner: self }
66 }
67}
68
69impl<T: ?Sized, Predicate> Specification<T> for Predicate
70where
71 Predicate: Fn(&T) -> bool,
72{
73 fn is_satisfied_by(&self, candidate: &T) -> bool {
74 self(candidate)
75 }
76}
77
78pub trait AsyncSpecification<T: ?Sized + Sync>: Sync {
92 type Error: Send;
94
95 fn is_satisfied_by<'candidate>(
97 &'candidate self,
98 candidate: &'candidate T,
99 ) -> impl Future<Output = Result<bool, Self::Error>> + Send + 'candidate;
100
101 fn and<Other>(self, other: Other) -> AsyncAnd<Self, Other>
103 where
104 Self: Sized,
105 Other: AsyncSpecification<T, Error = Self::Error>,
106 {
107 AsyncAnd {
108 left: self,
109 right: other,
110 }
111 }
112
113 fn or<Other>(self, other: Other) -> AsyncOr<Self, Other>
115 where
116 Self: Sized,
117 Other: AsyncSpecification<T, Error = Self::Error>,
118 {
119 AsyncOr {
120 left: self,
121 right: other,
122 }
123 }
124
125 fn not(self) -> AsyncNot<Self>
127 where
128 Self: Sized,
129 {
130 AsyncNot { inner: self }
131 }
132}
133
134pub struct AsyncAnd<Left, Right> {
136 left: Left,
137 right: Right,
138}
139
140impl<T: ?Sized + Sync, Left, Right> AsyncSpecification<T> for AsyncAnd<Left, Right>
141where
142 Left: AsyncSpecification<T>,
143 Right: AsyncSpecification<T, Error = Left::Error>,
144{
145 type Error = Left::Error;
146
147 #[allow(clippy::manual_async_fn)]
148 fn is_satisfied_by<'candidate>(
149 &'candidate self,
150 candidate: &'candidate T,
151 ) -> impl Future<Output = Result<bool, Self::Error>> + Send + 'candidate {
152 async move {
153 if !self.left.is_satisfied_by(candidate).await? {
154 return Ok(false);
155 }
156 self.right.is_satisfied_by(candidate).await
157 }
158 }
159}
160
161pub struct AsyncOr<Left, Right> {
163 left: Left,
164 right: Right,
165}
166
167impl<T: ?Sized + Sync, Left, Right> AsyncSpecification<T> for AsyncOr<Left, Right>
168where
169 Left: AsyncSpecification<T>,
170 Right: AsyncSpecification<T, Error = Left::Error>,
171{
172 type Error = Left::Error;
173
174 #[allow(clippy::manual_async_fn)]
175 fn is_satisfied_by<'candidate>(
176 &'candidate self,
177 candidate: &'candidate T,
178 ) -> impl Future<Output = Result<bool, Self::Error>> + Send + 'candidate {
179 async move {
180 if self.left.is_satisfied_by(candidate).await? {
181 return Ok(true);
182 }
183 self.right.is_satisfied_by(candidate).await
184 }
185 }
186}
187
188pub struct AsyncNot<Inner> {
190 inner: Inner,
191}
192
193impl<T: ?Sized + Sync, Inner> AsyncSpecification<T> for AsyncNot<Inner>
194where
195 Inner: AsyncSpecification<T>,
196{
197 type Error = Inner::Error;
198
199 #[allow(clippy::manual_async_fn)]
200 fn is_satisfied_by<'candidate>(
201 &'candidate self,
202 candidate: &'candidate T,
203 ) -> impl Future<Output = Result<bool, Self::Error>> + Send + 'candidate {
204 async move {
205 self.inner
206 .is_satisfied_by(candidate)
207 .await
208 .map(|result| !result)
209 }
210 }
211}
212
213pub type BoxFuture<'future, Error> =
215 Pin<Box<dyn Future<Output = Result<bool, Error>> + Send + 'future>>;
216
217trait DynAsyncSpecification<T: ?Sized + Sync, Error>: Send + Sync {
218 fn evaluate<'candidate>(
219 &'candidate self,
220 candidate: &'candidate T,
221 ) -> BoxFuture<'candidate, Error>;
222}
223
224impl<T: ?Sized + Sync, Error, Concrete> DynAsyncSpecification<T, Error> for Concrete
225where
226 Concrete: AsyncSpecification<T, Error = Error> + Send + Sync,
227{
228 fn evaluate<'candidate>(
229 &'candidate self,
230 candidate: &'candidate T,
231 ) -> BoxFuture<'candidate, Error> {
232 Box::pin(self.is_satisfied_by(candidate))
233 }
234}
235
236pub struct BoxedAsyncSpecification<T: ?Sized + Sync, Error: Send> {
242 inner: Box<dyn DynAsyncSpecification<T, Error>>,
243}
244
245impl<T: ?Sized + Sync, Error: Send> BoxedAsyncSpecification<T, Error> {
246 pub fn new<Concrete>(specification: Concrete) -> Self
248 where
249 Concrete: AsyncSpecification<T, Error = Error> + Send + Sync + 'static,
250 {
251 Self {
252 inner: Box::new(specification),
253 }
254 }
255}
256
257impl<T: ?Sized + Sync, Error: Send> AsyncSpecification<T> for BoxedAsyncSpecification<T, Error> {
258 type Error = Error;
259
260 #[allow(clippy::manual_async_fn)]
261 fn is_satisfied_by<'candidate>(
262 &'candidate self,
263 candidate: &'candidate T,
264 ) -> impl Future<Output = Result<bool, Self::Error>> + Send + 'candidate {
265 self.inner.evaluate(candidate)
266 }
267}
268
269pub struct And<Left, Right> {
275 left: Left,
276 right: Right,
277}
278
279impl<T: ?Sized, Left, Right> Specification<T> for And<Left, Right>
280where
281 Left: Specification<T>,
282 Right: Specification<T>,
283{
284 fn is_satisfied_by(&self, candidate: &T) -> bool {
285 self.left.is_satisfied_by(candidate) && self.right.is_satisfied_by(candidate)
286 }
287}
288
289pub struct Or<Left, Right> {
295 left: Left,
296 right: Right,
297}
298
299impl<T: ?Sized, Left, Right> Specification<T> for Or<Left, Right>
300where
301 Left: Specification<T>,
302 Right: Specification<T>,
303{
304 fn is_satisfied_by(&self, candidate: &T) -> bool {
305 self.left.is_satisfied_by(candidate) || self.right.is_satisfied_by(candidate)
306 }
307}
308
309pub struct Not<Inner> {
313 inner: Inner,
314}
315
316impl<T: ?Sized, Inner> Specification<T> for Not<Inner>
317where
318 Inner: Specification<T>,
319{
320 fn is_satisfied_by(&self, candidate: &T) -> bool {
321 !self.inner.is_satisfied_by(candidate)
322 }
323}
324
325#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
327pub struct Always;
328
329impl<T: ?Sized> Specification<T> for Always {
330 fn is_satisfied_by(&self, _: &T) -> bool {
331 true
332 }
333}
334
335#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
337pub struct Never;
338
339impl<T: ?Sized> Specification<T> for Never {
340 fn is_satisfied_by(&self, _: &T) -> bool {
341 false
342 }
343}
344
345pub struct AllOf<'specifications, SpecificationType> {
350 specifications: &'specifications [SpecificationType],
351}
352
353impl<'specifications, SpecificationType> AllOf<'specifications, SpecificationType> {
354 pub const fn new(specifications: &'specifications [SpecificationType]) -> Self {
356 Self { specifications }
357 }
358}
359
360impl<T: ?Sized, SpecificationType> Specification<T> for AllOf<'_, SpecificationType>
361where
362 SpecificationType: Specification<T>,
363{
364 fn is_satisfied_by(&self, candidate: &T) -> bool {
365 self.specifications
366 .iter()
367 .all(|specification| specification.is_satisfied_by(candidate))
368 }
369}
370
371pub struct AnyOf<'specifications, SpecificationType> {
376 specifications: &'specifications [SpecificationType],
377}
378
379impl<'specifications, SpecificationType> AnyOf<'specifications, SpecificationType> {
380 pub const fn new(specifications: &'specifications [SpecificationType]) -> Self {
382 Self { specifications }
383 }
384}
385
386impl<T: ?Sized, SpecificationType> Specification<T> for AnyOf<'_, SpecificationType>
387where
388 SpecificationType: Specification<T>,
389{
390 fn is_satisfied_by(&self, candidate: &T) -> bool {
391 self.specifications
392 .iter()
393 .any(|specification| specification.is_satisfied_by(candidate))
394 }
395}
396
397pub struct BoxedSpecification<T: ?Sized> {
402 inner: Box<dyn Specification<T>>,
403}
404
405impl<T: ?Sized> BoxedSpecification<T> {
406 pub fn new<Concrete>(specification: Concrete) -> Self
408 where
409 Concrete: Specification<T> + 'static,
410 {
411 Self {
412 inner: Box::new(specification),
413 }
414 }
415}
416
417impl<T: ?Sized> Specification<T> for BoxedSpecification<T> {
418 fn is_satisfied_by(&self, candidate: &T) -> bool {
419 self.inner.is_satisfied_by(candidate)
420 }
421}
422
423pub struct SharedSpecification<T: ?Sized> {
440 inner: Arc<dyn Specification<T> + Send + Sync>,
441}
442
443impl<T: ?Sized> Clone for SharedSpecification<T> {
444 fn clone(&self) -> Self {
445 Self {
446 inner: Arc::clone(&self.inner),
447 }
448 }
449}
450
451impl<T: ?Sized> SharedSpecification<T> {
452 pub fn new<Concrete>(specification: Concrete) -> Self
454 where
455 Concrete: Specification<T> + Send + Sync + 'static,
456 {
457 Self {
458 inner: Arc::new(specification),
459 }
460 }
461}
462
463impl<T: ?Sized> Specification<T> for SharedSpecification<T> {
464 fn is_satisfied_by(&self, candidate: &T) -> bool {
465 self.inner.is_satisfied_by(candidate)
466 }
467}
468
469pub trait DecisionSpecification<T: ?Sized> {
471 type Decision;
473
474 fn decide(&self, candidate: &T) -> Option<&Self::Decision>;
476}
477
478pub struct FirstMatch<T: ?Sized, Decision> {
483 rules: Vec<(BoxedSpecification<T>, Decision)>,
484}
485
486impl<T: ?Sized, Decision> FirstMatch<T, Decision> {
487 pub const fn new() -> Self {
489 Self { rules: Vec::new() }
490 }
491
492 pub fn push<Concrete>(&mut self, specification: Concrete, decision: Decision)
494 where
495 Concrete: Specification<T> + 'static,
496 {
497 self.rules
498 .push((BoxedSpecification::new(specification), decision));
499 }
500
501 pub fn decide_or<'decision>(
503 &'decision self,
504 candidate: &T,
505 fallback: &'decision Decision,
506 ) -> &'decision Decision {
507 self.decide(candidate).unwrap_or(fallback)
508 }
509}
510
511impl<T: ?Sized, Decision> Default for FirstMatch<T, Decision> {
512 fn default() -> Self {
513 Self::new()
514 }
515}
516
517impl<T: ?Sized, Decision> DecisionSpecification<T> for FirstMatch<T, Decision> {
518 type Decision = Decision;
519
520 fn decide(&self, candidate: &T) -> Option<&Decision> {
521 self.rules.iter().find_map(|(specification, decision)| {
522 specification.is_satisfied_by(candidate).then_some(decision)
523 })
524 }
525}
526
527pub struct EvaluationContext<UserData> {
535 user_data: UserData,
536 counters: BTreeMap<String, u64>,
537 flags: BTreeMap<String, bool>,
538 timestamps: BTreeMap<String, Duration>,
539}
540
541impl<UserData> EvaluationContext<UserData> {
542 pub fn new(user_data: UserData) -> Self {
544 Self {
545 user_data,
546 counters: BTreeMap::new(),
547 flags: BTreeMap::new(),
548 timestamps: BTreeMap::new(),
549 }
550 }
551
552 pub fn with_counter(mut self, key: impl Into<String>, value: u64) -> Self {
554 self.counters.insert(key.into(), value);
555 self
556 }
557
558 pub fn with_flag(mut self, key: impl Into<String>, value: bool) -> Self {
560 self.flags.insert(key.into(), value);
561 self
562 }
563
564 pub fn with_timestamp(mut self, key: impl Into<String>, value: Duration) -> Self {
566 self.timestamps.insert(key.into(), value);
567 self
568 }
569
570 pub fn user_data(&self) -> &UserData {
572 &self.user_data
573 }
574
575 pub fn counter(&self, key: &str) -> u64 {
577 self.counters.get(key).copied().unwrap_or_default()
578 }
579
580 pub fn flag(&self, key: &str) -> bool {
582 self.flags.get(key).copied().unwrap_or_default()
583 }
584
585 pub fn timestamp(&self, key: &str) -> Option<Duration> {
587 self.timestamps.get(key).copied()
588 }
589}
590
591pub trait Clock {
597 fn now(&self) -> Duration;
599}
600
601#[derive(Clone, Copy, Debug, Eq, PartialEq)]
603pub struct FixedClock(Duration);
604
605impl FixedClock {
606 pub const fn new(now: Duration) -> Self {
608 Self(now)
609 }
610}
611
612impl Clock for FixedClock {
613 fn now(&self) -> Duration {
614 self.0
615 }
616}
617
618pub struct MaxCount {
620 key: String,
621 maximum_count: u64,
622}
623
624impl MaxCount {
625 pub fn new(key: impl Into<String>, maximum_count: u64) -> Self {
627 Self {
628 key: key.into(),
629 maximum_count,
630 }
631 }
632}
633
634impl<UserData> Specification<EvaluationContext<UserData>> for MaxCount {
635 fn is_satisfied_by(&self, context: &EvaluationContext<UserData>) -> bool {
636 context.counter(&self.key) < self.maximum_count
637 }
638}
639
640pub struct Flag {
642 key: String,
643}
644
645impl Flag {
646 pub fn new(key: impl Into<String>) -> Self {
648 Self { key: key.into() }
649 }
650}
651
652impl<UserData> Specification<EvaluationContext<UserData>> for Flag {
653 fn is_satisfied_by(&self, context: &EvaluationContext<UserData>) -> bool {
654 context.flag(&self.key)
655 }
656}
657
658pub struct Cooldown<ClockType> {
663 key: String,
664 duration: Duration,
665 clock: ClockType,
666}
667
668impl<ClockType> Cooldown<ClockType> {
669 pub fn new(key: impl Into<String>, duration: Duration, clock: ClockType) -> Self {
671 Self {
672 key: key.into(),
673 duration,
674 clock,
675 }
676 }
677}
678
679impl<UserData, ClockType> Specification<EvaluationContext<UserData>> for Cooldown<ClockType>
680where
681 ClockType: Clock,
682{
683 fn is_satisfied_by(&self, context: &EvaluationContext<UserData>) -> bool {
684 context.timestamp(&self.key).is_none_or(|timestamp| {
685 let now = self.clock.now();
686 now >= timestamp && now - timestamp >= self.duration
687 })
688 }
689}
690
691#[cfg(test)]
692mod tests {
693 use std::cell::Cell;
694 use std::future::{Future, ready};
695 use std::rc::Rc;
696 use std::sync::{
697 Arc,
698 atomic::{AtomicUsize, Ordering},
699 };
700 use std::task::{Context, Poll, Waker};
701 use std::thread;
702 use std::time::Duration;
703
704 use super::{
705 AllOf, Always, AnyOf, AsyncSpecification, BoxedAsyncSpecification, BoxedSpecification,
706 Clock, Cooldown, DecisionSpecification, EvaluationContext, FirstMatch, FixedClock, Flag,
707 MaxCount, Never, SharedSpecification, Specification,
708 };
709
710 fn assert_send_sync<T: Send + Sync>() {}
711
712 struct CountingSpecification {
713 result: bool,
714 evaluations: Rc<Cell<u8>>,
715 }
716
717 impl CountingSpecification {
718 fn new(result: bool) -> Self {
719 Self {
720 result,
721 evaluations: Rc::new(Cell::new(0)),
722 }
723 }
724
725 fn counter(&self) -> Rc<Cell<u8>> {
726 Rc::clone(&self.evaluations)
727 }
728 }
729
730 impl Specification<i32> for CountingSpecification {
731 fn is_satisfied_by(&self, _: &i32) -> bool {
732 self.evaluations.set(self.evaluations.get() + 1);
733 self.result
734 }
735 }
736
737 #[derive(Clone)]
738 struct ImmediateAsyncSpecification {
739 result: Result<bool, &'static str>,
740 evaluations: Arc<AtomicUsize>,
741 }
742
743 impl ImmediateAsyncSpecification {
744 fn new(result: Result<bool, &'static str>) -> Self {
745 Self {
746 result,
747 evaluations: Arc::new(AtomicUsize::new(0)),
748 }
749 }
750
751 fn counter(&self) -> Arc<AtomicUsize> {
752 Arc::clone(&self.evaluations)
753 }
754 }
755
756 impl AsyncSpecification<i32> for ImmediateAsyncSpecification {
757 type Error = &'static str;
758
759 fn is_satisfied_by<'candidate>(
760 &'candidate self,
761 _: &'candidate i32,
762 ) -> impl Future<Output = Result<bool, Self::Error>> + Send + 'candidate {
763 self.evaluations.fetch_add(1, Ordering::Relaxed);
764 ready(self.result)
765 }
766 }
767
768 fn block_on<F: Future>(future: F) -> F::Output {
769 let waker = Waker::noop();
770 let mut context = Context::from_waker(waker);
771 let mut future = std::pin::pin!(future);
772
773 loop {
774 match future.as_mut().poll(&mut context) {
775 Poll::Ready(output) => return output,
776 Poll::Pending => std::thread::yield_now(),
777 }
778 }
779 }
780
781 #[test]
782 fn closures_are_specifications() {
783 let is_positive = |number: &i32| *number > 0;
784
785 assert!(is_positive.is_satisfied_by(&1));
786 assert!(!is_positive.is_satisfied_by(&0));
787 }
788
789 #[test]
790 fn and_requires_both_rules_to_succeed() {
791 let between = (|number: &i32| *number > 5).and(|number: &i32| *number < 10);
792
793 assert!(between.is_satisfied_by(&7));
794 assert!(!between.is_satisfied_by(&5));
795 assert!(!between.is_satisfied_by(&10));
796 }
797
798 #[test]
799 fn and_short_circuits_after_a_false_left_side() {
800 let left = CountingSpecification::new(false);
801 let right = CountingSpecification::new(true);
802 let left_evaluations = left.counter();
803 let right_evaluations = right.counter();
804 let specification = left.and(right);
805
806 assert!(!specification.is_satisfied_by(&0));
807 assert_eq!(left_evaluations.get(), 1);
808 assert_eq!(right_evaluations.get(), 0);
809 }
810
811 #[test]
812 fn or_accepts_either_rule() {
813 let outside = (|number: &i32| *number < 0).or(|number: &i32| *number > 10);
814
815 assert!(outside.is_satisfied_by(&-1));
816 assert!(outside.is_satisfied_by(&11));
817 assert!(!outside.is_satisfied_by(&5));
818 }
819
820 #[test]
821 fn or_short_circuits_after_a_true_left_side() {
822 let left = CountingSpecification::new(true);
823 let right = CountingSpecification::new(false);
824 let left_evaluations = left.counter();
825 let right_evaluations = right.counter();
826 let specification = left.or(right);
827
828 assert!(specification.is_satisfied_by(&0));
829 assert_eq!(left_evaluations.get(), 1);
830 assert_eq!(right_evaluations.get(), 0);
831 }
832
833 #[test]
834 fn not_negates_a_rule() {
835 let is_even = |number: &i32| *number % 2 == 0;
836 let is_odd = is_even.not();
837
838 assert!(is_odd.is_satisfied_by(&3));
839 assert!(!is_odd.is_satisfied_by(&4));
840 }
841
842 #[test]
843 fn constants_have_expected_results() {
844 assert!(Always.is_satisfied_by(&"candidate"));
845 assert!(!Never.is_satisfied_by(&"candidate"));
846 }
847
848 #[test]
849 fn all_of_uses_the_and_identity_and_short_circuits() {
850 let empty: [CountingSpecification; 0] = [];
851 let failing = CountingSpecification::new(false);
852 let skipped = CountingSpecification::new(true);
853 let failing_evaluations = failing.counter();
854 let skipped_evaluations = skipped.counter();
855 let specifications = [failing, skipped];
856
857 assert!(AllOf::new(&empty).is_satisfied_by(&0));
858 assert!(!AllOf::new(&specifications).is_satisfied_by(&0));
859 assert_eq!(failing_evaluations.get(), 1);
860 assert_eq!(skipped_evaluations.get(), 0);
861 }
862
863 #[test]
864 fn any_of_uses_the_or_identity_and_short_circuits() {
865 let empty: [CountingSpecification; 0] = [];
866 let succeeding = CountingSpecification::new(true);
867 let skipped = CountingSpecification::new(false);
868 let succeeding_evaluations = succeeding.counter();
869 let skipped_evaluations = skipped.counter();
870 let specifications = [succeeding, skipped];
871
872 assert!(!AnyOf::new(&empty).is_satisfied_by(&0));
873 assert!(AnyOf::new(&specifications).is_satisfied_by(&0));
874 assert_eq!(succeeding_evaluations.get(), 1);
875 assert_eq!(skipped_evaluations.get(), 0);
876 }
877
878 #[test]
879 fn boxed_specifications_store_heterogeneous_rules() {
880 let specifications: Vec<BoxedSpecification<i32>> = vec![
881 BoxedSpecification::new(|number: &i32| *number > 0),
882 BoxedSpecification::new(Always),
883 ];
884
885 assert!(specifications[0].is_satisfied_by(&1));
886 assert!(specifications[1].is_satisfied_by(&-1));
887 }
888
889 #[test]
890 fn shared_specifications_are_cloneable() {
891 let specification = SharedSpecification::new(|number: &i32| *number % 2 == 0);
892 let shared = specification.clone();
893
894 assert!(shared.is_satisfied_by(&2));
895 assert!(!specification.is_satisfied_by(&3));
896 }
897
898 #[test]
899 fn shared_api_and_immutable_context_expose_expected_auto_traits() {
900 assert_send_sync::<SharedSpecification<i32>>();
901 assert_send_sync::<EvaluationContext<String>>();
902 assert_send_sync::<Cooldown<FixedClock>>();
903 }
904
905 #[test]
906 fn shared_specification_can_be_evaluated_concurrently() {
907 let specification = SharedSpecification::new(|number: &u64| *number % 2 == 0);
908 let handles = (0..8)
909 .map(|_| {
910 let specification = specification.clone();
911 thread::spawn(move || {
912 (0..1_000_u64)
913 .filter(|number| specification.is_satisfied_by(number))
914 .count()
915 })
916 })
917 .collect::<Vec<_>>();
918
919 for handle in handles {
920 assert_eq!(handle.join().expect("worker thread should finish"), 500);
921 }
922 }
923
924 #[test]
925 fn async_and_short_circuits_and_propagates_errors() {
926 let left = ImmediateAsyncSpecification::new(Ok(false));
927 let right = ImmediateAsyncSpecification::new(Err("right"));
928 let right_evaluations = right.counter();
929 let specification = left.and(right);
930
931 assert_eq!(block_on(specification.is_satisfied_by(&0)), Ok(false));
932 assert_eq!(right_evaluations.load(Ordering::Relaxed), 0);
933
934 let failing_left = ImmediateAsyncSpecification::new(Err("left"));
935 let successful_right = ImmediateAsyncSpecification::new(Ok(true));
936 let successful_right_evaluations = successful_right.counter();
937 let specification = failing_left.and(successful_right);
938
939 assert_eq!(block_on(specification.is_satisfied_by(&0)), Err("left"));
940 assert_eq!(successful_right_evaluations.load(Ordering::Relaxed), 0);
941 }
942
943 #[test]
944 fn async_or_short_circuits_and_async_not_negates() {
945 let left = ImmediateAsyncSpecification::new(Ok(true));
946 let right = ImmediateAsyncSpecification::new(Err("right"));
947 let right_evaluations = right.counter();
948 let specification = left.or(right);
949
950 assert_eq!(block_on(specification.is_satisfied_by(&0)), Ok(true));
951 assert_eq!(right_evaluations.load(Ordering::Relaxed), 0);
952
953 let specification = ImmediateAsyncSpecification::new(Ok(false)).not();
954 assert_eq!(block_on(specification.is_satisfied_by(&0)), Ok(true));
955
956 let specification = ImmediateAsyncSpecification::new(Err("failure")).not();
957 assert_eq!(block_on(specification.is_satisfied_by(&0)), Err("failure"));
958 }
959
960 #[test]
961 fn boxed_async_specification_is_an_explicit_dynamic_boundary() {
962 assert_send_sync::<BoxedAsyncSpecification<i32, &'static str>>();
963 let specification =
964 BoxedAsyncSpecification::new(ImmediateAsyncSpecification::new(Ok(true)));
965
966 assert_eq!(block_on(specification.is_satisfied_by(&0)), Ok(true));
967 }
968
969 #[derive(Clone)]
970 struct CountingClock {
971 now: Duration,
972 reads: Arc<AtomicUsize>,
973 }
974
975 impl Clock for CountingClock {
976 fn now(&self) -> Duration {
977 self.reads.fetch_add(1, Ordering::Relaxed);
978 self.now
979 }
980 }
981
982 #[test]
983 fn injected_thread_safe_clock_can_back_a_shared_cooldown() {
984 let reads = Arc::new(AtomicUsize::new(0));
985 let clock = CountingClock {
986 now: Duration::from_secs(100),
987 reads: Arc::clone(&reads),
988 };
989 let specification =
990 SharedSpecification::new(Cooldown::new("last_login", Duration::from_secs(60), clock));
991 let handles = (0..4)
992 .map(|_| {
993 let specification = specification.clone();
994 let context = EvaluationContext::new(())
995 .with_timestamp("last_login", Duration::from_secs(40));
996 thread::spawn(move || specification.is_satisfied_by(&context))
997 })
998 .collect::<Vec<_>>();
999
1000 for handle in handles {
1001 assert!(handle.join().expect("worker thread should finish"));
1002 }
1003 assert_eq!(reads.load(Ordering::Relaxed), 4);
1004 }
1005
1006 #[test]
1007 fn first_match_returns_the_first_typed_decision_and_short_circuits() {
1008 let first = CountingSpecification::new(true);
1009 let later = CountingSpecification::new(true);
1010 let first_evaluations = first.counter();
1011 let later_evaluations = later.counter();
1012 let mut decisions = FirstMatch::new();
1013 decisions.push(first, "first");
1014 decisions.push(later, "later");
1015
1016 assert_eq!(decisions.decide(&0), Some(&"first"));
1017 assert_eq!(first_evaluations.get(), 1);
1018 assert_eq!(later_evaluations.get(), 0);
1019 }
1020
1021 #[test]
1022 fn first_match_exposes_no_match_and_explicit_fallback() {
1023 let mut decisions = FirstMatch::new();
1024 decisions.push(|number: &i32| *number > 10, "large");
1025 let fallback = "default";
1026
1027 assert_eq!(decisions.decide(&5), None);
1028 assert_eq!(decisions.decide_or(&5, &fallback), &fallback);
1029 }
1030
1031 #[test]
1032 fn context_preserves_typed_user_data_and_default_values() {
1033 let context = EvaluationContext::new("user").with_counter("visits", 3);
1034
1035 assert_eq!(context.user_data(), &"user");
1036 assert_eq!(context.counter("visits"), 3);
1037 assert_eq!(context.counter("missing"), 0);
1038 assert!(!context.flag("missing"));
1039 assert_eq!(context.timestamp("missing"), None);
1040 }
1041
1042 #[test]
1043 fn max_count_uses_a_strict_boundary() {
1044 let specification = MaxCount::new("attempts", 3);
1045
1046 assert!(
1047 specification.is_satisfied_by(&EvaluationContext::new(()).with_counter("attempts", 2))
1048 );
1049 assert!(
1050 !specification.is_satisfied_by(&EvaluationContext::new(()).with_counter("attempts", 3))
1051 );
1052 assert!(specification.is_satisfied_by(&EvaluationContext::new(())));
1053 }
1054
1055 #[test]
1056 fn flag_requires_a_true_context_value() {
1057 let specification = Flag::new("premium");
1058
1059 assert!(
1060 specification.is_satisfied_by(&EvaluationContext::new(()).with_flag("premium", true))
1061 );
1062 assert!(
1063 !specification.is_satisfied_by(&EvaluationContext::new(()).with_flag("premium", false))
1064 );
1065 assert!(!specification.is_satisfied_by(&EvaluationContext::new(())));
1066 }
1067
1068 #[test]
1069 fn cooldown_uses_an_injected_clock_and_inclusive_boundary() {
1070 let specification = Cooldown::new(
1071 "last_login",
1072 Duration::from_secs(60),
1073 FixedClock::new(Duration::from_secs(100)),
1074 );
1075
1076 assert!(specification.is_satisfied_by(&EvaluationContext::new(())));
1077 assert!(specification.is_satisfied_by(
1078 &EvaluationContext::new(()).with_timestamp("last_login", Duration::from_secs(40))
1079 ));
1080 assert!(!specification.is_satisfied_by(
1081 &EvaluationContext::new(()).with_timestamp("last_login", Duration::from_secs(41))
1082 ));
1083 }
1084
1085 #[test]
1086 fn cooldown_rejects_future_timestamps_even_at_zero_duration() {
1087 let specification = Cooldown::new(
1088 "last_login",
1089 Duration::ZERO,
1090 FixedClock::new(Duration::from_secs(100)),
1091 );
1092
1093 assert!(!specification.is_satisfied_by(
1094 &EvaluationContext::new(()).with_timestamp("last_login", Duration::from_secs(101))
1095 ));
1096 }
1097}