Skip to main content

cloud_sdk/
transport.rs

1//! Provider-neutral blocking and asynchronous transport contracts.
2
3mod asynchronous;
4
5pub use asynchronous::AsyncTransport;
6
7use core::fmt;
8
9use crate::Method;
10use crate::rate_limit::RateLimit;
11
12/// Maximum origin-form request-target length admitted by the core contract.
13pub const MAX_REQUEST_TARGET_BYTES: usize = 8192;
14
15/// Maximum content-type header value length admitted by the core contract.
16pub const MAX_CONTENT_TYPE_BYTES: usize = 128;
17
18/// Content-type validation error.
19#[derive(Clone, Copy, Debug, Eq, PartialEq)]
20pub enum ContentTypeError {
21    /// Content types must not be empty.
22    Empty,
23    /// Content types exceed [`MAX_CONTENT_TYPE_BYTES`].
24    TooLong,
25    /// Content types must contain a token-shaped `type/subtype` essence and
26    /// only visible ASCII bytes.
27    Invalid,
28}
29
30impl_static_error!(ContentTypeError,
31    Self::Empty => "content type is empty",
32    Self::TooLong => "content type exceeds the length limit",
33    Self::Invalid => "content type is invalid",
34);
35
36/// Borrowed, validated HTTP content type.
37#[derive(Clone, Copy, Eq, PartialEq)]
38pub struct ContentType<'a> {
39    value: &'a str,
40}
41
42impl fmt::Debug for ContentType<'_> {
43    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
44        formatter.write_str("ContentType([redacted])")
45    }
46}
47
48impl<'a> ContentType<'a> {
49    /// `application/json`.
50    pub const JSON: Self = Self {
51        value: "application/json",
52    };
53
54    /// Validates a content-type header value.
55    pub fn new(value: &'a str) -> Result<Self, ContentTypeError> {
56        if value.is_empty() {
57            return Err(ContentTypeError::Empty);
58        }
59        if value.len() > MAX_CONTENT_TYPE_BYTES {
60            return Err(ContentTypeError::TooLong);
61        }
62        if !value.bytes().all(|byte| (b' '..=b'~').contains(&byte)) {
63            return Err(ContentTypeError::Invalid);
64        }
65        let essence = value.split(';').next().unwrap_or_default();
66        let Some((media_type, subtype)) = essence.split_once('/') else {
67            return Err(ContentTypeError::Invalid);
68        };
69        if media_type.is_empty()
70            || subtype.is_empty()
71            || !media_type.bytes().all(is_http_token_byte)
72            || !subtype.bytes().all(is_http_token_byte)
73        {
74            return Err(ContentTypeError::Invalid);
75        }
76        Ok(Self { value })
77    }
78
79    /// Returns the validated header value.
80    #[must_use]
81    pub const fn as_str(self) -> &'a str {
82        self.value
83    }
84}
85
86const fn is_http_token_byte(byte: u8) -> bool {
87    byte.is_ascii_alphanumeric()
88        || matches!(
89            byte,
90            b'!' | b'#'
91                | b'$'
92                | b'%'
93                | b'&'
94                | b'\''
95                | b'*'
96                | b'+'
97                | b'-'
98                | b'.'
99                | b'^'
100                | b'_'
101                | b'`'
102                | b'|'
103                | b'~'
104        )
105}
106
107/// Request-target validation error.
108#[derive(Clone, Copy, Debug, Eq, PartialEq)]
109pub enum RequestTargetError {
110    /// Request targets must not be empty.
111    Empty,
112    /// Request targets must start with `/`.
113    NotOriginForm,
114    /// Request targets exceed [`MAX_REQUEST_TARGET_BYTES`].
115    TooLong,
116    /// Request targets contain a control, space, non-ASCII, fragment, or
117    /// backslash byte.
118    InvalidByte,
119}
120
121impl_static_error!(RequestTargetError,
122    Self::Empty => "request target is empty",
123    Self::NotOriginForm => "request target is not in origin form",
124    Self::TooLong => "request target exceeds the length limit",
125    Self::InvalidByte => "request target contains a forbidden byte",
126);
127
128/// Validated origin-form HTTP request target.
129#[derive(Clone, Copy, Eq, PartialEq)]
130pub struct RequestTarget<'a> {
131    value: &'a str,
132}
133
134impl<'a> RequestTarget<'a> {
135    /// Validates a `/path?query` request target.
136    pub fn new(value: &'a str) -> Result<Self, RequestTargetError> {
137        if value.is_empty() {
138            return Err(RequestTargetError::Empty);
139        }
140        if value.len() > MAX_REQUEST_TARGET_BYTES {
141            return Err(RequestTargetError::TooLong);
142        }
143        if !value.starts_with('/') || value.starts_with("//") {
144            return Err(RequestTargetError::NotOriginForm);
145        }
146        if !value
147            .bytes()
148            .all(|byte| byte.is_ascii_graphic() && byte != b'#' && byte != b'\\')
149        {
150            return Err(RequestTargetError::InvalidByte);
151        }
152        Ok(Self { value })
153    }
154
155    /// Returns the validated request target.
156    #[must_use]
157    pub const fn as_str(self) -> &'a str {
158        self.value
159    }
160}
161
162impl fmt::Debug for RequestTarget<'_> {
163    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
164        formatter.write_str("RequestTarget([redacted])")
165    }
166}
167
168/// Provider-neutral request passed to a blocking transport.
169#[derive(Clone, Copy)]
170pub struct TransportRequest<'a> {
171    method: Method,
172    target: RequestTarget<'a>,
173    body: &'a [u8],
174    content_type: Option<ContentType<'a>>,
175}
176
177impl<'a> TransportRequest<'a> {
178    /// Creates a bodyless request.
179    #[must_use]
180    pub const fn new(method: Method, target: RequestTarget<'a>) -> Self {
181        Self {
182            method,
183            target,
184            body: &[],
185            content_type: None,
186        }
187    }
188
189    /// Adds a borrowed request body.
190    #[must_use]
191    pub const fn with_body(mut self, body: &'a [u8]) -> Self {
192        self.body = body;
193        self
194    }
195
196    /// Adds an explicit content type for the borrowed request body.
197    #[must_use]
198    pub const fn with_content_type(mut self, content_type: ContentType<'a>) -> Self {
199        self.content_type = Some(content_type);
200        self
201    }
202
203    /// Returns the HTTP method.
204    #[must_use]
205    pub const fn method(self) -> Method {
206        self.method
207    }
208
209    /// Returns the validated origin-form target.
210    #[must_use]
211    pub const fn target(self) -> RequestTarget<'a> {
212        self.target
213    }
214
215    /// Returns the borrowed body bytes.
216    #[must_use]
217    pub const fn body(self) -> &'a [u8] {
218        self.body
219    }
220
221    /// Returns the explicit request-body content type, when configured.
222    #[must_use]
223    pub const fn content_type(self) -> Option<ContentType<'a>> {
224        self.content_type
225    }
226}
227
228impl fmt::Debug for TransportRequest<'_> {
229    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
230        formatter
231            .debug_struct("TransportRequest")
232            .field("method", &self.method)
233            .field("target", &self.target)
234            .field("body", &"[redacted]")
235            .field("content_type", &self.content_type)
236            .finish()
237    }
238}
239
240/// Valid HTTP response status code.
241#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
242pub struct StatusCode(u16);
243
244impl StatusCode {
245    /// `200 OK`.
246    pub const OK: Self = Self(200);
247    /// `429 Too Many Requests`.
248    pub const TOO_MANY_REQUESTS: Self = Self(429);
249
250    /// Creates a status code in the HTTP `100..=599` range.
251    #[must_use]
252    pub const fn new(value: u16) -> Option<Self> {
253        if value < 100 || value > 599 {
254            return None;
255        }
256        Some(Self(value))
257    }
258
259    /// Returns the numeric status code.
260    #[must_use]
261    pub const fn get(self) -> u16 {
262        self.0
263    }
264
265    /// Reports whether this is a success status.
266    #[must_use]
267    pub const fn is_success(self) -> bool {
268        self.0 >= 200 && self.0 <= 299
269    }
270
271    /// Reports whether this is a client or server error status.
272    #[must_use]
273    pub const fn is_error(self) -> bool {
274        self.0 >= 400
275    }
276}
277
278/// Response returned after a transport initializes part of a caller buffer.
279///
280/// The body is structurally bounded by the borrowed initialized slice. A
281/// transport cannot report a numeric length independently of the caller's
282/// response buffer.
283///
284/// ```compile_fail
285/// use cloud_sdk::transport::{StatusCode, TransportResponse};
286///
287/// let _ = TransportResponse::new(StatusCode::OK, 1024_usize);
288/// ```
289#[derive(Clone, Copy)]
290pub struct TransportResponse<'buffer> {
291    status: StatusCode,
292    body: &'buffer [u8],
293    rate_limit: Option<RateLimit>,
294}
295
296impl<'buffer> TransportResponse<'buffer> {
297    /// Creates a response over the initialized body bytes.
298    #[must_use]
299    pub const fn new(status: StatusCode, body: &'buffer [u8]) -> Self {
300        Self {
301            status,
302            body,
303            rate_limit: None,
304        }
305    }
306
307    /// Adds validated rate-limit metadata captured by the transport.
308    #[must_use]
309    pub const fn with_rate_limit(mut self, rate_limit: RateLimit) -> Self {
310        self.rate_limit = Some(rate_limit);
311        self
312    }
313
314    /// Returns the status code.
315    #[must_use]
316    pub const fn status(&self) -> StatusCode {
317        self.status
318    }
319
320    /// Returns the initialized response body bytes.
321    #[must_use]
322    pub const fn body(&self) -> &'buffer [u8] {
323        self.body
324    }
325
326    /// Returns validated rate-limit metadata when the response supplied it.
327    #[must_use]
328    pub const fn rate_limit(&self) -> Option<RateLimit> {
329        self.rate_limit
330    }
331}
332
333impl fmt::Debug for TransportResponse<'_> {
334    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
335        formatter
336            .debug_struct("TransportResponse")
337            .field("status", &self.status)
338            .field("body_len", &self.body.len())
339            .field("body", &"[redacted]")
340            .field("rate_limit", &self.rate_limit)
341            .finish()
342    }
343}
344
345/// Synchronous transport over caller-owned request and response buffers.
346///
347/// Authentication, base URLs, headers, timeouts, TLS, and retry policy belong
348/// to adapters and are intentionally outside this minimal contract.
349pub trait BlockingTransport {
350    /// Transport-specific failure.
351    type Error;
352
353    /// Sends one request and writes the response body into the caller buffer.
354    fn send<'buffer>(
355        &mut self,
356        request: TransportRequest<'_>,
357        response_body: &'buffer mut [u8],
358    ) -> Result<TransportResponse<'buffer>, Self::Error>;
359}
360
361#[cfg(test)]
362mod tests;