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