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