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