Skip to main content

cloud_sdk/operation/
prepared.rs

1//! Prepared operation storage, endpoint binding, and execution.
2
3use core::fmt;
4
5use cloud_sdk_sanitization::sanitize_bytes;
6
7use crate::authentication::{
8    AsyncAuthenticatedTransport, AuthenticatedRequest, AuthenticationScopePolicy,
9    BlockingAuthenticatedTransport, drive_async_authenticated,
10};
11use crate::operation::{
12    CheckedResponseGuard, OperationId, OperationImpact, OperationMetadata, RequestIdPolicy,
13    ResponsePolicy, ResponsePolicyError,
14};
15use crate::transport::{
16    BoundTransport, EndpointIdentity, RawResponsePolicy, RequestHeaders, ResponseBuffer,
17    TransportRequest,
18};
19
20mod body;
21mod error;
22mod service;
23mod storage;
24pub use body::{BodyReplayability, RequestBodySensitivity};
25use error::{EndpointCheckError, map_endpoint_error};
26pub use error::{PreparedExecutionError, PreparedRequestPolicyError};
27pub use service::ProviderService;
28pub use storage::{PreparationStorage, PrepareOperation};
29
30/// Complete request, endpoint, operation metadata, and response policy.
31#[derive(Clone, Copy)]
32pub struct PreparedRequest<'request> {
33    request: TransportRequest<'request>,
34    service: ProviderService<'request>,
35    metadata: OperationMetadata,
36    response_policy: ResponsePolicy,
37    authentication_policy: AuthenticationScopePolicy<'request>,
38    raw_response_policy: RawResponsePolicy<'request>,
39    operation_id: Option<OperationId>,
40    body_replayability: BodyReplayability,
41    body_sensitivity: RequestBodySensitivity,
42}
43
44impl<'request> PreparedRequest<'request> {
45    /// Creates a complete prepared request after checking cross-policy invariants.
46    ///
47    /// # Errors
48    ///
49    /// Returns [`PreparedRequestPolicyError::MissingRequestIdHeader`] when
50    /// operation metadata protects or retains request IDs but the raw response
51    /// policy does not admit `x-request-id`. Returns
52    /// [`PreparedRequestPolicyError::ReadOnlyMethodMismatch`] when read-only
53    /// metadata is paired with any method other than `GET` or `HEAD`.
54    /// `body_sensitivity` is mandatory so provider implementations cannot omit
55    /// the confidential-body review and silently receive a public default.
56    pub fn new(
57        request: TransportRequest<'request>,
58        service: ProviderService<'request>,
59        metadata: OperationMetadata,
60        response_policy: ResponsePolicy,
61        authentication_policy: AuthenticationScopePolicy<'request>,
62        raw_response_policy: RawResponsePolicy<'request>,
63        body_sensitivity: RequestBodySensitivity,
64    ) -> Result<Self, PreparedRequestPolicyError> {
65        if matches!(metadata.impact(), OperationImpact::ReadOnly)
66            && !request.method().permits_direct_read_only()
67        {
68            return Err(PreparedRequestPolicyError::ReadOnlyMethodMismatch);
69        }
70        if metadata.request_id_policy() != RequestIdPolicy::Discard
71            && !raw_response_policy.admits_header("x-request-id")
72        {
73            return Err(PreparedRequestPolicyError::MissingRequestIdHeader);
74        }
75        Ok(Self {
76            request,
77            service,
78            metadata,
79            response_policy,
80            authentication_policy,
81            raw_response_policy,
82            operation_id: None,
83            body_replayability: if request.body().is_empty() {
84                BodyReplayability::Replayable
85            } else {
86                BodyReplayability::NotReplayable
87            },
88            body_sensitivity,
89        })
90    }
91
92    /// Binds a validated provider operation identifier to this request.
93    #[must_use]
94    pub const fn with_operation_id(mut self, operation_id: OperationId) -> Self {
95        self.operation_id = Some(operation_id);
96        self
97    }
98
99    /// Marks the immutable prepared body snapshot as byte-for-byte replayable.
100    ///
101    /// Providers must call this only after preparation has completed and when
102    /// the borrowed body bytes cannot change for the prepared request lifetime.
103    #[must_use]
104    pub const fn with_replayable_body(mut self) -> Self {
105        self.body_replayability = BodyReplayability::Replayable;
106        self
107    }
108
109    /// Upgrades the explicit body classification to sensitive.
110    ///
111    /// This cannot downgrade a sensitive body. Providers should classify the
112    /// body at construction; this helper supports reviewed wrapper policies.
113    #[must_use]
114    pub const fn with_sensitive_body(mut self) -> Self {
115        self.body_sensitivity = RequestBodySensitivity::Sensitive;
116        self
117    }
118
119    /// Returns the validated transport request.
120    #[must_use]
121    pub const fn transport_request(self) -> TransportRequest<'request> {
122        self.request
123    }
124
125    /// Returns the bound provider service.
126    #[must_use]
127    pub const fn service(self) -> ProviderService<'request> {
128        self.service
129    }
130
131    /// Returns complete safety and retry metadata.
132    #[must_use]
133    pub const fn metadata(self) -> OperationMetadata {
134        self.metadata
135    }
136
137    /// Returns complete checked-response policy.
138    #[must_use]
139    pub const fn response_policy(self) -> ResponsePolicy {
140        self.response_policy
141    }
142
143    /// Returns the complete provider-owned authentication-scope policy.
144    #[must_use]
145    pub const fn authentication_policy(self) -> AuthenticationScopePolicy<'request> {
146        self.authentication_policy
147    }
148
149    /// Returns the complete status-class raw response policy.
150    #[must_use]
151    pub const fn raw_response_policy(self) -> RawResponsePolicy<'request> {
152        self.raw_response_policy
153    }
154
155    /// Returns the request with its mandatory authentication and raw wire policy.
156    #[must_use]
157    pub(crate) const fn authenticated_request(self) -> AuthenticatedRequest<'request, 'request> {
158        AuthenticatedRequest::new(
159            self.request,
160            self.authentication_policy,
161            &self.raw_response_policy,
162        )
163    }
164
165    /// Returns the provider operation identifier when one was bound.
166    #[must_use]
167    pub const fn operation_id(self) -> Option<OperationId> {
168        self.operation_id
169    }
170
171    /// Returns the explicit request-body replay capability.
172    #[must_use]
173    pub const fn body_replayability(self) -> BodyReplayability {
174        self.body_replayability
175    }
176
177    /// Returns the provider-declared request-body sensitivity.
178    #[must_use]
179    pub const fn body_sensitivity(self) -> RequestBodySensitivity {
180        self.body_sensitivity
181    }
182
183    pub(crate) fn with_request_headers<'headers>(
184        self,
185        headers: RequestHeaders<'headers>,
186    ) -> PreparedRequest<'headers>
187    where
188        'request: 'headers,
189    {
190        let request: TransportRequest<'headers> = self.request;
191        PreparedRequest {
192            request: request.with_headers(headers),
193            service: self.service,
194            metadata: self.metadata,
195            response_policy: self.response_policy,
196            authentication_policy: self.authentication_policy,
197            raw_response_policy: self.raw_response_policy,
198            operation_id: self.operation_id,
199            body_replayability: self.body_replayability,
200            body_sensitivity: self.body_sensitivity,
201        }
202    }
203
204    pub(crate) fn has_same_retry_policy(&self, other: &Self) -> bool {
205        self.service == other.service
206            && self.metadata == other.metadata
207            && self.response_policy == other.response_policy
208            && self.authentication_policy == other.authentication_policy
209            && self.raw_response_policy == other.raw_response_policy
210            && self.operation_id == other.operation_id
211            && self.body_replayability == other.body_replayability
212            && self.body_sensitivity == other.body_sensitivity
213            && self.has_same_header_policy(other)
214    }
215
216    fn has_same_header_policy(&self, other: &Self) -> bool {
217        let left = self.request.headers().as_slice();
218        let right = other.request.headers().as_slice();
219        left.len() == right.len()
220            && left
221                .iter()
222                .zip(right)
223                .all(|(left, right)| left.sensitivity() == right.sensitivity())
224    }
225
226    /// Applies the complete prepared response policy without executing transport.
227    pub fn validate_response<'buffer>(
228        self,
229        response: ResponseBuffer<'buffer>,
230    ) -> Result<CheckedResponseGuard<'buffer>, ResponsePolicyError> {
231        self.response_policy
232            .validate(response, self.metadata.request_id_policy())
233    }
234
235    /// Applies operation-owned metadata policy before provider error decoding.
236    ///
237    /// This is the error-status counterpart to [`Self::validate_response`].
238    /// It extracts and protects, discards, or admits retention of the provider
239    /// request identifier without applying success-status or body policy.
240    pub fn apply_response_metadata_policy(
241        self,
242        response: &mut ResponseBuffer<'_>,
243    ) -> Result<(), ResponsePolicyError> {
244        super::policy::apply_request_id_policy(response, self.metadata.request_id_policy())
245    }
246
247    /// Verifies endpoint identity, executes once, and validates the response.
248    pub fn execute_blocking<'buffer, T>(
249        self,
250        transport: &T,
251        response_storage: &'buffer mut [u8],
252        response_header_storage: &'buffer mut [u8],
253    ) -> Result<CheckedResponseGuard<'buffer>, PreparedExecutionError<T::Error>>
254    where
255        T: BlockingAuthenticatedTransport + BoundTransport,
256    {
257        let response = self.send_blocking(transport, response_storage, response_header_storage)?;
258        self.response_policy
259            .validate(response, self.metadata.request_id_policy())
260            .map_err(PreparedExecutionError::ResponsePolicy)
261    }
262
263    pub(crate) fn send_blocking<'buffer, T>(
264        self,
265        transport: &T,
266        response_storage: &'buffer mut [u8],
267        response_header_storage: &'buffer mut [u8],
268    ) -> Result<ResponseBuffer<'buffer>, PreparedExecutionError<T::Error>>
269    where
270        T: BlockingAuthenticatedTransport + BoundTransport,
271    {
272        if self.requires_execution_permit() {
273            sanitize_bytes(response_storage);
274            sanitize_bytes(response_header_storage);
275            return Err(PreparedExecutionError::AuthorizationRequired);
276        }
277        self.send_blocking_authorized(transport, None, response_storage, response_header_storage)
278    }
279
280    pub(crate) fn execute_blocking_authorized<'buffer, T>(
281        self,
282        transport: &T,
283        confirmed_endpoint: Option<EndpointIdentity<'_>>,
284        response_storage: &'buffer mut [u8],
285        response_header_storage: &'buffer mut [u8],
286    ) -> Result<CheckedResponseGuard<'buffer>, PreparedExecutionError<T::Error>>
287    where
288        T: BlockingAuthenticatedTransport + BoundTransport,
289    {
290        let response = self.send_blocking_authorized(
291            transport,
292            confirmed_endpoint,
293            response_storage,
294            response_header_storage,
295        )?;
296        self.response_policy
297            .validate(response, self.metadata.request_id_policy())
298            .map_err(PreparedExecutionError::ResponsePolicy)
299    }
300
301    pub(crate) fn send_blocking_authorized<'buffer, T>(
302        self,
303        transport: &T,
304        confirmed_endpoint: Option<EndpointIdentity<'_>>,
305        response_storage: &'buffer mut [u8],
306        response_header_storage: &'buffer mut [u8],
307    ) -> Result<ResponseBuffer<'buffer>, PreparedExecutionError<T::Error>>
308    where
309        T: BlockingAuthenticatedTransport + BoundTransport,
310    {
311        let mut response = ResponseBuffer::new(
312            response_storage,
313            self.raw_response_policy.max_body_bytes(),
314            response_header_storage,
315        );
316        self.verify_endpoint(transport, confirmed_endpoint)
317            .map_err(map_endpoint_error)?;
318        transport
319            .send_authenticated(self.authenticated_request(), response.writer())
320            .map_err(PreparedExecutionError::Transport)?;
321        Ok(response)
322    }
323
324    /// Async equivalent of [`Self::execute_blocking`] without owning an executor.
325    pub async fn execute_async<'transport, 'buffer, T>(
326        &'transport self,
327        transport: &'transport T,
328        response_storage: &'buffer mut [u8],
329        response_header_storage: &'buffer mut [u8],
330    ) -> Result<CheckedResponseGuard<'buffer>, PreparedExecutionError<T::Error>>
331    where
332        T: AsyncAuthenticatedTransport + BoundTransport,
333        'request: 'transport,
334    {
335        let response = self
336            .send_async(transport, response_storage, response_header_storage)
337            .await?;
338        self.response_policy
339            .validate(response, self.metadata.request_id_policy())
340            .map_err(PreparedExecutionError::ResponsePolicy)
341    }
342
343    pub(crate) async fn send_async<'transport, 'buffer, T>(
344        &'transport self,
345        transport: &'transport T,
346        response_storage: &'buffer mut [u8],
347        response_header_storage: &'buffer mut [u8],
348    ) -> Result<ResponseBuffer<'buffer>, PreparedExecutionError<T::Error>>
349    where
350        T: AsyncAuthenticatedTransport + BoundTransport,
351        'request: 'transport,
352    {
353        if self.requires_execution_permit() {
354            sanitize_bytes(response_storage);
355            sanitize_bytes(response_header_storage);
356            return Err(PreparedExecutionError::AuthorizationRequired);
357        }
358        self.send_async_authorized(transport, None, response_storage, response_header_storage)
359            .await
360    }
361
362    pub(crate) async fn execute_async_authorized<'transport, 'buffer, T>(
363        &'transport self,
364        transport: &'transport T,
365        confirmed_endpoint: Option<EndpointIdentity<'_>>,
366        response_storage: &'buffer mut [u8],
367        response_header_storage: &'buffer mut [u8],
368    ) -> Result<CheckedResponseGuard<'buffer>, PreparedExecutionError<T::Error>>
369    where
370        T: AsyncAuthenticatedTransport + BoundTransport,
371        'request: 'transport,
372    {
373        let response = self
374            .send_async_authorized(
375                transport,
376                confirmed_endpoint,
377                response_storage,
378                response_header_storage,
379            )
380            .await?;
381        self.response_policy
382            .validate(response, self.metadata.request_id_policy())
383            .map_err(PreparedExecutionError::ResponsePolicy)
384    }
385
386    pub(crate) async fn send_async_authorized<'transport, 'buffer, T>(
387        &'transport self,
388        transport: &'transport T,
389        confirmed_endpoint: Option<EndpointIdentity<'_>>,
390        response_storage: &'buffer mut [u8],
391        response_header_storage: &'buffer mut [u8],
392    ) -> Result<ResponseBuffer<'buffer>, PreparedExecutionError<T::Error>>
393    where
394        T: AsyncAuthenticatedTransport + BoundTransport,
395        'request: 'transport,
396    {
397        let mut response = ResponseBuffer::new(
398            response_storage,
399            self.raw_response_policy.max_body_bytes(),
400            response_header_storage,
401        );
402        self.verify_endpoint(transport, confirmed_endpoint)
403            .map_err(map_endpoint_error)?;
404        drive_async_authenticated(transport, self.authenticated_request(), response.writer())
405            .await
406            .map_err(|error| match error {
407                crate::transport::AsyncExecutionError::Transport(error) => {
408                    PreparedExecutionError::Transport(error)
409                }
410                crate::transport::AsyncExecutionError::Response(error) => {
411                    PreparedExecutionError::ResponseWriter(error)
412                }
413            })?;
414        Ok(response)
415    }
416
417    pub(crate) const fn requires_execution_permit(self) -> bool {
418        !self.request.method().permits_direct_read_only()
419            || !matches!(self.metadata.impact(), OperationImpact::ReadOnly)
420            || matches!(self.metadata.cost_intent(), super::CostIntent::MayIncurCost)
421    }
422
423    fn verify_endpoint<T>(
424        self,
425        transport: &T,
426        confirmed_endpoint: Option<EndpointIdentity<'_>>,
427    ) -> Result<(), EndpointCheckError>
428    where
429        T: BoundTransport,
430    {
431        let actual = transport
432            .endpoint_identity()
433            .map_err(EndpointCheckError::Invalid)?;
434        match confirmed_endpoint {
435            Some(expected) if actual == expected => Ok(()),
436            Some(_) => Err(EndpointCheckError::Mismatch),
437            None => self
438                .service
439                .endpoint_policy()
440                .verify(actual)
441                .map_err(|_| EndpointCheckError::Mismatch),
442        }
443    }
444}
445
446impl fmt::Debug for PreparedRequest<'_> {
447    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
448        formatter
449            .debug_struct("PreparedRequest")
450            .field("request", &self.request)
451            .field("service", &self.service)
452            .field("metadata", &self.metadata)
453            .field("response_policy", &self.response_policy)
454            .field("authentication_policy", &self.authentication_policy)
455            .field("raw_response_policy", &self.raw_response_policy)
456            .field("operation_id", &self.operation_id)
457            .field("body_replayability", &self.body_replayability)
458            .field("body_sensitivity", &self.body_sensitivity)
459            .finish()
460    }
461}