use core::fmt;
use crate::rate_limit::RateLimit;
use crate::transport::{MediaType, ResponseContentType, StatusCode, TransportResponse};
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum ResponseBodyPolicy {
Required,
Optional,
Forbidden,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ContentTypePolicy {
Required(&'static [MediaType<'static>]),
Optional(&'static [MediaType<'static>]),
Forbidden,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ResponsePolicyValidationError {
MissingSuccessStatus,
NonSuccessStatus,
DuplicateSuccessStatus,
MissingAcceptedMediaType,
DuplicateAcceptedMediaType,
RequiredBodyHasZeroLimit,
ForbiddenBodyHasNonzeroLimit,
ForbiddenBodyAllowsContentType,
RequiredBodyForbidsContentType,
}
impl_static_error!(ResponsePolicyValidationError,
Self::MissingSuccessStatus => "response policy has no success status",
Self::NonSuccessStatus => "response policy contains a non-success status",
Self::DuplicateSuccessStatus => "response policy contains duplicate statuses",
Self::MissingAcceptedMediaType => "response policy has no accepted media type",
Self::DuplicateAcceptedMediaType => "response policy contains duplicate media types",
Self::RequiredBodyHasZeroLimit => "required response body has a zero limit",
Self::ForbiddenBodyHasNonzeroLimit => "forbidden response body has a nonzero limit",
Self::ForbiddenBodyAllowsContentType => "forbidden response body allows a content type",
Self::RequiredBodyForbidsContentType => "required response body forbids its content type",
);
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ResponsePolicyError {
UnexpectedStatus,
BodyTooLarge,
MissingBody,
ForbiddenBody,
MissingContentType,
UnexpectedContentType,
ForbiddenContentType,
}
impl_static_error!(ResponsePolicyError,
Self::UnexpectedStatus => "response status is not expected",
Self::BodyTooLarge => "response body exceeds the operation limit",
Self::MissingBody => "required response body is missing",
Self::ForbiddenBody => "response body is forbidden",
Self::MissingContentType => "required response content type is missing",
Self::UnexpectedContentType => "response content type is not accepted",
Self::ForbiddenContentType => "response content type is forbidden",
);
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ResponsePolicy {
success_statuses: &'static [StatusCode],
content_type: ContentTypePolicy,
body: ResponseBodyPolicy,
max_body_bytes: usize,
}
impl ResponsePolicy {
pub fn new(
success_statuses: &'static [StatusCode],
content_type: ContentTypePolicy,
body: ResponseBodyPolicy,
max_body_bytes: usize,
) -> Result<Self, ResponsePolicyValidationError> {
validate_statuses(success_statuses)?;
validate_media_types(content_type)?;
match (body, content_type, max_body_bytes) {
(ResponseBodyPolicy::Required, _, 0) => {
return Err(ResponsePolicyValidationError::RequiredBodyHasZeroLimit);
}
(ResponseBodyPolicy::Forbidden, _, limit) if limit != 0 => {
return Err(ResponsePolicyValidationError::ForbiddenBodyHasNonzeroLimit);
}
(
ResponseBodyPolicy::Forbidden,
ContentTypePolicy::Required(_) | ContentTypePolicy::Optional(_),
_,
) => {
return Err(ResponsePolicyValidationError::ForbiddenBodyAllowsContentType);
}
(ResponseBodyPolicy::Required, ContentTypePolicy::Forbidden, _) => {
return Err(ResponsePolicyValidationError::RequiredBodyForbidsContentType);
}
_ => {}
}
Ok(Self {
success_statuses,
content_type,
body,
max_body_bytes,
})
}
#[must_use]
pub const fn success_statuses(self) -> &'static [StatusCode] {
self.success_statuses
}
#[must_use]
pub const fn content_type_policy(self) -> ContentTypePolicy {
self.content_type
}
#[must_use]
pub const fn body_policy(self) -> ResponseBodyPolicy {
self.body
}
#[must_use]
pub const fn max_body_bytes(self) -> usize {
self.max_body_bytes
}
pub fn validate<'body>(
self,
response: TransportResponse<'body>,
) -> Result<CheckedResponse<'body>, ResponsePolicyError> {
if !self.success_statuses.contains(&response.status()) {
return Err(ResponsePolicyError::UnexpectedStatus);
}
match self.body {
ResponseBodyPolicy::Forbidden if !response.body().is_empty() => {
return Err(ResponsePolicyError::ForbiddenBody);
}
_ => {}
}
if response.body().len() > self.max_body_bytes {
return Err(ResponsePolicyError::BodyTooLarge);
}
if matches!(self.body, ResponseBodyPolicy::Required) && response.body().is_empty() {
return Err(ResponsePolicyError::MissingBody);
}
validate_content_type(self.content_type, response.content_type())?;
Ok(CheckedResponse { response })
}
}
#[derive(Clone, Copy, Eq, PartialEq)]
pub struct CheckedResponse<'body> {
response: TransportResponse<'body>,
}
impl CheckedResponse<'_> {
#[must_use]
pub const fn status(&self) -> StatusCode {
self.response.status()
}
#[must_use]
pub const fn body(&self) -> &[u8] {
self.response.body()
}
#[must_use]
pub const fn content_type(&self) -> Option<ResponseContentType> {
self.response.content_type()
}
#[must_use]
pub const fn rate_limit(&self) -> Option<RateLimit> {
self.response.rate_limit()
}
}
impl fmt::Debug for CheckedResponse<'_> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("CheckedResponse")
.field("status", &self.status())
.field("body_len", &self.body().len())
.field("body", &"[redacted]")
.field("content_type", &self.content_type())
.field("rate_limit", &self.rate_limit())
.finish()
}
}
fn validate_statuses(statuses: &[StatusCode]) -> Result<(), ResponsePolicyValidationError> {
if statuses.is_empty() {
return Err(ResponsePolicyValidationError::MissingSuccessStatus);
}
for (index, status) in statuses.iter().enumerate() {
if !status.is_success() {
return Err(ResponsePolicyValidationError::NonSuccessStatus);
}
if statuses
.get(..index)
.is_some_and(|seen| seen.contains(status))
{
return Err(ResponsePolicyValidationError::DuplicateSuccessStatus);
}
}
Ok(())
}
fn validate_media_types(policy: ContentTypePolicy) -> Result<(), ResponsePolicyValidationError> {
let media_types = match policy {
ContentTypePolicy::Required(values) | ContentTypePolicy::Optional(values) => values,
ContentTypePolicy::Forbidden => return Ok(()),
};
if media_types.is_empty() {
return Err(ResponsePolicyValidationError::MissingAcceptedMediaType);
}
for (index, media_type) in media_types.iter().enumerate() {
if media_types.get(..index).is_some_and(|seen| {
seen.iter()
.any(|candidate| candidate.as_str().eq_ignore_ascii_case(media_type.as_str()))
}) {
return Err(ResponsePolicyValidationError::DuplicateAcceptedMediaType);
}
}
Ok(())
}
fn validate_content_type(
policy: ContentTypePolicy,
actual: Option<ResponseContentType>,
) -> Result<(), ResponsePolicyError> {
match (policy, actual) {
(ContentTypePolicy::Required(_), None) => Err(ResponsePolicyError::MissingContentType),
(ContentTypePolicy::Forbidden, Some(_)) => Err(ResponsePolicyError::ForbiddenContentType),
(ContentTypePolicy::Forbidden | ContentTypePolicy::Optional(_), None) => Ok(()),
(
ContentTypePolicy::Required(accepted) | ContentTypePolicy::Optional(accepted),
Some(actual),
) => {
if accepted
.iter()
.any(|media_type| actual.matches(*media_type))
{
Ok(())
} else {
Err(ResponsePolicyError::UnexpectedContentType)
}
}
}
}