1use thiserror::Error;
2
3use crate::{CounterKey, FixedWindowPolicy, SubjectKey};
4
5#[derive(Clone, Copy, Debug, Eq, PartialEq)]
11pub struct Check<'a> {
12 policy: &'a FixedWindowPolicy,
13 subject: SubjectKey,
14 cost: u64,
15}
16
17impl<'a> Check<'a> {
18 pub const fn new(policy: &'a FixedWindowPolicy, subject: SubjectKey) -> Self {
20 Self {
21 policy,
22 subject,
23 cost: 1,
24 }
25 }
26
27 pub fn with_cost(
33 policy: &'a FixedWindowPolicy,
34 subject: SubjectKey,
35 cost: u64,
36 ) -> Result<Self, CheckError> {
37 validate_cost(policy, cost)?;
38 Ok(Self {
39 policy,
40 subject,
41 cost,
42 })
43 }
44
45 pub fn try_with_cost(mut self, cost: u64) -> Result<Self, CheckError> {
51 validate_cost(self.policy, cost)?;
52 self.cost = cost;
53 Ok(self)
54 }
55
56 pub const fn policy(&self) -> &'a FixedWindowPolicy {
58 self.policy
59 }
60
61 pub const fn subject(&self) -> SubjectKey {
63 self.subject
64 }
65
66 pub const fn counter_key(&self) -> CounterKey {
68 CounterKey::new(self.policy.fingerprint(), self.subject)
69 }
70
71 pub const fn cost(&self) -> u64 {
73 self.cost
74 }
75}
76
77fn validate_cost(policy: &FixedWindowPolicy, cost: u64) -> Result<(), CheckError> {
78 if cost == 0 {
79 return Err(CheckError::ZeroCost);
80 }
81 if cost > policy.limit() {
82 return Err(CheckError::CostExceedsLimit {
83 cost,
84 limit: policy.limit(),
85 });
86 }
87 Ok(())
88}
89
90#[derive(Clone, Copy, Debug, Error, Eq, PartialEq)]
92pub enum CheckError {
93 #[error("check cost must be greater than zero")]
95 ZeroCost,
96 #[error("check cost ({cost}) exceeds the policy limit ({limit})")]
98 CostExceedsLimit {
99 cost: u64,
101 limit: u64,
103 },
104}
105
106#[cfg(test)]
107mod tests {
108 use std::time::Duration;
109
110 use super::{Check, CheckError};
111 use crate::{FixedWindowPolicy, PolicyId, ScopeId, SubjectKey};
112
113 fn policy() -> FixedWindowPolicy {
114 FixedWindowPolicy::new(
115 PolicyId::new("auth.login").unwrap(),
116 ScopeId::new("client").unwrap(),
117 8,
118 Duration::from_secs(60),
119 )
120 .unwrap()
121 }
122
123 #[test]
124 fn defaults_to_one_unit_of_cost() {
125 let policy = policy();
126 let subject = SubjectKey::from_digest([1; 32]);
127 let check = Check::new(&policy, subject);
128
129 assert_eq!(check.policy(), &policy);
130 assert_eq!(check.subject(), subject);
131 assert_eq!(check.cost(), 1);
132 }
133
134 #[test]
135 fn accepts_cost_up_to_and_including_the_limit() {
136 let policy = policy();
137 let subject = SubjectKey::from_digest([2; 32]);
138
139 assert_eq!(Check::with_cost(&policy, subject, 3).unwrap().cost(), 3);
140 assert_eq!(
141 Check::new(&policy, subject)
142 .try_with_cost(policy.limit())
143 .unwrap()
144 .cost(),
145 policy.limit()
146 );
147 }
148
149 #[test]
150 fn rejects_zero_cost() {
151 let policy = policy();
152
153 assert_eq!(
154 Check::with_cost(&policy, SubjectKey::from_digest([3; 32]), 0),
155 Err(CheckError::ZeroCost)
156 );
157 }
158
159 #[test]
160 fn rejects_cost_above_policy_limit() {
161 let policy = policy();
162
163 assert_eq!(
164 Check::with_cost(&policy, SubjectKey::from_digest([4; 32]), 9),
165 Err(CheckError::CostExceedsLimit { cost: 9, limit: 8 })
166 );
167 }
168}