Skip to main content

cloud_sdk/operation/
prepared.rs

1//! Prepared operation storage, endpoint binding, and execution.
2
3use core::fmt;
4
5use crate::authentication::{
6    AsyncAuthenticatedTransport, AuthenticatedRequest, AuthenticationScopePolicy,
7    BlockingAuthenticatedTransport,
8};
9use crate::operation::{
10    CheckedResponseGuard, OperationId, OperationMetadata, RequestIdPolicy, ResponsePolicy,
11    ResponsePolicyError,
12};
13use crate::transport::{
14    BoundTransport, EndpointIdentityError, EndpointPolicy, RawResponsePolicy, ResponseBuffer,
15    TransportRequest,
16};
17use crate::{ProviderId, ProviderMarker, ServiceId, ServiceMarker};
18
19/// Caller-owned target and request-body storage supplied to preparation.
20pub struct PreparationStorage<'storage> {
21    target: &'storage mut [u8],
22    body: &'storage mut [u8],
23}
24
25impl<'storage> PreparationStorage<'storage> {
26    /// Creates complete caller-owned storage for one preparation attempt.
27    ///
28    /// # Security
29    ///
30    /// Preparation may write credentials or other secrets into `body`. A
31    /// successful [`PreparedRequest`] must retain those bytes until transport
32    /// use, so this wrapper cannot clear them on success. For secret-bearing
33    /// operations, guard `body` with a volatile-clearing type such as
34    /// `cloud_sdk_sanitization::SecretBuffer` and drop the guard immediately
35    /// after transport use. A plain mutable slice is not cleared when the
36    /// prepared request is dropped.
37    #[must_use]
38    pub const fn new(target: &'storage mut [u8], body: &'storage mut [u8]) -> Self {
39        Self { target, body }
40    }
41
42    /// Consumes the storage wrapper and returns both independent buffers.
43    #[must_use]
44    pub fn into_parts(self) -> (&'storage mut [u8], &'storage mut [u8]) {
45        (self.target, self.body)
46    }
47}
48
49impl fmt::Debug for PreparationStorage<'_> {
50    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
51        formatter
52            .debug_struct("PreparationStorage")
53            .field("target_capacity", &self.target.len())
54            .field("body_capacity", &self.body.len())
55            .finish()
56    }
57}
58
59/// Typed provider operation that can prepare one complete request.
60///
61/// ```compile_fail
62/// use cloud_sdk::operation::PrepareOperation;
63///
64/// fn prepare_without_storage<O: PrepareOperation>(operation: &O) {
65///     let _ = operation.prepare();
66/// }
67/// ```
68pub trait PrepareOperation {
69    /// Preparation-specific failure.
70    type Error;
71
72    /// Writes into caller storage and returns an executable prepared request.
73    fn prepare<'storage>(
74        &self,
75        storage: PreparationStorage<'storage>,
76    ) -> Result<PreparedRequest<'storage>, Self::Error>;
77}
78
79/// Provider service and immutable endpoint trust policy.
80#[derive(Clone, Copy, Debug, Eq, PartialEq)]
81pub struct ProviderService<'endpoint> {
82    provider_id: ProviderId,
83    service_id: ServiceId,
84    endpoint_policy: EndpointPolicy<'endpoint>,
85}
86
87impl<'endpoint> ProviderService<'endpoint> {
88    /// Binds validated provider and service IDs to an endpoint trust policy.
89    #[must_use]
90    pub const fn new(
91        provider_id: ProviderId,
92        service_id: ServiceId,
93        endpoint_policy: EndpointPolicy<'endpoint>,
94    ) -> Self {
95        Self {
96            provider_id,
97            service_id,
98            endpoint_policy,
99        }
100    }
101
102    /// Binds a provider-owned service marker to an endpoint trust policy.
103    #[must_use]
104    pub const fn from_marker<S: ServiceMarker>(endpoint_policy: EndpointPolicy<'endpoint>) -> Self {
105        Self::new(<S::Provider as ProviderMarker>::ID, S::ID, endpoint_policy)
106    }
107
108    /// Returns the canonical provider namespace.
109    #[must_use]
110    pub const fn provider_id(self) -> ProviderId {
111        self.provider_id
112    }
113
114    /// Returns the canonical provider-owned service namespace.
115    #[must_use]
116    pub const fn service_id(self) -> ServiceId {
117        self.service_id
118    }
119
120    /// Returns the immutable endpoint trust policy.
121    #[must_use]
122    pub const fn endpoint_policy(self) -> EndpointPolicy<'endpoint> {
123        self.endpoint_policy
124    }
125}
126
127/// Complete request, endpoint, operation metadata, and response policy.
128#[derive(Clone, Copy)]
129pub struct PreparedRequest<'request> {
130    request: TransportRequest<'request>,
131    service: ProviderService<'request>,
132    metadata: OperationMetadata,
133    response_policy: ResponsePolicy,
134    authentication_policy: AuthenticationScopePolicy<'request>,
135    raw_response_policy: RawResponsePolicy<'request>,
136    operation_id: Option<OperationId>,
137}
138
139/// Incoherent policy supplied while constructing a prepared request.
140#[derive(Clone, Copy, Debug, Eq, PartialEq)]
141pub enum PreparedRequestPolicyError {
142    /// Protected or retainable request IDs were not admitted by raw transport.
143    MissingRequestIdHeader,
144}
145
146impl fmt::Display for PreparedRequestPolicyError {
147    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
148        formatter.write_str(match self {
149            Self::MissingRequestIdHeader => {
150                "prepared request ID policy requires raw x-request-id admission"
151            }
152        })
153    }
154}
155
156impl core::error::Error for PreparedRequestPolicyError {}
157
158impl<'request> PreparedRequest<'request> {
159    /// Creates a complete prepared request after checking cross-policy invariants.
160    ///
161    /// # Errors
162    ///
163    /// Returns [`PreparedRequestPolicyError::MissingRequestIdHeader`] when
164    /// operation metadata protects or retains request IDs but the raw response
165    /// policy does not admit `x-request-id`.
166    pub fn new(
167        request: TransportRequest<'request>,
168        service: ProviderService<'request>,
169        metadata: OperationMetadata,
170        response_policy: ResponsePolicy,
171        authentication_policy: AuthenticationScopePolicy<'request>,
172        raw_response_policy: RawResponsePolicy<'request>,
173    ) -> Result<Self, PreparedRequestPolicyError> {
174        if metadata.request_id_policy() != RequestIdPolicy::Discard
175            && !raw_response_policy.admits_header("x-request-id")
176        {
177            return Err(PreparedRequestPolicyError::MissingRequestIdHeader);
178        }
179        Ok(Self {
180            request,
181            service,
182            metadata,
183            response_policy,
184            authentication_policy,
185            raw_response_policy,
186            operation_id: None,
187        })
188    }
189
190    /// Binds a validated provider operation identifier to this request.
191    #[must_use]
192    pub const fn with_operation_id(mut self, operation_id: OperationId) -> Self {
193        self.operation_id = Some(operation_id);
194        self
195    }
196
197    /// Returns the validated transport request.
198    #[must_use]
199    pub const fn transport_request(self) -> TransportRequest<'request> {
200        self.request
201    }
202
203    /// Returns the bound provider service.
204    #[must_use]
205    pub const fn service(self) -> ProviderService<'request> {
206        self.service
207    }
208
209    /// Returns complete safety and retry metadata.
210    #[must_use]
211    pub const fn metadata(self) -> OperationMetadata {
212        self.metadata
213    }
214
215    /// Returns complete checked-response policy.
216    #[must_use]
217    pub const fn response_policy(self) -> ResponsePolicy {
218        self.response_policy
219    }
220
221    /// Returns the complete provider-owned authentication-scope policy.
222    #[must_use]
223    pub const fn authentication_policy(self) -> AuthenticationScopePolicy<'request> {
224        self.authentication_policy
225    }
226
227    /// Returns the complete status-class raw response policy.
228    #[must_use]
229    pub const fn raw_response_policy(self) -> RawResponsePolicy<'request> {
230        self.raw_response_policy
231    }
232
233    /// Returns the request with its mandatory authentication and raw wire policy.
234    #[must_use]
235    pub const fn authenticated_request(self) -> AuthenticatedRequest<'request, 'request> {
236        AuthenticatedRequest::new(
237            self.request,
238            self.authentication_policy,
239            self.raw_response_policy,
240        )
241    }
242
243    /// Returns the provider operation identifier when one was bound.
244    #[must_use]
245    pub const fn operation_id(self) -> Option<OperationId> {
246        self.operation_id
247    }
248
249    /// Applies the complete prepared response policy without executing transport.
250    pub fn validate_response<'buffer>(
251        self,
252        response: ResponseBuffer<'buffer>,
253    ) -> Result<CheckedResponseGuard<'buffer>, ResponsePolicyError> {
254        self.response_policy
255            .validate(response, self.metadata.request_id_policy())
256    }
257
258    /// Applies operation-owned metadata policy before provider error decoding.
259    ///
260    /// This is the error-status counterpart to [`Self::validate_response`].
261    /// It extracts and protects, discards, or admits retention of the provider
262    /// request identifier without applying success-status or body policy.
263    pub fn apply_response_metadata_policy(
264        self,
265        response: &mut ResponseBuffer<'_>,
266    ) -> Result<(), ResponsePolicyError> {
267        super::policy::apply_request_id_policy(response, self.metadata.request_id_policy())
268    }
269
270    /// Verifies endpoint identity, executes once, and validates the response.
271    pub fn execute_blocking<'buffer, T>(
272        self,
273        transport: &T,
274        response_storage: &'buffer mut [u8],
275        response_header_storage: &'buffer mut [u8],
276    ) -> Result<CheckedResponseGuard<'buffer>, PreparedExecutionError<T::Error>>
277    where
278        T: BlockingAuthenticatedTransport + BoundTransport,
279    {
280        let mut response = ResponseBuffer::new(
281            response_storage,
282            self.raw_response_policy.max_body_bytes(),
283            response_header_storage,
284        );
285        self.verify_endpoint(transport)
286            .map_err(map_endpoint_error)?;
287        transport
288            .send_authenticated(self.authenticated_request(), response.writer())
289            .map_err(PreparedExecutionError::Transport)?;
290        self.response_policy
291            .validate(response, self.metadata.request_id_policy())
292            .map_err(PreparedExecutionError::ResponsePolicy)
293    }
294
295    /// Async equivalent of [`Self::execute_blocking`] without owning an executor.
296    pub async fn execute_async<'transport, 'buffer, T>(
297        &'transport self,
298        transport: &'transport T,
299        response_storage: &'buffer mut [u8],
300        response_header_storage: &'buffer mut [u8],
301    ) -> Result<CheckedResponseGuard<'buffer>, PreparedExecutionError<T::Error>>
302    where
303        T: AsyncAuthenticatedTransport + BoundTransport,
304        'request: 'transport,
305    {
306        let mut response = ResponseBuffer::new(
307            response_storage,
308            self.raw_response_policy.max_body_bytes(),
309            response_header_storage,
310        );
311        self.verify_endpoint(transport)
312            .map_err(map_endpoint_error)?;
313        transport
314            .send_authenticated(self.authenticated_request(), response.writer())
315            .await
316            .map_err(PreparedExecutionError::Transport)?;
317        self.response_policy
318            .validate(response, self.metadata.request_id_policy())
319            .map_err(PreparedExecutionError::ResponsePolicy)
320    }
321
322    fn verify_endpoint<T>(self, transport: &T) -> Result<(), EndpointCheckError>
323    where
324        T: BoundTransport,
325    {
326        let actual = transport
327            .endpoint_identity()
328            .map_err(EndpointCheckError::Invalid)?;
329        self.service
330            .endpoint_policy
331            .verify(actual)
332            .map_err(|_| EndpointCheckError::Mismatch)
333    }
334}
335
336impl fmt::Debug for PreparedRequest<'_> {
337    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
338        formatter
339            .debug_struct("PreparedRequest")
340            .field("request", &self.request)
341            .field("service", &self.service)
342            .field("metadata", &self.metadata)
343            .field("response_policy", &self.response_policy)
344            .field("authentication_policy", &self.authentication_policy)
345            .field("raw_response_policy", &self.raw_response_policy)
346            .field("operation_id", &self.operation_id)
347            .finish()
348    }
349}
350
351/// Prepared execution failure with transport details redacted from diagnostics.
352#[derive(Clone, Copy, Eq, PartialEq)]
353pub enum PreparedExecutionError<E> {
354    /// The bound transport returned invalid endpoint identity.
355    EndpointIdentity(EndpointIdentityError),
356    /// The bound endpoint differs from the prepared provider service.
357    EndpointMismatch,
358    /// The concrete transport failed.
359    Transport(E),
360    /// The response failed provider-neutral policy.
361    ResponsePolicy(ResponsePolicyError),
362}
363
364impl<E> fmt::Debug for PreparedExecutionError<E> {
365    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
366        match self {
367            Self::EndpointIdentity(error) => formatter
368                .debug_tuple("EndpointIdentity")
369                .field(error)
370                .finish(),
371            Self::EndpointMismatch => formatter.write_str("EndpointMismatch"),
372            Self::Transport(_) => formatter.write_str("Transport([redacted])"),
373            Self::ResponsePolicy(error) => formatter
374                .debug_tuple("ResponsePolicy")
375                .field(error)
376                .finish(),
377        }
378    }
379}
380
381impl<E> fmt::Display for PreparedExecutionError<E> {
382    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
383        formatter.write_str(match self {
384            Self::EndpointIdentity(_) => "transport endpoint identity is invalid",
385            Self::EndpointMismatch => "transport endpoint differs from prepared service",
386            Self::Transport(_) => "prepared request transport failed",
387            Self::ResponsePolicy(_) => "prepared response policy failed",
388        })
389    }
390}
391
392impl<E: fmt::Debug> core::error::Error for PreparedExecutionError<E> {}
393
394enum EndpointCheckError {
395    Invalid(EndpointIdentityError),
396    Mismatch,
397}
398
399fn map_endpoint_error<E>(error: EndpointCheckError) -> PreparedExecutionError<E> {
400    match error {
401        EndpointCheckError::Invalid(error) => PreparedExecutionError::EndpointIdentity(error),
402        EndpointCheckError::Mismatch => PreparedExecutionError::EndpointMismatch,
403    }
404}