1use core::fmt;
4
5use crate::rate_limit::RateLimit;
6use crate::transport::{MediaType, ResponseContentType, StatusCode, TransportResponse};
7
8#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
10pub enum ResponseBodyPolicy {
11 Required,
13 Optional,
15 Forbidden,
17}
18
19#[derive(Clone, Copy, Debug, Eq, PartialEq)]
21pub enum ContentTypePolicy {
22 Required(&'static [MediaType<'static>]),
24 Optional(&'static [MediaType<'static>]),
26 Forbidden,
28}
29
30#[derive(Clone, Copy, Debug, Eq, PartialEq)]
32pub enum ResponsePolicyValidationError {
33 MissingSuccessStatus,
35 NonSuccessStatus,
37 DuplicateSuccessStatus,
39 MissingAcceptedMediaType,
41 DuplicateAcceptedMediaType,
43 RequiredBodyHasZeroLimit,
45 ForbiddenBodyHasNonzeroLimit,
47 ForbiddenBodyAllowsContentType,
49 RequiredBodyForbidsContentType,
51}
52
53impl_static_error!(ResponsePolicyValidationError,
54 Self::MissingSuccessStatus => "response policy has no success status",
55 Self::NonSuccessStatus => "response policy contains a non-success status",
56 Self::DuplicateSuccessStatus => "response policy contains duplicate statuses",
57 Self::MissingAcceptedMediaType => "response policy has no accepted media type",
58 Self::DuplicateAcceptedMediaType => "response policy contains duplicate media types",
59 Self::RequiredBodyHasZeroLimit => "required response body has a zero limit",
60 Self::ForbiddenBodyHasNonzeroLimit => "forbidden response body has a nonzero limit",
61 Self::ForbiddenBodyAllowsContentType => "forbidden response body allows a content type",
62 Self::RequiredBodyForbidsContentType => "required response body forbids its content type",
63);
64
65#[derive(Clone, Copy, Debug, Eq, PartialEq)]
67pub enum ResponsePolicyError {
68 UnexpectedStatus,
70 BodyTooLarge,
72 MissingBody,
74 ForbiddenBody,
76 MissingContentType,
78 UnexpectedContentType,
80 ForbiddenContentType,
82}
83
84impl_static_error!(ResponsePolicyError,
85 Self::UnexpectedStatus => "response status is not expected",
86 Self::BodyTooLarge => "response body exceeds the operation limit",
87 Self::MissingBody => "required response body is missing",
88 Self::ForbiddenBody => "response body is forbidden",
89 Self::MissingContentType => "required response content type is missing",
90 Self::UnexpectedContentType => "response content type is not accepted",
91 Self::ForbiddenContentType => "response content type is forbidden",
92);
93
94#[derive(Clone, Copy, Debug, Eq, PartialEq)]
96pub struct ResponsePolicy {
97 success_statuses: &'static [StatusCode],
98 content_type: ContentTypePolicy,
99 body: ResponseBodyPolicy,
100 max_body_bytes: usize,
101}
102
103impl ResponsePolicy {
104 pub fn new(
106 success_statuses: &'static [StatusCode],
107 content_type: ContentTypePolicy,
108 body: ResponseBodyPolicy,
109 max_body_bytes: usize,
110 ) -> Result<Self, ResponsePolicyValidationError> {
111 validate_statuses(success_statuses)?;
112 validate_media_types(content_type)?;
113 match (body, content_type, max_body_bytes) {
114 (ResponseBodyPolicy::Required, _, 0) => {
115 return Err(ResponsePolicyValidationError::RequiredBodyHasZeroLimit);
116 }
117 (ResponseBodyPolicy::Forbidden, _, limit) if limit != 0 => {
118 return Err(ResponsePolicyValidationError::ForbiddenBodyHasNonzeroLimit);
119 }
120 (
121 ResponseBodyPolicy::Forbidden,
122 ContentTypePolicy::Required(_) | ContentTypePolicy::Optional(_),
123 _,
124 ) => {
125 return Err(ResponsePolicyValidationError::ForbiddenBodyAllowsContentType);
126 }
127 (ResponseBodyPolicy::Required, ContentTypePolicy::Forbidden, _) => {
128 return Err(ResponsePolicyValidationError::RequiredBodyForbidsContentType);
129 }
130 _ => {}
131 }
132 Ok(Self {
133 success_statuses,
134 content_type,
135 body,
136 max_body_bytes,
137 })
138 }
139
140 #[must_use]
142 pub const fn success_statuses(self) -> &'static [StatusCode] {
143 self.success_statuses
144 }
145
146 #[must_use]
148 pub const fn content_type_policy(self) -> ContentTypePolicy {
149 self.content_type
150 }
151
152 #[must_use]
154 pub const fn body_policy(self) -> ResponseBodyPolicy {
155 self.body
156 }
157
158 #[must_use]
160 pub const fn max_body_bytes(self) -> usize {
161 self.max_body_bytes
162 }
163
164 pub fn validate<'body>(
166 self,
167 response: TransportResponse<'body>,
168 ) -> Result<CheckedResponse<'body>, ResponsePolicyError> {
169 if !self.success_statuses.contains(&response.status()) {
170 return Err(ResponsePolicyError::UnexpectedStatus);
171 }
172 match self.body {
173 ResponseBodyPolicy::Forbidden if !response.body().is_empty() => {
174 return Err(ResponsePolicyError::ForbiddenBody);
175 }
176 _ => {}
177 }
178 if response.body().len() > self.max_body_bytes {
179 return Err(ResponsePolicyError::BodyTooLarge);
180 }
181 if matches!(self.body, ResponseBodyPolicy::Required) && response.body().is_empty() {
182 return Err(ResponsePolicyError::MissingBody);
183 }
184 validate_content_type(self.content_type, response.content_type())?;
185 Ok(CheckedResponse { response })
186 }
187}
188
189#[derive(Clone, Copy, Eq, PartialEq)]
191pub struct CheckedResponse<'body> {
192 response: TransportResponse<'body>,
193}
194
195impl CheckedResponse<'_> {
196 #[must_use]
198 pub const fn status(&self) -> StatusCode {
199 self.response.status()
200 }
201
202 #[must_use]
204 pub const fn body(&self) -> &[u8] {
205 self.response.body()
206 }
207
208 #[must_use]
210 pub const fn content_type(&self) -> Option<ResponseContentType> {
211 self.response.content_type()
212 }
213
214 #[must_use]
216 pub const fn rate_limit(&self) -> Option<RateLimit> {
217 self.response.rate_limit()
218 }
219}
220
221impl fmt::Debug for CheckedResponse<'_> {
222 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
223 formatter
224 .debug_struct("CheckedResponse")
225 .field("status", &self.status())
226 .field("body_len", &self.body().len())
227 .field("body", &"[redacted]")
228 .field("content_type", &self.content_type())
229 .field("rate_limit", &self.rate_limit())
230 .finish()
231 }
232}
233
234fn validate_statuses(statuses: &[StatusCode]) -> Result<(), ResponsePolicyValidationError> {
235 if statuses.is_empty() {
236 return Err(ResponsePolicyValidationError::MissingSuccessStatus);
237 }
238 for (index, status) in statuses.iter().enumerate() {
239 if !status.is_success() {
240 return Err(ResponsePolicyValidationError::NonSuccessStatus);
241 }
242 if statuses
243 .get(..index)
244 .is_some_and(|seen| seen.contains(status))
245 {
246 return Err(ResponsePolicyValidationError::DuplicateSuccessStatus);
247 }
248 }
249 Ok(())
250}
251
252fn validate_media_types(policy: ContentTypePolicy) -> Result<(), ResponsePolicyValidationError> {
253 let media_types = match policy {
254 ContentTypePolicy::Required(values) | ContentTypePolicy::Optional(values) => values,
255 ContentTypePolicy::Forbidden => return Ok(()),
256 };
257 if media_types.is_empty() {
258 return Err(ResponsePolicyValidationError::MissingAcceptedMediaType);
259 }
260 for (index, media_type) in media_types.iter().enumerate() {
261 if media_types.get(..index).is_some_and(|seen| {
262 seen.iter()
263 .any(|candidate| candidate.as_str().eq_ignore_ascii_case(media_type.as_str()))
264 }) {
265 return Err(ResponsePolicyValidationError::DuplicateAcceptedMediaType);
266 }
267 }
268 Ok(())
269}
270
271fn validate_content_type(
272 policy: ContentTypePolicy,
273 actual: Option<ResponseContentType>,
274) -> Result<(), ResponsePolicyError> {
275 match (policy, actual) {
276 (ContentTypePolicy::Required(_), None) => Err(ResponsePolicyError::MissingContentType),
277 (ContentTypePolicy::Forbidden, Some(_)) => Err(ResponsePolicyError::ForbiddenContentType),
278 (ContentTypePolicy::Forbidden | ContentTypePolicy::Optional(_), None) => Ok(()),
279 (
280 ContentTypePolicy::Required(accepted) | ContentTypePolicy::Optional(accepted),
281 Some(actual),
282 ) => {
283 if accepted
284 .iter()
285 .any(|media_type| actual.matches(*media_type))
286 {
287 Ok(())
288 } else {
289 Err(ResponsePolicyError::UnexpectedContentType)
290 }
291 }
292 }
293}