stripe_client_core/
request_strategy.rs1use std::time::Duration;
2
3fn is_status_client_error(status: u16) -> bool {
4 (400..500).contains(&status)
5}
6
7#[derive(Clone, Debug)]
10pub enum RequestStrategy {
11 Once,
13 Idempotent(IdempotencyKey),
15 Retry(u32),
19 ExponentialBackoff(u32),
23}
24
25impl RequestStrategy {
26 pub fn test(
28 &self,
29 status: Option<u16>,
33 stripe_should_retry: Option<bool>,
34 retry_count: u32,
35 ) -> Outcome {
36 if !stripe_should_retry.unwrap_or(true) {
38 return Outcome::Stop;
39 }
40
41 use RequestStrategy::*;
42
43 match (self, status, retry_count) {
44 (Once | Idempotent(_), _, 0) => Outcome::Continue(None),
46
47 (_, Some(c), _) if is_status_client_error(c) => Outcome::Stop,
51
52 (Retry(n), _, x) if x < *n => Outcome::Continue(None),
55 (ExponentialBackoff(n), _, x) if x < *n => {
56 Outcome::Continue(Some(calculate_backoff(x)))
57 }
58
59 _ => Outcome::Stop,
61 }
62 }
63
64 #[cfg(feature = "uuid")]
66 pub fn idempotent_with_uuid() -> Self {
67 Self::Idempotent(IdempotencyKey::new_uuid_v4())
68 }
69
70 pub fn get_key(&self) -> Option<IdempotencyKey> {
72 match self {
73 RequestStrategy::Once => None,
74 RequestStrategy::Idempotent(key) => Some(key.clone()),
75 #[cfg(feature = "uuid")]
76 RequestStrategy::Retry(_) | RequestStrategy::ExponentialBackoff(_) => {
77 Some(IdempotencyKey::new_uuid_v4())
78 }
79 #[cfg(not(feature = "uuid"))]
80 RequestStrategy::Retry(_) | RequestStrategy::ExponentialBackoff(_) => None,
81 }
82 }
83}
84
85#[derive(Debug, Clone, PartialEq, Eq)]
86#[repr(transparent)]
87pub struct IdempotencyKey(String);
91
92#[derive(Debug, thiserror::Error)]
93pub enum IdempotentKeyError {
95 #[error("Idempotency Key cannot be empty")]
96 EmptyKey,
98 #[error("Idempotency key cannot be longer than 255 characters (you supplied: {0})")]
99 KeyTooLong(usize),
101}
102
103impl IdempotencyKey {
104 pub fn new(val: impl AsRef<str>) -> Result<Self, IdempotentKeyError> {
110 let val = val.as_ref();
111 if val.is_empty() {
112 Err(IdempotentKeyError::EmptyKey)
113 } else if val.len() > 255 {
114 Err(IdempotentKeyError::KeyTooLong(val.len()))
115 } else {
116 Ok(Self(val.to_owned()))
117 }
118 }
119
120 #[cfg(feature = "uuid")]
121 pub fn new_uuid_v4() -> Self {
123 let uuid = uuid::Uuid::new_v4().to_string();
124 Self(uuid)
125 }
126
127 pub fn as_str(&self) -> &str {
129 &self.0
130 }
131
132 pub fn into_inner(self) -> String {
134 self.0
135 }
136}
137
138impl TryFrom<String> for IdempotencyKey {
139 type Error = IdempotentKeyError;
140
141 fn try_from(value: String) -> Result<Self, Self::Error> {
142 Self::new(value)
143 }
144}
145
146#[cfg(feature = "uuid")]
147impl From<uuid::Uuid> for IdempotencyKey {
148 #[inline]
149 fn from(value: uuid::Uuid) -> Self {
150 Self(value.to_string())
151 }
152}
153
154fn calculate_backoff(retry_count: u32) -> Duration {
155 Duration::from_secs(2_u64.pow(retry_count))
156}
157
158#[derive(PartialEq, Eq, Debug)]
160pub enum Outcome {
161 Stop,
163 Continue(Option<Duration>),
166}
167
168#[cfg(test)]
169mod tests {
170 use std::time::Duration;
171
172 use super::{Outcome, RequestStrategy};
173 use crate::IdempotencyKey;
174
175 #[test]
176 fn test_idempotent_strategy() {
177 let key: IdempotencyKey = "key".to_string().try_into().unwrap();
178 let strategy = RequestStrategy::Idempotent(key.clone());
179 assert_eq!(strategy.get_key(), Some(key));
180 }
181
182 #[test]
183 fn test_once_strategy() {
184 let strategy = RequestStrategy::Once;
185 assert_eq!(strategy.get_key(), None);
186 assert_eq!(strategy.test(None, None, 0), Outcome::Continue(None));
187 assert_eq!(strategy.test(None, None, 1), Outcome::Stop);
188 }
189
190 #[test]
191 #[cfg(feature = "uuid")]
192 fn test_uuid_idempotency() {
193 use uuid::Uuid;
194 let strategy = RequestStrategy::Retry(3);
195 assert!(Uuid::parse_str(&strategy.get_key().unwrap().as_str()).is_ok());
196 }
197
198 #[test]
199 #[cfg(not(feature = "uuid"))]
200 fn test_uuid_idempotency() {
201 let strategy = RequestStrategy::Retry(3);
202 assert_eq!(strategy.get_key(), None);
203 }
204
205 #[test]
206 fn test_retry_strategy() {
207 let strategy = RequestStrategy::Retry(3);
208 assert_eq!(strategy.test(None, None, 0), Outcome::Continue(None));
209 assert_eq!(strategy.test(None, None, 1), Outcome::Continue(None));
210 assert_eq!(strategy.test(None, None, 2), Outcome::Continue(None));
211 assert_eq!(strategy.test(None, None, 3), Outcome::Stop);
212 assert_eq!(strategy.test(None, None, 4), Outcome::Stop);
213 }
214
215 #[test]
216 fn test_backoff_strategy() {
217 let strategy = RequestStrategy::ExponentialBackoff(3);
218 assert_eq!(strategy.test(None, None, 0), Outcome::Continue(Some(Duration::from_secs(1))));
219 assert_eq!(strategy.test(None, None, 1), Outcome::Continue(Some(Duration::from_secs(2))));
220 assert_eq!(strategy.test(None, None, 2), Outcome::Continue(Some(Duration::from_secs(4))));
221 assert_eq!(strategy.test(None, None, 3), Outcome::Stop);
222 assert_eq!(strategy.test(None, None, 4), Outcome::Stop);
223 }
224
225 #[test]
226 fn test_retry_header() {
227 let strategy = RequestStrategy::Retry(3);
228 assert_eq!(strategy.test(None, Some(false), 0), Outcome::Stop);
229 }
230}