use http::{header, HeaderMap, StatusCode};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ResponseClass {
PartialContent,
NotModified,
RangeNotSatisfiable,
AuthChallenge,
RateLimited,
StructuredError,
Redirect,
Success,
Other,
}
impl ResponseClass {
#[must_use]
pub fn classify(status: StatusCode, headers: &HeaderMap) -> Self {
if status == StatusCode::PARTIAL_CONTENT {
return Self::PartialContent;
}
if status == StatusCode::NOT_MODIFIED {
return Self::NotModified;
}
if status == StatusCode::RANGE_NOT_SATISFIABLE {
return Self::RangeNotSatisfiable;
}
if status == StatusCode::UNAUTHORIZED || status == StatusCode::PROXY_AUTHENTICATION_REQUIRED
{
return Self::AuthChallenge;
}
if status == StatusCode::TOO_MANY_REQUESTS || status == StatusCode::SERVICE_UNAVAILABLE {
return Self::RateLimited;
}
if status.is_client_error() && has_json_content_type(headers) {
return Self::StructuredError;
}
if status.is_redirection() && headers.contains_key(header::LOCATION) {
return Self::Redirect;
}
if status.is_success() {
return Self::Success;
}
Self::Other
}
}
fn has_json_content_type(headers: &HeaderMap) -> bool {
headers
.get(header::CONTENT_TYPE)
.and_then(|v| v.to_str().ok())
.is_some_and(|ct| {
ct.contains("application/problem+json") || ct.contains("application/json")
})
}
#[cfg(test)]
mod tests {
use super::*;
use http::HeaderValue;
fn empty_headers() -> HeaderMap {
HeaderMap::new()
}
fn headers_with_content_type(ct: &str) -> HeaderMap {
let mut map = HeaderMap::new();
map.insert(
header::CONTENT_TYPE,
HeaderValue::from_str(ct).expect("valid header value"),
);
map
}
fn headers_with_location(url: &str) -> HeaderMap {
let mut map = HeaderMap::new();
map.insert(
header::LOCATION,
HeaderValue::from_str(url).expect("valid header value"),
);
map
}
#[test]
fn partial_content_206() {
assert_eq!(
ResponseClass::classify(StatusCode::PARTIAL_CONTENT, &empty_headers()),
ResponseClass::PartialContent
);
}
#[test]
fn not_modified_304() {
assert_eq!(
ResponseClass::classify(StatusCode::NOT_MODIFIED, &empty_headers()),
ResponseClass::NotModified
);
}
#[test]
fn range_not_satisfiable_416() {
assert_eq!(
ResponseClass::classify(StatusCode::RANGE_NOT_SATISFIABLE, &empty_headers()),
ResponseClass::RangeNotSatisfiable
);
}
#[test]
fn auth_challenge_401() {
assert_eq!(
ResponseClass::classify(StatusCode::UNAUTHORIZED, &empty_headers()),
ResponseClass::AuthChallenge
);
}
#[test]
fn auth_challenge_407() {
assert_eq!(
ResponseClass::classify(StatusCode::PROXY_AUTHENTICATION_REQUIRED, &empty_headers()),
ResponseClass::AuthChallenge
);
}
#[test]
fn rate_limited_429() {
assert_eq!(
ResponseClass::classify(StatusCode::TOO_MANY_REQUESTS, &empty_headers()),
ResponseClass::RateLimited
);
}
#[test]
fn rate_limited_503() {
assert_eq!(
ResponseClass::classify(StatusCode::SERVICE_UNAVAILABLE, &empty_headers()),
ResponseClass::RateLimited
);
}
#[test]
fn structured_error_problem_json() {
assert_eq!(
ResponseClass::classify(
StatusCode::BAD_REQUEST,
&headers_with_content_type("application/problem+json")
),
ResponseClass::StructuredError
);
}
#[test]
fn structured_error_application_json() {
assert_eq!(
ResponseClass::classify(
StatusCode::UNPROCESSABLE_ENTITY,
&headers_with_content_type("application/json")
),
ResponseClass::StructuredError
);
}
#[test]
fn redirect_301_with_location() {
assert_eq!(
ResponseClass::classify(
StatusCode::MOVED_PERMANENTLY,
&headers_with_location("https://example.com/new")
),
ResponseClass::Redirect
);
}
#[test]
fn success_200() {
assert_eq!(
ResponseClass::classify(StatusCode::OK, &empty_headers()),
ResponseClass::Success
);
}
#[test]
fn success_201() {
assert_eq!(
ResponseClass::classify(StatusCode::CREATED, &empty_headers()),
ResponseClass::Success
);
}
#[test]
fn other_500() {
assert_eq!(
ResponseClass::classify(StatusCode::INTERNAL_SERVER_ERROR, &empty_headers()),
ResponseClass::Other
);
}
#[test]
fn other_100() {
assert_eq!(
ResponseClass::classify(StatusCode::CONTINUE, &empty_headers()),
ResponseClass::Other
);
}
#[test]
fn precedence_206_not_success() {
assert_eq!(
ResponseClass::classify(StatusCode::PARTIAL_CONTENT, &empty_headers()),
ResponseClass::PartialContent
);
}
#[test]
fn precedence_304_not_redirect_even_with_location() {
assert_eq!(
ResponseClass::classify(
StatusCode::NOT_MODIFIED,
&headers_with_location("https://example.com/new")
),
ResponseClass::NotModified
);
}
#[test]
fn precedence_401_not_structured_error_with_json_content_type() {
assert_eq!(
ResponseClass::classify(
StatusCode::UNAUTHORIZED,
&headers_with_content_type("application/json")
),
ResponseClass::AuthChallenge
);
}
#[test]
fn precedence_429_not_structured_error_with_problem_json() {
assert_eq!(
ResponseClass::classify(
StatusCode::TOO_MANY_REQUESTS,
&headers_with_content_type("application/problem+json")
),
ResponseClass::RateLimited
);
}
#[test]
fn other_4xx_without_json_content_type() {
assert_eq!(
ResponseClass::classify(StatusCode::BAD_REQUEST, &empty_headers()),
ResponseClass::Other
);
}
#[test]
fn other_3xx_without_location() {
assert_eq!(
ResponseClass::classify(StatusCode::MOVED_PERMANENTLY, &empty_headers()),
ResponseClass::Other
);
}
}