1use axum::response::{IntoResponse, Response};
2use http::{HeaderName, HeaderValue, header};
3use std::time::{SystemTime, UNIX_EPOCH};
4use thiserror::Error;
5
6pub static DEPRECATION_HEADER: HeaderName = HeaderName::from_static("deprecation");
7pub static SUNSET_HEADER: HeaderName = HeaderName::from_static("sunset");
8
9#[non_exhaustive]
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum BearerChallenge {
17 Required,
18 InvalidRequest,
19 InvalidToken,
20 InsufficientScope,
21}
22
23impl BearerChallenge {
24 #[must_use]
25 pub const fn as_str(self) -> &'static str {
26 match self {
27 Self::Required => "Bearer",
28 Self::InvalidRequest => r#"Bearer error="invalid_request""#,
29 Self::InvalidToken => r#"Bearer error="invalid_token""#,
30 Self::InsufficientScope => r#"Bearer error="insufficient_scope""#,
31 }
32 }
33}
34
35#[must_use]
40#[derive(Debug, Clone, Default)]
41pub struct ApiResponseMetadata {
42 retry_after: Option<HeaderValue>,
43 www_authenticate: Option<HeaderValue>,
44 deprecation: Option<HeaderValue>,
45 sunset: Option<HeaderValue>,
46 links: Vec<HeaderValue>,
47}
48
49impl ApiResponseMetadata {
50 pub const fn new() -> Self {
51 Self {
52 retry_after: None,
53 www_authenticate: None,
54 deprecation: None,
55 sunset: None,
56 links: Vec::new(),
57 }
58 }
59
60 pub fn retry_after(mut self, value: HeaderValue) -> Self {
62 self.retry_after = Some(value);
63 self
64 }
65
66 pub fn retry_after_seconds(self, seconds: u64) -> Self {
67 self.retry_after(
68 HeaderValue::from_str(&seconds.to_string())
69 .expect("a retry delay generated from u64 is a valid header value"),
70 )
71 }
72
73 pub fn www_authenticate(mut self, value: HeaderValue) -> Self {
74 self.www_authenticate = Some(value);
75 self
76 }
77
78 pub fn bearer_challenge(self, challenge: BearerChallenge) -> Self {
79 self.www_authenticate(HeaderValue::from_static(challenge.as_str()))
80 }
81
82 pub fn deprecation_at(
84 mut self,
85 deprecation: SystemTime,
86 ) -> Result<Self, ApiResponseMetadataError> {
87 let seconds = deprecation
88 .duration_since(UNIX_EPOCH)
89 .map_err(|_| ApiResponseMetadataError::BeforeUnixEpoch)?
90 .as_secs();
91 self.deprecation = Some(
92 HeaderValue::from_str(&format!("@{seconds}"))
93 .expect("a deprecation date generated from u64 is a valid header value"),
94 );
95 Ok(self)
96 }
97
98 pub fn sunset(mut self, sunset: HeaderValue) -> Self {
100 self.sunset = Some(sunset);
101 self
102 }
103
104 pub fn link(mut self, link: HeaderValue) -> Self {
106 self.links.push(link);
107 self
108 }
109
110 pub fn apply(self, response: &mut Response) {
111 let headers = response.headers_mut();
112 if let Some(value) = self.retry_after {
113 headers.insert(header::RETRY_AFTER, value);
114 }
115 if let Some(value) = self.www_authenticate {
116 headers.insert(header::WWW_AUTHENTICATE, value);
117 }
118 if let Some(value) = self.deprecation {
119 headers.insert(DEPRECATION_HEADER.clone(), value);
120 }
121 if let Some(value) = self.sunset {
122 headers.insert(SUNSET_HEADER.clone(), value);
123 }
124 for link in self.links {
125 headers.append(header::LINK, link);
126 }
127 }
128
129 pub const fn wrap<T>(self, inner: T) -> ApiResponse<T> {
130 ApiResponse {
131 inner,
132 metadata: self,
133 }
134 }
135}
136
137#[must_use]
139#[derive(Debug, Clone)]
140pub struct ApiResponse<T> {
141 inner: T,
142 metadata: ApiResponseMetadata,
143}
144
145impl<T> IntoResponse for ApiResponse<T>
146where
147 T: IntoResponse,
148{
149 fn into_response(self) -> Response {
150 let mut response = self.inner.into_response();
151 self.metadata.apply(&mut response);
152 response
153 }
154}
155
156#[non_exhaustive]
157#[derive(Debug, Error, Clone, Copy, PartialEq, Eq)]
158pub enum ApiResponseMetadataError {
159 #[error("deprecation timestamps before the Unix epoch are unsupported")]
160 BeforeUnixEpoch,
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166 use http::StatusCode;
167 use std::time::Duration;
168
169 #[test]
170 fn metadata_wraps_any_axum_response_without_changing_its_status() {
171 let response = ApiResponseMetadata::new()
172 .retry_after_seconds(30)
173 .bearer_challenge(BearerChallenge::InvalidToken)
174 .deprecation_at(UNIX_EPOCH + Duration::from_hours(500_000))
175 .unwrap()
176 .sunset(HeaderValue::from_static("Sun, 06 Nov 1994 08:49:37 GMT"))
177 .link(HeaderValue::from_static(
178 r#"<https://api.example.invalid/migration>; rel="deprecation""#,
179 ))
180 .link(HeaderValue::from_static(
181 r#"<https://api.example.invalid/replacement>; rel="successor-version""#,
182 ))
183 .wrap((StatusCode::TOO_MANY_REQUESTS, "slow down"))
184 .into_response();
185
186 assert_eq!(response.status(), StatusCode::TOO_MANY_REQUESTS);
187 assert_eq!(response.headers()[header::RETRY_AFTER], "30");
188 assert_eq!(
189 response.headers()[header::WWW_AUTHENTICATE],
190 r#"Bearer error="invalid_token""#
191 );
192 assert_eq!(response.headers()[&DEPRECATION_HEADER], "@1800000000");
193 assert_eq!(
194 response.headers()[&SUNSET_HEADER],
195 "Sun, 06 Nov 1994 08:49:37 GMT"
196 );
197 assert_eq!(response.headers().get_all(header::LINK).iter().count(), 2);
198 }
199
200 #[test]
201 fn retry_after_accepts_an_http_date() {
202 let response = ApiResponseMetadata::new()
203 .retry_after(HeaderValue::from_static("Sun, 06 Nov 1994 08:49:37 GMT"))
204 .wrap((StatusCode::SERVICE_UNAVAILABLE, "try later"))
205 .into_response();
206
207 assert_eq!(
208 response.headers()[header::RETRY_AFTER],
209 "Sun, 06 Nov 1994 08:49:37 GMT"
210 );
211 }
212
213 #[test]
214 fn deprecation_rejects_dates_before_the_unix_epoch() {
215 let before_epoch = UNIX_EPOCH.checked_sub(Duration::from_secs(1)).unwrap();
216 let error = ApiResponseMetadata::new()
217 .deprecation_at(before_epoch)
218 .unwrap_err();
219 assert_eq!(error, ApiResponseMetadataError::BeforeUnixEpoch);
220 }
221}