Skip to main content

cloud_sdk/operation/
prepared.rs

1//! Prepared operation storage, endpoint binding, and execution.
2
3use core::fmt;
4
5use crate::operation::{
6    CheckedResponse, OperationId, OperationMetadata, ResponsePolicy, ResponsePolicyError,
7};
8use crate::transport::{
9    AsyncTransport, BlockingTransport, BoundTransport, EndpointIdentityError, EndpointPolicy,
10    ResponseStorageSanitizer, TransportRequest,
11};
12use crate::{ProviderId, ProviderMarker, ServiceId, ServiceMarker};
13
14/// Caller-owned target and request-body storage supplied to preparation.
15pub struct PreparationStorage<'storage> {
16    target: &'storage mut [u8],
17    body: &'storage mut [u8],
18}
19
20impl<'storage> PreparationStorage<'storage> {
21    /// Creates complete caller-owned storage for one preparation attempt.
22    ///
23    /// # Security
24    ///
25    /// Preparation may write credentials or other secrets into `body`. A
26    /// successful [`PreparedRequest`] must retain those bytes until transport
27    /// use, so this wrapper cannot clear them on success. For secret-bearing
28    /// operations, guard `body` with a volatile-clearing type such as
29    /// `cloud_sdk_sanitization::SecretBuffer` and drop the guard immediately
30    /// after transport use. A plain mutable slice is not cleared when the
31    /// prepared request is dropped.
32    #[must_use]
33    pub const fn new(target: &'storage mut [u8], body: &'storage mut [u8]) -> Self {
34        Self { target, body }
35    }
36
37    /// Consumes the storage wrapper and returns both independent buffers.
38    #[must_use]
39    pub fn into_parts(self) -> (&'storage mut [u8], &'storage mut [u8]) {
40        (self.target, self.body)
41    }
42}
43
44impl fmt::Debug for PreparationStorage<'_> {
45    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
46        formatter
47            .debug_struct("PreparationStorage")
48            .field("target_capacity", &self.target.len())
49            .field("body_capacity", &self.body.len())
50            .finish()
51    }
52}
53
54/// Typed provider operation that can prepare one complete request.
55///
56/// ```compile_fail
57/// use cloud_sdk::operation::PrepareOperation;
58///
59/// fn prepare_without_storage<O: PrepareOperation>(operation: &O) {
60///     let _ = operation.prepare();
61/// }
62/// ```
63pub trait PrepareOperation {
64    /// Preparation-specific failure.
65    type Error;
66
67    /// Writes into caller storage and returns an executable prepared request.
68    fn prepare<'storage>(
69        &self,
70        storage: PreparationStorage<'storage>,
71    ) -> Result<PreparedRequest<'storage>, Self::Error>;
72}
73
74/// Provider service and immutable endpoint trust policy.
75#[derive(Clone, Copy, Debug, Eq, PartialEq)]
76pub struct ProviderService<'endpoint> {
77    provider_id: ProviderId,
78    service_id: ServiceId,
79    endpoint_policy: EndpointPolicy<'endpoint>,
80}
81
82impl<'endpoint> ProviderService<'endpoint> {
83    /// Binds validated provider and service IDs to an endpoint trust policy.
84    #[must_use]
85    pub const fn new(
86        provider_id: ProviderId,
87        service_id: ServiceId,
88        endpoint_policy: EndpointPolicy<'endpoint>,
89    ) -> Self {
90        Self {
91            provider_id,
92            service_id,
93            endpoint_policy,
94        }
95    }
96
97    /// Binds a provider-owned service marker to an endpoint trust policy.
98    #[must_use]
99    pub const fn from_marker<S: ServiceMarker>(endpoint_policy: EndpointPolicy<'endpoint>) -> Self {
100        Self::new(<S::Provider as ProviderMarker>::ID, S::ID, endpoint_policy)
101    }
102
103    /// Returns the canonical provider namespace.
104    #[must_use]
105    pub const fn provider_id(self) -> ProviderId {
106        self.provider_id
107    }
108
109    /// Returns the canonical provider-owned service namespace.
110    #[must_use]
111    pub const fn service_id(self) -> ServiceId {
112        self.service_id
113    }
114
115    /// Returns the immutable endpoint trust policy.
116    #[must_use]
117    pub const fn endpoint_policy(self) -> EndpointPolicy<'endpoint> {
118        self.endpoint_policy
119    }
120}
121
122/// Complete request, endpoint, operation metadata, and response policy.
123#[derive(Clone, Copy)]
124pub struct PreparedRequest<'request> {
125    request: TransportRequest<'request>,
126    service: ProviderService<'request>,
127    metadata: OperationMetadata,
128    response_policy: ResponsePolicy,
129    operation_id: Option<OperationId>,
130}
131
132impl<'request> PreparedRequest<'request> {
133    /// Creates a complete prepared request without policy defaults.
134    #[must_use]
135    pub const fn new(
136        request: TransportRequest<'request>,
137        service: ProviderService<'request>,
138        metadata: OperationMetadata,
139        response_policy: ResponsePolicy,
140    ) -> Self {
141        Self {
142            request,
143            service,
144            metadata,
145            response_policy,
146            operation_id: None,
147        }
148    }
149
150    /// Binds a validated provider operation identifier to this request.
151    #[must_use]
152    pub const fn with_operation_id(mut self, operation_id: OperationId) -> Self {
153        self.operation_id = Some(operation_id);
154        self
155    }
156
157    /// Returns the validated transport request.
158    #[must_use]
159    pub const fn transport_request(self) -> TransportRequest<'request> {
160        self.request
161    }
162
163    /// Returns the bound provider service.
164    #[must_use]
165    pub const fn service(self) -> ProviderService<'request> {
166        self.service
167    }
168
169    /// Returns complete safety and retry metadata.
170    #[must_use]
171    pub const fn metadata(self) -> OperationMetadata {
172        self.metadata
173    }
174
175    /// Returns complete checked-response policy.
176    #[must_use]
177    pub const fn response_policy(self) -> ResponsePolicy {
178        self.response_policy
179    }
180
181    /// Returns the provider operation identifier when one was bound.
182    #[must_use]
183    pub const fn operation_id(self) -> Option<OperationId> {
184        self.operation_id
185    }
186
187    /// Applies the complete prepared response policy without executing transport.
188    pub fn validate_response<'buffer>(
189        self,
190        response: crate::transport::TransportResponse<'buffer>,
191    ) -> Result<CheckedResponse<'buffer>, ResponsePolicyError> {
192        self.response_policy.validate(response)
193    }
194
195    /// Verifies endpoint identity, executes once, and validates the response.
196    pub fn execute_blocking<'buffer, T>(
197        self,
198        transport: &T,
199        response_storage: &'buffer mut [u8],
200    ) -> Result<CheckedResponse<'buffer>, PreparedExecutionError<T::Error>>
201    where
202        T: BlockingTransport + BoundTransport + ResponseStorageSanitizer,
203    {
204        transport.sanitize_response_storage(response_storage);
205        self.verify_endpoint(transport)
206            .map_err(map_endpoint_error)?;
207        let admitted = self.admit_response_storage(response_storage)?;
208        let response = transport
209            .send(self.request, admitted)
210            .map_err(PreparedExecutionError::Transport)?;
211        self.response_policy
212            .validate(response)
213            .map_err(PreparedExecutionError::ResponsePolicy)
214    }
215
216    /// Async equivalent of [`Self::execute_blocking`] without owning an executor.
217    pub async fn execute_async<'transport, 'buffer, T>(
218        &'transport self,
219        transport: &'transport T,
220        response_storage: &'buffer mut [u8],
221    ) -> Result<CheckedResponse<'buffer>, PreparedExecutionError<T::Error>>
222    where
223        T: AsyncTransport + BoundTransport + ResponseStorageSanitizer,
224        'request: 'transport,
225        'buffer: 'transport,
226    {
227        transport.sanitize_response_storage(response_storage);
228        self.verify_endpoint(transport)
229            .map_err(map_endpoint_error)?;
230        let admitted = self.admit_response_storage(response_storage)?;
231        let response = transport
232            .send(self.request, admitted)
233            .await
234            .map_err(PreparedExecutionError::Transport)?;
235        self.response_policy
236            .validate(response)
237            .map_err(PreparedExecutionError::ResponsePolicy)
238    }
239
240    fn verify_endpoint<T>(self, transport: &T) -> Result<(), EndpointCheckError>
241    where
242        T: BoundTransport,
243    {
244        let actual = transport
245            .endpoint_identity()
246            .map_err(EndpointCheckError::Invalid)?;
247        self.service
248            .endpoint_policy
249            .verify(actual)
250            .map_err(|_| EndpointCheckError::Mismatch)
251    }
252
253    fn admit_response_storage<E>(
254        self,
255        storage: &mut [u8],
256    ) -> Result<&mut [u8], PreparedExecutionError<E>> {
257        let admitted_len = core::cmp::min(storage.len(), self.response_policy.max_body_bytes());
258        storage
259            .get_mut(..admitted_len)
260            .ok_or(PreparedExecutionError::ResponseStorageUnavailable)
261    }
262}
263
264impl fmt::Debug for PreparedRequest<'_> {
265    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
266        formatter
267            .debug_struct("PreparedRequest")
268            .field("request", &self.request)
269            .field("service", &self.service)
270            .field("metadata", &self.metadata)
271            .field("response_policy", &self.response_policy)
272            .field("operation_id", &self.operation_id)
273            .finish()
274    }
275}
276
277/// Prepared execution failure with transport details redacted from diagnostics.
278#[derive(Clone, Copy, Eq, PartialEq)]
279pub enum PreparedExecutionError<E> {
280    /// The bound transport returned invalid endpoint identity.
281    EndpointIdentity(EndpointIdentityError),
282    /// The bound endpoint differs from the prepared provider service.
283    EndpointMismatch,
284    /// Response storage could not be structurally limited.
285    ResponseStorageUnavailable,
286    /// The concrete transport failed.
287    Transport(E),
288    /// The response failed provider-neutral policy.
289    ResponsePolicy(ResponsePolicyError),
290}
291
292impl<E> fmt::Debug for PreparedExecutionError<E> {
293    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
294        match self {
295            Self::EndpointIdentity(error) => formatter
296                .debug_tuple("EndpointIdentity")
297                .field(error)
298                .finish(),
299            Self::EndpointMismatch => formatter.write_str("EndpointMismatch"),
300            Self::ResponseStorageUnavailable => formatter.write_str("ResponseStorageUnavailable"),
301            Self::Transport(_) => formatter.write_str("Transport([redacted])"),
302            Self::ResponsePolicy(error) => formatter
303                .debug_tuple("ResponsePolicy")
304                .field(error)
305                .finish(),
306        }
307    }
308}
309
310impl<E> fmt::Display for PreparedExecutionError<E> {
311    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
312        formatter.write_str(match self {
313            Self::EndpointIdentity(_) => "transport endpoint identity is invalid",
314            Self::EndpointMismatch => "transport endpoint differs from prepared service",
315            Self::ResponseStorageUnavailable => "response storage is unavailable",
316            Self::Transport(_) => "prepared request transport failed",
317            Self::ResponsePolicy(_) => "prepared response policy failed",
318        })
319    }
320}
321
322impl<E: fmt::Debug> core::error::Error for PreparedExecutionError<E> {}
323
324enum EndpointCheckError {
325    Invalid(EndpointIdentityError),
326    Mismatch,
327}
328
329fn map_endpoint_error<E>(error: EndpointCheckError) -> PreparedExecutionError<E> {
330    match error {
331        EndpointCheckError::Invalid(error) => PreparedExecutionError::EndpointIdentity(error),
332        EndpointCheckError::Mismatch => PreparedExecutionError::EndpointMismatch,
333    }
334}