Skip to main content

cloud_sdk/
transport.rs

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