Skip to main content

cloud_sdk/
transport.rs

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