1use std::{collections::BTreeMap, error::Error as StdError, fmt};
2
3use axum::http::{HeaderMap, HeaderName, HeaderValue, StatusCode};
4use serde_json::Value;
5
6use crate::ApiError;
7
8pub const IDEMPOTENCY_KEY: HeaderName = HeaderName::from_static("idempotency-key");
10
11pub const MAX_IDEMPOTENCY_KEY_BYTES: usize = 255;
13
14pub const IDEMPOTENCY_KEY_REQUIRED_CODE: &str = "idempotency_key_required";
16
17pub const INVALID_IDEMPOTENCY_KEY_CODE: &str = "invalid_idempotency_key";
19
20pub const IDEMPOTENCY_KEY_REUSED_CODE: &str = "idempotency_key_reused";
22
23pub const IDEMPOTENCY_KEY_IN_PROGRESS_CODE: &str = "idempotency_key_in_progress";
25
26const REASON_DETAIL: &str = "reason";
27
28#[derive(Clone, Copy, Debug, Eq, PartialEq)]
33pub struct IdempotencyKeyRule {
34 min_bytes: usize,
35 max_bytes: usize,
36}
37
38impl IdempotencyKeyRule {
39 #[must_use]
47 pub const fn new(min_bytes: usize, max_bytes: usize) -> Self {
48 assert!(
49 are_valid_bounds(min_bytes, max_bytes),
50 "invalid idempotency key bounds"
51 );
52 Self {
53 min_bytes,
54 max_bytes,
55 }
56 }
57
58 pub const fn try_new(
60 min_bytes: usize,
61 max_bytes: usize,
62 ) -> Result<Self, InvalidIdempotencyKeyRule> {
63 if are_valid_bounds(min_bytes, max_bytes) {
64 Ok(Self {
65 min_bytes,
66 max_bytes,
67 })
68 } else {
69 Err(InvalidIdempotencyKeyRule)
70 }
71 }
72
73 #[must_use]
75 pub const fn min_bytes(&self) -> usize {
76 self.min_bytes
77 }
78
79 #[must_use]
81 pub const fn max_bytes(&self) -> usize {
82 self.max_bytes
83 }
84
85 pub fn parse<'h>(
87 &self,
88 value: &'h HeaderValue,
89 ) -> Result<IdempotencyKey<'h>, InvalidIdempotencyKey> {
90 let bytes = value.as_bytes();
91 if bytes.is_empty() {
92 return Err(InvalidIdempotencyKey::Empty);
93 }
94 if !bytes.iter().all(u8::is_ascii_graphic) {
95 return Err(InvalidIdempotencyKey::InvalidCharacter);
96 }
97 if bytes.len() < self.min_bytes {
98 return Err(InvalidIdempotencyKey::TooShort);
99 }
100 if bytes.len() > self.max_bytes {
101 return Err(InvalidIdempotencyKey::TooLong);
102 }
103 std::str::from_utf8(bytes)
104 .map(IdempotencyKey)
105 .map_err(|_| InvalidIdempotencyKey::InvalidCharacter)
106 }
107
108 pub fn required<'h>(
112 &self,
113 headers: &'h HeaderMap,
114 ) -> Result<IdempotencyKey<'h>, IdempotencyError> {
115 self.optional(headers)?.ok_or(IdempotencyError::Required)
116 }
117
118 pub fn optional<'h>(
122 &self,
123 headers: &'h HeaderMap,
124 ) -> Result<Option<IdempotencyKey<'h>>, IdempotencyError> {
125 let mut values = headers.get_all(IDEMPOTENCY_KEY).iter();
126 let Some(value) = values.next() else {
127 return Ok(None);
128 };
129 if values.next().is_some() {
130 return Err(InvalidIdempotencyKey::RepeatedHeader.into());
131 }
132 Ok(Some(self.parse(value)?))
133 }
134}
135
136#[derive(Clone, Copy, Debug, Eq, PartialEq)]
138pub struct InvalidIdempotencyKeyRule;
139
140impl fmt::Display for InvalidIdempotencyKeyRule {
141 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
142 write!(
143 formatter,
144 "idempotency key bounds must satisfy 1 <= min <= max <= {MAX_IDEMPOTENCY_KEY_BYTES}"
145 )
146 }
147}
148
149impl StdError for InvalidIdempotencyKeyRule {}
150
151#[derive(Clone, Copy, Eq, Hash, PartialEq)]
155pub struct IdempotencyKey<'h>(&'h str);
156
157impl<'h> IdempotencyKey<'h> {
158 #[must_use]
160 pub const fn as_str(&self) -> &'h str {
161 self.0
162 }
163}
164
165impl fmt::Debug for IdempotencyKey<'_> {
166 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
167 formatter.write_str("IdempotencyKey(..)")
168 }
169}
170
171#[derive(Clone, Copy, Debug, Eq, PartialEq)]
173pub enum InvalidIdempotencyKey {
174 RepeatedHeader,
176 Empty,
178 InvalidCharacter,
180 TooShort,
182 TooLong,
184}
185
186impl InvalidIdempotencyKey {
187 #[must_use]
189 pub const fn reason(self) -> &'static str {
190 match self {
191 Self::RepeatedHeader => "repeated_header",
192 Self::Empty => "empty",
193 Self::InvalidCharacter => "invalid_character",
194 Self::TooShort => "too_short",
195 Self::TooLong => "too_long",
196 }
197 }
198}
199
200impl fmt::Display for InvalidIdempotencyKey {
201 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
202 write!(formatter, "invalid Idempotency-Key: {}", self.reason())
203 }
204}
205
206impl StdError for InvalidIdempotencyKey {}
207
208#[derive(Clone, Copy, Debug, Eq, PartialEq)]
213pub enum IdempotencyError {
214 Required,
216 Invalid(InvalidIdempotencyKey),
218 Reused,
220 InProgress,
222}
223
224impl IdempotencyError {
225 #[must_use]
227 pub const fn status(&self) -> StatusCode {
228 match self {
229 Self::Required | Self::Invalid(_) => StatusCode::BAD_REQUEST,
230 Self::Reused | Self::InProgress => StatusCode::CONFLICT,
231 }
232 }
233
234 #[must_use]
236 pub const fn code(&self) -> &'static str {
237 match self {
238 Self::Required => IDEMPOTENCY_KEY_REQUIRED_CODE,
239 Self::Invalid(_) => INVALID_IDEMPOTENCY_KEY_CODE,
240 Self::Reused => IDEMPOTENCY_KEY_REUSED_CODE,
241 Self::InProgress => IDEMPOTENCY_KEY_IN_PROGRESS_CODE,
242 }
243 }
244
245 const fn message(&self) -> &'static str {
246 match self {
247 Self::Required => "Idempotency-Key is required",
248 Self::Invalid(_) => "Idempotency-Key is not a valid key for this route",
249 Self::Reused => "Idempotency-Key was already used with a different request",
250 Self::InProgress => "A request with this Idempotency-Key is still running",
251 }
252 }
253
254 fn details(&self) -> BTreeMap<String, Value> {
255 match self {
256 Self::Invalid(reason) => {
257 BTreeMap::from([(REASON_DETAIL.to_owned(), Value::from(reason.reason()))])
258 }
259 Self::Required | Self::Reused | Self::InProgress => BTreeMap::new(),
260 }
261 }
262}
263
264impl From<InvalidIdempotencyKey> for IdempotencyError {
265 fn from(error: InvalidIdempotencyKey) -> Self {
266 Self::Invalid(error)
267 }
268}
269
270impl fmt::Display for IdempotencyError {
271 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
272 write!(formatter, "{} ({})", self.message(), self.code())
273 }
274}
275
276impl StdError for IdempotencyError {}
277
278impl From<IdempotencyError> for ApiError {
279 fn from(error: IdempotencyError) -> Self {
280 Self::new(error.status(), error.code(), error.message()).with_details(error.details())
281 }
282}
283
284const fn are_valid_bounds(min_bytes: usize, max_bytes: usize) -> bool {
285 min_bytes >= 1 && min_bytes <= max_bytes && max_bytes <= MAX_IDEMPOTENCY_KEY_BYTES
286}
287
288#[cfg(test)]
289mod tests;