1use std::fmt;
3use std::sync::Arc;
4
5use chrono::{DateTime, Datelike, Duration, Local, Timelike, Utc, Weekday};
6
7use crate::{Error, LimitExceeded, Result, TimeUnit, TimeWindow};
8
9use super::inner::EventStoreInner;
10use super::query::Query;
11use super::EventId;
12
13pub struct Limiter {
18 store: Arc<EventStoreInner>,
19 constraints: Vec<Constraint>,
20}
21
22#[derive(Debug, Clone)]
24pub enum Constraint {
25 AtMost {
27 event_id: String,
28 limit: u32,
29 window_count: usize,
30 time_unit: TimeUnit,
31 },
32 AtLeast {
34 event_id: String,
35 count: u32,
36 window_count: usize,
37 time_unit: TimeUnit,
38 },
39 Cooldown {
41 event_id: String,
42 duration: Duration,
43 },
44 Within {
46 prerequisite_event: String,
47 duration: Duration,
48 },
49 During { schedule: Schedule },
51 OutsideOf { schedule: Schedule },
53}
54
55#[derive(Clone)]
57pub enum Schedule {
58 Hours {
62 start_hour: u8,
63 end_hour: u8,
64 use_local_tz: bool,
65 },
66
67 Weekdays { use_local_tz: bool },
71
72 Weekends { use_local_tz: bool },
76
77 Custom(Arc<dyn Fn(DateTime<Utc>) -> bool + Send + Sync>),
79}
80
81impl fmt::Debug for Schedule {
82 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
83 match self {
84 Schedule::Hours {
85 start_hour,
86 end_hour,
87 use_local_tz,
88 } => {
89 let tz_str = if *use_local_tz { "Local" } else { "UTC" };
90 write!(
91 f,
92 "Hours {{ start_hour: {}, end_hour: {}, timezone: {} }}",
93 start_hour, end_hour, tz_str
94 )
95 }
96 Schedule::Weekdays { use_local_tz } => {
97 let tz_str = if *use_local_tz { "Local" } else { "UTC" };
98 write!(f, "Weekdays({})", tz_str)
99 }
100 Schedule::Weekends { use_local_tz } => {
101 let tz_str = if *use_local_tz { "Local" } else { "UTC" };
102 write!(f, "Weekends({})", tz_str)
103 }
104 Schedule::Custom(_) => write!(f, "Custom(<closure>)"),
105 }
106 }
107}
108
109impl Schedule {
110 pub fn hours(start_hour: u8, end_hour: u8) -> Result<Self> {
131 Self::validate_hours(start_hour, end_hour)?;
132 Ok(Schedule::Hours {
133 start_hour,
134 end_hour,
135 use_local_tz: false,
136 })
137 }
138
139 pub fn hours_local_tz(start_hour: u8, end_hour: u8) -> Result<Self> {
156 Self::validate_hours(start_hour, end_hour)?;
157 Ok(Schedule::Hours {
158 start_hour,
159 end_hour,
160 use_local_tz: true,
161 })
162 }
163
164 pub fn weekdays() -> Self {
166 Schedule::Weekdays {
167 use_local_tz: false,
168 }
169 }
170
171 pub fn weekdays_local_tz() -> Self {
173 Schedule::Weekdays { use_local_tz: true }
174 }
175
176 pub fn weekends() -> Self {
178 Schedule::Weekends {
179 use_local_tz: false,
180 }
181 }
182
183 pub fn weekends_local_tz() -> Self {
185 Schedule::Weekends { use_local_tz: true }
186 }
187
188 fn validate_hours(start_hour: u8, end_hour: u8) -> Result<()> {
189 if start_hour > 23 {
190 Err(Error::InvalidHour(start_hour))
191 } else if end_hour > 23 {
192 Err(Error::InvalidHour(end_hour))
193 } else if start_hour >= end_hour {
194 Err(Error::InvalidRange(start_hour, end_hour))
195 } else {
196 Ok(())
197 }
198 }
199}
200
201pub struct LimitUsage {
203 pub count: u32,
204 pub limit: u32,
205 pub remaining: u32,
206}
207
208pub struct Reservation {
233 store: Arc<EventStoreInner>,
234 event_id: String,
235 committed: bool,
236}
237
238impl Reservation {
239 pub fn commit(mut self) {
243 self.release_and_record();
244 self.committed = true;
245 }
246
247 pub fn cancel(mut self) {
251 self.release_pending();
252 self.committed = true;
253 }
254
255 fn release_and_record(&self) {
257 if let Some(counter_arc) = self.store.events.get(&self.event_id) {
258 let mut counter = counter_arc.lock().unwrap();
259 counter.decrement_pending();
260 let now = self.store.clock_now();
261 counter.advance_if_needed(now);
262 counter.record(1);
263 counter.mark_dirty();
264 }
265 }
266
267 fn release_pending(&self) {
269 if let Some(counter_arc) = self.store.events.get(&self.event_id) {
270 let mut counter = counter_arc.lock().unwrap();
271 counter.decrement_pending();
272 counter.mark_dirty();
273 }
274 }
275}
276
277impl Drop for Reservation {
278 fn drop(&mut self) {
279 if !self.committed {
280 self.release_pending();
282 }
283 }
284}
285
286impl Limiter {
287 pub(crate) fn new(store: Arc<EventStoreInner>) -> Self {
289 Self {
290 store,
291 constraints: Vec::new(),
292 }
293 }
294
295 pub fn at_most(
332 mut self,
333 event_id: impl EventId,
334 limit: u32,
335 window: impl Into<TimeWindow>,
336 ) -> Self {
337 let window = window.into();
338 self.constraints.push(Constraint::AtMost {
339 event_id: event_id.as_ref().to_string(),
340 limit,
341 window_count: window.count,
342 time_unit: window.time_unit,
343 });
344 self
345 }
346
347 pub fn at_least(
354 mut self,
355 event_id: impl EventId,
356 count: u32,
357 window: impl Into<TimeWindow>,
358 ) -> Self {
359 let window = window.into();
360 self.constraints.push(Constraint::AtLeast {
361 event_id: event_id.as_ref().to_string(),
362 count,
363 window_count: window.count,
364 time_unit: window.time_unit,
365 });
366 self
367 }
368
369 pub fn cooldown(mut self, event_id: impl EventId, duration: Duration) -> Self {
371 self.constraints.push(Constraint::Cooldown {
372 event_id: event_id.as_ref().to_string(),
373 duration,
374 });
375 self
376 }
377
378 pub fn within(mut self, prerequisite_event: impl EventId, duration: Duration) -> Self {
380 self.constraints.push(Constraint::Within {
381 prerequisite_event: prerequisite_event.as_ref().to_string(),
382 duration,
383 });
384 self
385 }
386
387 pub fn during(mut self, schedule: Schedule) -> Self {
389 self.constraints.push(Constraint::During { schedule });
390 self
391 }
392
393 pub fn outside_of(mut self, schedule: Schedule) -> Self {
395 self.constraints.push(Constraint::OutsideOf { schedule });
396 self
397 }
398
399 pub fn check(self, event_id: impl EventId) -> Result<()> {
416 let event_id_str = event_id.as_ref();
417 self.evaluate_constraints(event_id_str)
418 }
419
420 pub fn check_and_record(self, event_id: impl EventId) -> Result<()> {
446 let event_id_str = event_id.as_ref();
447 self.evaluate_constraints(event_id_str)?;
448
449 let now = self.store.clock.now();
451 let counter = self.store.get_counter_for_record(event_id_str);
452
453 let mut counter = counter.lock().unwrap();
454 counter.advance_if_needed(now);
455 counter.record(1);
456 counter.mark_dirty();
457
458 Ok(())
459 }
460
461 pub fn reserve(self, event_id: impl EventId) -> Result<Reservation> {
512 let clock_now = self.store.clock_now();
513 let event_id_str = event_id.as_ref();
514
515 let counter_arc = self.store.get_counter_for_record(event_id_str);
517
518 let mut counter = counter_arc.lock().unwrap();
519 counter.advance_if_needed(clock_now);
520
521 for constraint in &self.constraints {
523 match constraint {
524 Constraint::AtMost {
525 event_id: _constraint_event,
526 limit,
527 window_count,
528 time_unit,
529 } => {
530 let total = counter.total_with_pending(*time_unit, 0..*window_count);
532 if total >= *limit {
533 return Err(Error::LimitExceeded(LimitExceeded {
534 event_id: event_id_str.to_string(),
535 constraint: constraint.clone(),
536 retry_after: Some(time_unit.duration()),
537 }));
538 }
539 }
540 _ => {
542 drop(counter);
544 self.evaluate_constraint(event_id_str, constraint)?;
545 counter = counter_arc.lock().unwrap();
546 }
547 }
548 }
549
550 counter.increment_pending();
552 counter.mark_dirty();
553
554 Ok(Reservation {
555 store: self.store.clone(),
556 event_id: event_id_str.to_string(),
557 committed: false,
558 })
559 }
560
561 pub fn allowed(self, event_id: impl EventId) -> bool {
563 self.check(event_id).is_ok()
564 }
565
566 pub fn usage(self, event_id: impl EventId) -> Result<LimitUsage> {
570 let event_id_str = event_id.as_ref();
571
572 for constraint in &self.constraints {
574 if let Constraint::AtMost {
575 event_id: constraint_event,
576 limit,
577 window_count,
578 time_unit,
579 } = constraint
580 {
581 if constraint_event == event_id_str {
582 let current = self
583 .query_sum_window(constraint_event, *window_count, *time_unit)
584 .unwrap_or(0);
585 let remaining = limit.saturating_sub(current);
586 return Ok(LimitUsage {
587 count: current,
588 limit: *limit,
589 remaining,
590 });
591 }
592 }
593 }
594
595 Ok(LimitUsage {
597 count: 0,
598 limit: 0,
599 remaining: 0,
600 })
601 }
602
603 fn evaluate_constraints(&self, event_id: &str) -> Result<()> {
605 for constraint in &self.constraints {
606 self.evaluate_constraint(event_id, constraint)?;
607 }
608 Ok(())
609 }
610
611 fn evaluate_constraint(&self, event_id: &str, constraint: &Constraint) -> Result<()> {
613 match constraint {
614 Constraint::AtMost {
615 event_id: constraint_event,
616 limit,
617 window_count,
618 time_unit,
619 } => {
620 let current = self
621 .query_sum_window(constraint_event, *window_count, *time_unit)
622 .unwrap_or(0);
623 if current >= *limit {
624 return Err(Error::LimitExceeded(LimitExceeded {
625 event_id: event_id.to_string(),
626 constraint: constraint.clone(),
627 retry_after: Some(time_unit.duration()),
628 }));
629 }
630 }
631 Constraint::AtLeast {
632 event_id: constraint_event,
633 count,
634 window_count,
635 time_unit,
636 } => {
637 let current = self
638 .query_sum_window(constraint_event, *window_count, *time_unit)
639 .unwrap_or(0);
640 if current < *count {
641 return Err(Error::LimitExceeded(LimitExceeded {
642 event_id: event_id.to_string(),
643 constraint: constraint.clone(),
644 retry_after: None,
645 }));
646 }
647 }
648 Constraint::Cooldown {
649 event_id: constraint_event,
650 duration,
651 } => {
652 if let Some(elapsed) = self.query_last_seen(constraint_event) {
653 if elapsed < *duration {
654 let remaining = *duration - elapsed;
655 return Err(Error::LimitExceeded(LimitExceeded {
656 event_id: event_id.to_string(),
657 constraint: constraint.clone(),
658 retry_after: Some(remaining),
659 }));
660 }
661 }
662 }
664 Constraint::Within {
665 prerequisite_event,
666 duration,
667 } => {
668 match self.query_last_seen(prerequisite_event) {
669 None => {
670 return Err(Error::LimitExceeded(LimitExceeded {
672 event_id: event_id.to_string(),
673 constraint: constraint.clone(),
674 retry_after: None,
675 }));
676 }
677 Some(elapsed) => {
678 if elapsed > *duration {
679 return Err(Error::LimitExceeded(LimitExceeded {
681 event_id: event_id.to_string(),
682 constraint: constraint.clone(),
683 retry_after: None,
684 }));
685 }
686 }
687 }
688 }
689 Constraint::During { schedule } => {
690 if !schedule.allows(&self.store) {
691 return Err(Error::LimitExceeded(LimitExceeded {
692 event_id: event_id.to_string(),
693 constraint: constraint.clone(),
694 retry_after: schedule.next_allowed(&self.store),
695 }));
696 }
697 }
698 Constraint::OutsideOf { schedule } => {
699 if schedule.allows(&self.store) {
700 return Err(Error::LimitExceeded(LimitExceeded {
701 event_id: event_id.to_string(),
702 constraint: constraint.clone(),
703 retry_after: schedule.block_end(&self.store),
704 }));
705 }
706 }
707 }
708 Ok(())
709 }
710
711 fn query_sum_window(
713 &self,
714 event_id: &str,
715 window_count: usize,
716 time_unit: TimeUnit,
717 ) -> Option<u32> {
718 let query = Query::new(self.store.clone(), event_id.to_string());
719 query.last(window_count, time_unit).sum()
720 }
721
722 fn query_last_seen(&self, event_id: &str) -> Option<Duration> {
724 Query::new(self.store.clone(), event_id.to_string()).last_seen()
725 }
726}
727
728impl Schedule {
729 fn allows(&self, store: &EventStoreInner) -> bool {
731 let now = store.clock_now();
732 match self {
733 Schedule::Hours {
734 start_hour,
735 end_hour,
736 use_local_tz,
737 } => {
738 let hour = if *use_local_tz {
739 now.with_timezone(&Local).hour() as u8
740 } else {
741 now.hour() as u8
742 };
743 hour >= *start_hour && hour < *end_hour
744 }
745 Schedule::Weekdays { use_local_tz } => {
746 let weekday = if *use_local_tz {
747 now.with_timezone(&Local).weekday()
748 } else {
749 now.weekday()
750 };
751 !matches!(weekday, Weekday::Sat | Weekday::Sun)
752 }
753 Schedule::Weekends { use_local_tz } => {
754 let weekday = if *use_local_tz {
755 now.with_timezone(&Local).weekday()
756 } else {
757 now.weekday()
758 };
759 matches!(weekday, Weekday::Sat | Weekday::Sun)
760 }
761 Schedule::Custom(f) => f(now),
762 }
763 }
764
765 fn next_allowed(&self, store: &EventStoreInner) -> Option<Duration> {
767 let now = store.clock_now();
768 match self {
769 Schedule::Hours {
770 start_hour,
771 end_hour: _,
772 use_local_tz,
773 } => {
774 let current_hour = if *use_local_tz {
775 now.with_timezone(&Local).hour() as u8
776 } else {
777 now.hour() as u8
778 };
779
780 if current_hour < *start_hour {
781 let hours_until = (*start_hour - current_hour) as i64;
783 Some(Duration::hours(hours_until))
784 } else {
785 let hours_until = 24 - current_hour + *start_hour;
787 Some(Duration::hours(hours_until as i64))
788 }
789 }
790 Schedule::Weekdays { use_local_tz } => {
791 let weekday = if *use_local_tz {
792 now.with_timezone(&Local).weekday()
793 } else {
794 now.weekday()
795 };
796 match weekday {
798 Weekday::Sat => Some(Duration::days(2)),
799 Weekday::Sun => Some(Duration::days(1)),
800 _ => None,
801 }
802 }
803 Schedule::Weekends { use_local_tz } => {
804 let weekday = if *use_local_tz {
805 now.with_timezone(&Local).weekday()
806 } else {
807 now.weekday()
808 };
809 let days_until_sat = match weekday {
811 Weekday::Mon => 5,
812 Weekday::Tue => 4,
813 Weekday::Wed => 3,
814 Weekday::Thu => 2,
815 Weekday::Fri => 1,
816 _ => 0,
817 };
818 if days_until_sat > 0 {
819 Some(Duration::days(days_until_sat))
820 } else {
821 None
822 }
823 }
824 Schedule::Custom(_) => None, }
826 }
827
828 fn block_end(&self, store: &EventStoreInner) -> Option<Duration> {
830 let now = store.clock_now();
831 match self {
832 Schedule::Hours {
833 start_hour: _,
834 end_hour,
835 use_local_tz,
836 } => {
837 let current_hour = if *use_local_tz {
838 now.with_timezone(&Local).hour() as u8
839 } else {
840 now.hour() as u8
841 };
842
843 if current_hour < *end_hour {
844 let hours_until = (*end_hour - current_hour) as i64;
845 Some(Duration::hours(hours_until))
846 } else {
847 None
848 }
849 }
850 Schedule::Weekdays { use_local_tz } => {
851 let weekday = if *use_local_tz {
852 now.with_timezone(&Local).weekday()
853 } else {
854 now.weekday()
855 };
856 match weekday {
858 Weekday::Mon => Some(Duration::days(5)),
859 Weekday::Tue => Some(Duration::days(4)),
860 Weekday::Wed => Some(Duration::days(3)),
861 Weekday::Thu => Some(Duration::days(2)),
862 Weekday::Fri => Some(Duration::days(1)),
863 _ => None,
864 }
865 }
866 Schedule::Weekends { use_local_tz } => {
867 let weekday = if *use_local_tz {
868 now.with_timezone(&Local).weekday()
869 } else {
870 now.weekday()
871 };
872 match weekday {
874 Weekday::Sat => Some(Duration::days(2)),
875 Weekday::Sun => Some(Duration::days(1)),
876 _ => None,
877 }
878 }
879 Schedule::Custom(_) => None, }
881 }
882}
883
884#[cfg(test)]
885mod tests {
886 use std::thread;
887
888 use chrono::{Duration, TimeZone, Weekday};
889
890 use crate::{EventStore, TestClock, TimeUnit};
891
892 use super::*;
893
894 #[test]
895 fn test_at_most_constraint_passes() {
896 let store = EventStore::new();
897 store.record_count("test_event", 5);
898
899 let result = store
900 .limit()
901 .at_most("test_event", 10, TimeUnit::Days)
902 .check("action");
903
904 assert!(result.is_ok());
905 }
906
907 #[test]
908 fn test_at_most_constraint_fails() {
909 let store = EventStore::new();
910 store.record_count("test_event", 10);
911
912 let result = store
913 .limit()
914 .at_most("test_event", 5, TimeUnit::Days)
915 .check("action");
916
917 assert!(result.is_err());
918 let Error::LimitExceeded(err) = result.unwrap_err() else {
919 panic!("Expected LimitExceeded error");
920 };
921 assert_eq!(err.event_id, "action");
922 assert!(err.retry_after.is_some());
923 }
924
925 #[test]
926 fn test_at_least_constraint_passes() {
927 let store = EventStore::new();
928 store.record_count("prerequisite", 5);
929
930 let result = store
931 .limit()
932 .at_least("prerequisite", 3, TimeUnit::Days)
933 .check("action");
934
935 assert!(result.is_ok());
936 }
937
938 #[test]
939 fn test_at_least_constraint_fails() {
940 let store = EventStore::new();
941 store.record_count("prerequisite", 2);
942
943 let result = store
944 .limit()
945 .at_least("prerequisite", 5, TimeUnit::Days)
946 .check("action");
947
948 assert!(result.is_err());
949 let Error::LimitExceeded(err) = result.unwrap_err() else {
950 panic!("Expected LimitExceeded error");
951 };
952 assert!(err.retry_after.is_none());
953 }
954
955 #[test]
956 fn test_cooldown_constraint_passes() {
957 let store = EventStore::new();
958 store.record_ago("test_event", Duration::hours(2));
959
960 let result = store
961 .limit()
962 .cooldown("test_event", Duration::hours(1))
963 .check("action");
964
965 assert!(result.is_ok());
966 }
967
968 #[test]
969 fn test_cooldown_constraint_fails() {
970 let store = EventStore::new();
971 store.record("test_event");
972
973 let result = store
974 .limit()
975 .cooldown("test_event", Duration::hours(1))
976 .check("action");
977
978 assert!(result.is_err());
979 let Error::LimitExceeded(err) = result.unwrap_err() else {
980 panic!("Expected LimitExceeded error");
981 };
982 assert!(err.retry_after.is_some());
983 }
984
985 #[test]
986 fn test_within_constraint_passes() {
987 let store = EventStore::new();
988 store.record("prerequisite");
989
990 let result = store
991 .limit()
992 .within("prerequisite", Duration::hours(1))
993 .check("action");
994
995 assert!(result.is_ok());
996 }
997
998 #[test]
999 fn test_within_constraint_fails_never_seen() {
1000 let store = EventStore::new();
1001
1002 let result = store
1003 .limit()
1004 .within("prerequisite", Duration::hours(1))
1005 .check("action");
1006
1007 assert!(result.is_err());
1008 }
1009
1010 #[test]
1011 fn test_within_constraint_fails_too_long_ago() {
1012 let store = EventStore::new();
1013 store.record_ago("prerequisite", Duration::hours(2));
1014
1015 let result = store
1016 .limit()
1017 .within("prerequisite", Duration::hours(1))
1018 .check("action");
1019
1020 assert!(result.is_err());
1021 }
1022
1023 #[test]
1024 fn test_multiple_constraints_all_pass() {
1025 let store = EventStore::new();
1026 store.record_count("prerequisite", 5);
1027 store.record_count("test_event", 2);
1028
1029 let result = store
1030 .limit()
1031 .at_least("prerequisite", 3, TimeUnit::Days)
1032 .at_most("test_event", 10, TimeUnit::Days)
1033 .check("action");
1034
1035 assert!(result.is_ok());
1036 }
1037
1038 #[test]
1039 fn test_multiple_constraints_first_fails() {
1040 let store = EventStore::new();
1041 store.record_count("prerequisite", 1);
1042 store.record_count("test_event", 2);
1043
1044 let result = store
1045 .limit()
1046 .at_least("prerequisite", 3, TimeUnit::Days)
1047 .at_most("test_event", 10, TimeUnit::Days)
1048 .check("action");
1049
1050 assert!(result.is_err());
1051 }
1052
1053 #[test]
1054 fn test_check_and_record_success() {
1055 let store = EventStore::new();
1056
1057 let result = store
1058 .limit()
1059 .at_most("test_event", 5, TimeUnit::Days)
1060 .check_and_record("test_event");
1061
1062 assert!(result.is_ok());
1063
1064 let count = store.query("test_event").last_days(1).sum();
1066 assert_eq!(count, Some(1));
1067 }
1068
1069 #[test]
1070 fn test_check_and_record_failure_does_not_record() {
1071 let store = EventStore::new();
1072 store.record_count("test_event", 10);
1073
1074 let result = store
1075 .limit()
1076 .at_most("test_event", 5, TimeUnit::Days)
1077 .check_and_record("test_event");
1078
1079 assert!(result.is_err());
1080
1081 let count = store.query("test_event").last_days(1).sum();
1083 assert_eq!(count, Some(10));
1084 }
1085
1086 #[test]
1087 fn test_allowed_convenience_method() {
1088 let store = EventStore::new();
1089 store.record_count("test_event", 3);
1090
1091 let allowed = store
1092 .limit()
1093 .at_most("test_event", 5, TimeUnit::Days)
1094 .allowed("action");
1095 assert!(allowed);
1096
1097 store.record_count("test_event", 2);
1099 let not_allowed = store
1100 .limit()
1101 .at_most("test_event", 5, TimeUnit::Days)
1102 .allowed("action");
1103 assert!(!not_allowed);
1104 }
1105
1106 #[test]
1107 fn test_usage_returns_correct_counts() {
1108 let store = EventStore::new();
1109 store.record_count("test_event", 3);
1110
1111 let usage = store
1112 .limit()
1113 .at_most("test_event", 10, TimeUnit::Days)
1114 .usage("test_event")
1115 .unwrap();
1116
1117 assert_eq!(usage.count, 3);
1118 assert_eq!(usage.limit, 10);
1119 assert_eq!(usage.remaining, 7);
1120 }
1121
1122 #[test]
1123 fn test_business_hours_schedule_during_hours() {
1124 let tuesday_10am = Utc.with_ymd_and_hms(2025, 1, 7, 10, 0, 0).unwrap();
1126 let clock = TestClock::build_for_testing_at(tuesday_10am);
1127
1128 let store = EventStore::builder()
1129 .with_clock(Arc::new(clock.clone()))
1130 .build()
1131 .unwrap();
1132
1133 let result = store
1135 .limit()
1136 .during(Schedule::hours(9, 17).unwrap())
1137 .check_and_record("action");
1138 assert!(result.is_ok());
1139
1140 let tuesday_7pm = Utc.with_ymd_and_hms(2025, 1, 7, 19, 0, 0).unwrap();
1142 clock.set(tuesday_7pm);
1143
1144 let result = store
1146 .limit()
1147 .during(Schedule::hours(9, 17).unwrap())
1148 .check("action");
1149 assert!(result.is_err());
1150 }
1151
1152 #[test]
1153 fn test_weekdays_schedule() {
1154 let tuesday = Utc.with_ymd_and_hms(2025, 1, 7, 12, 0, 0).unwrap();
1156 assert_eq!(tuesday.weekday(), Weekday::Tue);
1157 let clock = TestClock::build_for_testing_at(tuesday);
1158
1159 let store = EventStore::builder()
1160 .with_clock(Arc::new(clock.clone()))
1161 .build()
1162 .unwrap();
1163
1164 let result = store
1166 .limit()
1167 .during(Schedule::weekdays())
1168 .check_and_record("action");
1169 assert!(result.is_ok());
1170
1171 let saturday = Utc.with_ymd_and_hms(2025, 1, 11, 12, 0, 0).unwrap();
1173 assert_eq!(saturday.weekday(), Weekday::Sat);
1174 clock.set(saturday);
1175
1176 let result = store.limit().during(Schedule::weekdays()).check("action");
1178 assert!(result.is_err());
1179 }
1180
1181 #[test]
1182 fn test_custom_schedule() {
1183 let store = EventStore::new();
1184 let schedule = Schedule::Custom(Arc::new(|_time| true)); let result = store.limit().during(schedule).check("action");
1187 assert!(result.is_ok());
1188 }
1189
1190 #[test]
1191 fn test_schedule_hours_constructor() {
1192 let schedule = Schedule::hours(9, 17).unwrap();
1193 match schedule {
1194 Schedule::Hours {
1195 start_hour,
1196 end_hour,
1197 ..
1198 } => {
1199 assert_eq!(start_hour, 9);
1200 assert_eq!(end_hour, 17);
1201 }
1202 _ => panic!("Expected Hours variant"),
1203 }
1204 }
1205
1206 #[test]
1207 fn test_schedule_hours_validates_start_hour() {
1208 let result = Schedule::hours(24, 17);
1209 assert!(result.is_err());
1210 match result.unwrap_err() {
1211 Error::InvalidHour(hour) => assert_eq!(hour, 24),
1212 _ => panic!("Expected InvalidHour error"),
1213 }
1214 }
1215
1216 #[test]
1217 fn test_schedule_hours_validates_end_hour() {
1218 let result = Schedule::hours(9, 25);
1219 assert!(result.is_err());
1220 match result.unwrap_err() {
1221 Error::InvalidHour(hour) => assert_eq!(hour, 25),
1222 _ => panic!("Expected InvalidHour error"),
1223 }
1224 }
1225
1226 #[test]
1227 fn test_schedule_hours_validates_range() {
1228 let result = Schedule::hours(17, 9);
1229 assert!(result.is_err());
1230 match result.unwrap_err() {
1231 Error::InvalidRange(start, end) => {
1232 assert_eq!(start, 17);
1233 assert_eq!(end, 9);
1234 }
1235 _ => panic!("Expected InvalidRange error"),
1236 }
1237 }
1238
1239 #[test]
1240 fn test_schedule_hours_rejects_equal_hours() {
1241 let result = Schedule::hours(9, 9);
1242 assert!(result.is_err());
1243 match result.unwrap_err() {
1244 Error::InvalidRange(start, end) => {
1245 assert_eq!(start, 9);
1246 assert_eq!(end, 9);
1247 }
1248 _ => panic!("Expected InvalidRange error"),
1249 }
1250 }
1251
1252 #[test]
1253 fn test_schedule_hours_accepts_valid_boundary_cases() {
1254 let schedule = Schedule::hours(0, 23).unwrap();
1256 match schedule {
1257 Schedule::Hours {
1258 start_hour,
1259 end_hour,
1260 ..
1261 } => {
1262 assert_eq!(start_hour, 0);
1263 assert_eq!(end_hour, 23);
1264 }
1265 _ => panic!("Expected Hours variant"),
1266 }
1267
1268 let schedule = Schedule::hours(0, 1).unwrap();
1270 match schedule {
1271 Schedule::Hours {
1272 start_hour,
1273 end_hour,
1274 ..
1275 } => {
1276 assert_eq!(start_hour, 0);
1277 assert_eq!(end_hour, 1);
1278 }
1279 _ => panic!("Expected Hours variant"),
1280 }
1281 }
1282
1283 #[test]
1284 fn test_retry_after_calculation_for_at_most() {
1285 let store = EventStore::new();
1286 store.record_count("test_event", 10);
1287
1288 let result = store
1289 .limit()
1290 .at_most("test_event", 5, TimeUnit::Hours)
1291 .check("action");
1292
1293 assert!(result.is_err());
1294 let Error::LimitExceeded(err) = result.unwrap_err() else {
1295 panic!("Expected LimitExceeded error");
1296 };
1297 assert_eq!(err.retry_after, Some(Duration::hours(1)));
1298 }
1299
1300 #[test]
1301 fn test_cooldown_never_seen_passes() {
1302 let store = EventStore::new();
1303
1304 let result = store
1305 .limit()
1306 .cooldown("never_seen", Duration::hours(1))
1307 .check("action");
1308
1309 assert!(result.is_ok());
1310 }
1311
1312 #[test]
1313 fn test_flexible_time_windows_with_tuple() {
1314 let start_time = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
1315 let clock = TestClock::build_for_testing_at(start_time);
1316
1317 let store = EventStore::builder()
1318 .with_clock(Arc::new(clock.clone()))
1319 .build()
1320 .unwrap();
1321
1322 for _i in 0..10 {
1324 store.record("api");
1325 clock.advance(Duration::hours(7));
1326 }
1327
1328 let result = store
1330 .limit()
1331 .at_most("api", 11, (7, TimeUnit::Days))
1332 .check("api");
1333 assert!(result.is_ok());
1334
1335 let result = store
1337 .limit()
1338 .at_most("api", 10, (7, TimeUnit::Days))
1339 .check("api");
1340 assert!(result.is_err());
1341 }
1342
1343 #[test]
1344 fn test_flexible_time_windows_with_duration() {
1345 let start_time = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
1346 let clock = TestClock::build_for_testing_at(start_time);
1347
1348 let store = EventStore::builder()
1349 .with_clock(Arc::new(clock.clone()))
1350 .build()
1351 .unwrap();
1352
1353 for _i in 0..10 {
1355 store.record("api");
1356 clock.advance(Duration::hours(7));
1357 }
1358
1359 let result = store
1361 .limit()
1362 .at_most("api", 11, Duration::days(7))
1363 .check("api");
1364 assert!(result.is_ok());
1365
1366 let result = store
1368 .limit()
1369 .at_most("api", 10, Duration::days(7))
1370 .check("api");
1371 assert!(result.is_err());
1372 }
1373
1374 #[test]
1375 fn test_flexible_time_windows_backward_compatible() {
1376 let store = EventStore::new();
1377 store.record_count("api", 5);
1378
1379 let result = store
1381 .limit()
1382 .at_most("api", 10, TimeUnit::Days)
1383 .check("api");
1384 assert!(result.is_ok());
1385 }
1386
1387 #[test]
1388 fn test_flexible_time_windows_30_minutes() {
1389 let start_time = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
1390 let clock = TestClock::build_for_testing_at(start_time);
1391
1392 let store = EventStore::builder()
1393 .with_clock(Arc::new(clock.clone()))
1394 .build()
1395 .unwrap();
1396
1397 for _i in 0..10 {
1399 store.record("api");
1400 clock.advance(Duration::minutes(2));
1401 }
1402
1403 let result = store
1405 .limit()
1406 .at_most("api", 11, (30, TimeUnit::Minutes))
1407 .check("api");
1408 assert!(result.is_ok());
1409
1410 let result = store
1412 .limit()
1413 .at_most("api", 10, (30, TimeUnit::Minutes))
1414 .check("api");
1415 assert!(result.is_err());
1416 }
1417
1418 #[test]
1419 fn test_at_least_with_flexible_windows() {
1420 let start_time = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
1421 let clock = TestClock::build_for_testing_at(start_time);
1422
1423 let store = EventStore::builder()
1424 .with_clock(Arc::new(clock.clone()))
1425 .build()
1426 .unwrap();
1427
1428 for _i in 0..10 {
1430 store.record("prerequisite");
1431 clock.advance(Duration::hours(7));
1432 }
1433
1434 let result = store
1436 .limit()
1437 .at_least("prerequisite", 5, (7, TimeUnit::Days))
1438 .check("action");
1439 assert!(result.is_ok());
1440
1441 let result = store
1443 .limit()
1444 .at_least("prerequisite", 15, (7, TimeUnit::Days))
1445 .check("action");
1446 assert!(result.is_err());
1447 }
1448
1449 #[test]
1450 fn test_duration_conversion_to_time_window() {
1451 let start_time = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
1452 let clock = TestClock::build_for_testing_at(start_time);
1453
1454 let store = EventStore::builder()
1455 .with_clock(Arc::new(clock.clone()))
1456 .build()
1457 .unwrap();
1458
1459 for _i in 0..5 {
1461 store.record("api");
1462 clock.advance(Duration::hours(1));
1463 }
1464
1465 let result = store
1467 .limit()
1468 .at_most("api", 10, Duration::hours(12))
1469 .check("api");
1470 assert!(result.is_ok());
1471
1472 store.record_count("fast", 20);
1474 let result = store
1475 .limit()
1476 .at_most("fast", 25, Duration::minutes(30))
1477 .check("fast");
1478 assert!(result.is_ok());
1479 }
1480
1481 #[test]
1482 fn test_reservation_prevents_race_condition() {
1483 let store = EventStore::new();
1484
1485 let res1 = store
1487 .limit()
1488 .at_most("api", 1, TimeUnit::Hours)
1489 .reserve("api")
1490 .unwrap();
1491
1492 let res2 = store
1494 .limit()
1495 .at_most("api", 1, TimeUnit::Hours)
1496 .reserve("api");
1497 assert!(res2.is_err());
1498
1499 res1.commit();
1501
1502 assert_eq!(store.query("api").last_hours(1).sum(), Some(1));
1504 }
1505
1506 #[test]
1507 fn test_reservation_auto_cancel_on_drop() {
1508 let store = EventStore::new();
1509
1510 {
1511 let _res = store
1512 .limit()
1513 .at_most("api", 10, TimeUnit::Hours)
1514 .reserve("api")
1515 .unwrap();
1516 }
1518
1519 let sum = store.query("api").last_hours(1).sum();
1521 assert!(sum.is_none() || sum == Some(0));
1522
1523 let res = store
1525 .limit()
1526 .at_most("api", 1, TimeUnit::Hours)
1527 .reserve("api");
1528 assert!(res.is_ok());
1529 }
1530
1531 #[test]
1532 fn test_reservation_explicit_cancel() {
1533 let store = EventStore::new();
1534
1535 let res = store
1536 .limit()
1537 .at_most("api", 10, TimeUnit::Hours)
1538 .reserve("api")
1539 .unwrap();
1540
1541 res.cancel();
1543
1544 let sum = store.query("api").last_hours(1).sum();
1546 assert!(sum.is_none() || sum == Some(0));
1547
1548 let res = store
1550 .limit()
1551 .at_most("api", 1, TimeUnit::Hours)
1552 .reserve("api");
1553 assert!(res.is_ok());
1554 }
1555
1556 #[test]
1557 fn test_reservation_commit_records_event() {
1558 let store = EventStore::new();
1559
1560 let res = store
1561 .limit()
1562 .at_most("api", 10, TimeUnit::Hours)
1563 .reserve("api")
1564 .unwrap();
1565
1566 res.commit();
1568
1569 assert_eq!(store.query("api").last_hours(1).sum(), Some(1));
1571 }
1572
1573 #[test]
1574 fn test_reservation_concurrent_limits() {
1575 let store = Arc::new(EventStore::new());
1576
1577 let handles: Vec<_> = (0..20)
1579 .map(|_| {
1580 let store = store.clone();
1581 thread::spawn(move || {
1582 store
1583 .limit()
1584 .at_most("api", 10, TimeUnit::Hours)
1585 .reserve("api")
1586 })
1587 })
1588 .collect();
1589
1590 let results: Vec<_> = handles.into_iter().map(|h| h.join().unwrap()).collect();
1591
1592 let successes: Vec<_> = results.into_iter().filter(|r| r.is_ok()).collect();
1594 assert_eq!(successes.len(), 10);
1595
1596 for result in successes {
1598 result.unwrap().commit();
1599 }
1600
1601 assert_eq!(store.query("api").last_hours(1).sum(), Some(10));
1603 }
1604
1605 #[test]
1606 fn test_reservation_transactional_pattern() {
1607 fn simulated_work(should_succeed: bool) -> std::result::Result<(), &'static str> {
1608 if should_succeed {
1609 Ok(())
1610 } else {
1611 Err("Work failed")
1612 }
1613 }
1614
1615 let store = EventStore::new();
1616
1617 let res = store
1619 .limit()
1620 .at_most("api", 10, TimeUnit::Hours)
1621 .reserve("api")
1622 .unwrap();
1623
1624 match simulated_work(true) {
1625 Ok(_) => res.commit(),
1626 Err(_) => res.cancel(),
1627 }
1628
1629 assert_eq!(store.query("api").last_hours(1).sum(), Some(1));
1631
1632 let res = store
1634 .limit()
1635 .at_most("api", 10, TimeUnit::Hours)
1636 .reserve("api")
1637 .unwrap();
1638
1639 match simulated_work(false) {
1640 Ok(_) => res.commit(),
1641 Err(_) => res.cancel(),
1642 }
1643
1644 assert_eq!(store.query("api").last_hours(1).sum(), Some(1));
1646 }
1647
1648 #[test]
1649 fn test_reservation_with_multiple_constraints() {
1650 let store = EventStore::new();
1651
1652 store.record_count("prerequisite", 5);
1654
1655 let res = store
1657 .limit()
1658 .at_most("api", 10, TimeUnit::Hours)
1659 .at_least("prerequisite", 3, TimeUnit::Days)
1660 .reserve("api")
1661 .unwrap();
1662
1663 res.commit();
1664
1665 assert_eq!(store.query("api").last_hours(1).sum(), Some(1));
1667 }
1668}