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