parlov_core/
response_class.rs1use http::{header, HeaderMap, StatusCode};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum ResponseClass {
14 PartialContent,
16 NotModified,
18 RangeNotSatisfiable,
20 AuthChallenge,
22 RateLimited,
24 StructuredError,
26 Redirect,
28 Success,
30 Other,
32}
33
34impl ResponseClass {
35 #[must_use]
40 pub fn classify(status: StatusCode, headers: &HeaderMap) -> Self {
41 if status == StatusCode::PARTIAL_CONTENT {
42 return Self::PartialContent;
43 }
44 if status == StatusCode::NOT_MODIFIED {
45 return Self::NotModified;
46 }
47 if status == StatusCode::RANGE_NOT_SATISFIABLE {
48 return Self::RangeNotSatisfiable;
49 }
50 if status == StatusCode::UNAUTHORIZED || status == StatusCode::PROXY_AUTHENTICATION_REQUIRED
51 {
52 return Self::AuthChallenge;
53 }
54 if status == StatusCode::TOO_MANY_REQUESTS || status == StatusCode::SERVICE_UNAVAILABLE {
55 return Self::RateLimited;
56 }
57 if status.is_client_error() && has_json_content_type(headers) {
58 return Self::StructuredError;
59 }
60 if status.is_redirection() && headers.contains_key(header::LOCATION) {
61 return Self::Redirect;
62 }
63 if status.is_success() {
64 return Self::Success;
65 }
66 Self::Other
67 }
68}
69
70fn has_json_content_type(headers: &HeaderMap) -> bool {
74 headers
75 .get(header::CONTENT_TYPE)
76 .and_then(|v| v.to_str().ok())
77 .is_some_and(|ct| {
78 ct.contains("application/problem+json") || ct.contains("application/json")
79 })
80}
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85 use http::HeaderValue;
86
87 fn empty_headers() -> HeaderMap {
88 HeaderMap::new()
89 }
90
91 fn headers_with_content_type(ct: &str) -> HeaderMap {
92 let mut map = HeaderMap::new();
93 map.insert(
94 header::CONTENT_TYPE,
95 HeaderValue::from_str(ct).expect("valid header value"),
96 );
97 map
98 }
99
100 fn headers_with_location(url: &str) -> HeaderMap {
101 let mut map = HeaderMap::new();
102 map.insert(
103 header::LOCATION,
104 HeaderValue::from_str(url).expect("valid header value"),
105 );
106 map
107 }
108
109 #[test]
112 fn partial_content_206() {
113 assert_eq!(
114 ResponseClass::classify(StatusCode::PARTIAL_CONTENT, &empty_headers()),
115 ResponseClass::PartialContent
116 );
117 }
118
119 #[test]
120 fn not_modified_304() {
121 assert_eq!(
122 ResponseClass::classify(StatusCode::NOT_MODIFIED, &empty_headers()),
123 ResponseClass::NotModified
124 );
125 }
126
127 #[test]
128 fn range_not_satisfiable_416() {
129 assert_eq!(
130 ResponseClass::classify(StatusCode::RANGE_NOT_SATISFIABLE, &empty_headers()),
131 ResponseClass::RangeNotSatisfiable
132 );
133 }
134
135 #[test]
136 fn auth_challenge_401() {
137 assert_eq!(
138 ResponseClass::classify(StatusCode::UNAUTHORIZED, &empty_headers()),
139 ResponseClass::AuthChallenge
140 );
141 }
142
143 #[test]
144 fn auth_challenge_407() {
145 assert_eq!(
146 ResponseClass::classify(StatusCode::PROXY_AUTHENTICATION_REQUIRED, &empty_headers()),
147 ResponseClass::AuthChallenge
148 );
149 }
150
151 #[test]
152 fn rate_limited_429() {
153 assert_eq!(
154 ResponseClass::classify(StatusCode::TOO_MANY_REQUESTS, &empty_headers()),
155 ResponseClass::RateLimited
156 );
157 }
158
159 #[test]
160 fn rate_limited_503() {
161 assert_eq!(
162 ResponseClass::classify(StatusCode::SERVICE_UNAVAILABLE, &empty_headers()),
163 ResponseClass::RateLimited
164 );
165 }
166
167 #[test]
168 fn structured_error_problem_json() {
169 assert_eq!(
170 ResponseClass::classify(
171 StatusCode::BAD_REQUEST,
172 &headers_with_content_type("application/problem+json")
173 ),
174 ResponseClass::StructuredError
175 );
176 }
177
178 #[test]
179 fn structured_error_application_json() {
180 assert_eq!(
181 ResponseClass::classify(
182 StatusCode::UNPROCESSABLE_ENTITY,
183 &headers_with_content_type("application/json")
184 ),
185 ResponseClass::StructuredError
186 );
187 }
188
189 #[test]
190 fn redirect_301_with_location() {
191 assert_eq!(
192 ResponseClass::classify(
193 StatusCode::MOVED_PERMANENTLY,
194 &headers_with_location("https://example.com/new")
195 ),
196 ResponseClass::Redirect
197 );
198 }
199
200 #[test]
201 fn success_200() {
202 assert_eq!(
203 ResponseClass::classify(StatusCode::OK, &empty_headers()),
204 ResponseClass::Success
205 );
206 }
207
208 #[test]
209 fn success_201() {
210 assert_eq!(
211 ResponseClass::classify(StatusCode::CREATED, &empty_headers()),
212 ResponseClass::Success
213 );
214 }
215
216 #[test]
217 fn other_500() {
218 assert_eq!(
219 ResponseClass::classify(StatusCode::INTERNAL_SERVER_ERROR, &empty_headers()),
220 ResponseClass::Other
221 );
222 }
223
224 #[test]
225 fn other_100() {
226 assert_eq!(
227 ResponseClass::classify(StatusCode::CONTINUE, &empty_headers()),
228 ResponseClass::Other
229 );
230 }
231
232 #[test]
235 fn precedence_206_not_success() {
236 assert_eq!(
238 ResponseClass::classify(StatusCode::PARTIAL_CONTENT, &empty_headers()),
239 ResponseClass::PartialContent
240 );
241 }
242
243 #[test]
244 fn precedence_304_not_redirect_even_with_location() {
245 assert_eq!(
247 ResponseClass::classify(
248 StatusCode::NOT_MODIFIED,
249 &headers_with_location("https://example.com/new")
250 ),
251 ResponseClass::NotModified
252 );
253 }
254
255 #[test]
256 fn precedence_401_not_structured_error_with_json_content_type() {
257 assert_eq!(
259 ResponseClass::classify(
260 StatusCode::UNAUTHORIZED,
261 &headers_with_content_type("application/json")
262 ),
263 ResponseClass::AuthChallenge
264 );
265 }
266
267 #[test]
268 fn precedence_429_not_structured_error_with_problem_json() {
269 assert_eq!(
271 ResponseClass::classify(
272 StatusCode::TOO_MANY_REQUESTS,
273 &headers_with_content_type("application/problem+json")
274 ),
275 ResponseClass::RateLimited
276 );
277 }
278
279 #[test]
280 fn other_4xx_without_json_content_type() {
281 assert_eq!(
283 ResponseClass::classify(StatusCode::BAD_REQUEST, &empty_headers()),
284 ResponseClass::Other
285 );
286 }
287
288 #[test]
289 fn other_3xx_without_location() {
290 assert_eq!(
292 ResponseClass::classify(StatusCode::MOVED_PERMANENTLY, &empty_headers()),
293 ResponseClass::Other
294 );
295 }
296}