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    CheckedResponseGuard, OperationId, OperationMetadata, ResponsePolicy, ResponsePolicyError,
7};
8use crate::transport::{
9    AsyncTransport, BlockingTransport, BoundTransport, EndpointIdentityError, EndpointPolicy,
10    ResponseBuffer, 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, 'sanitizer, S>(
189        self,
190        response: ResponseBuffer<'buffer, 'sanitizer, S>,
191    ) -> Result<CheckedResponseGuard<'buffer, 'sanitizer, S>, ResponsePolicyError>
192    where
193        S: ResponseStorageSanitizer + ?Sized,
194    {
195        self.response_policy.validate(response)
196    }
197
198    /// Verifies endpoint identity, executes once, and validates the response.
199    pub fn execute_blocking<'transport, 'buffer, T>(
200        self,
201        transport: &'transport T,
202        response_storage: &'buffer mut [u8],
203    ) -> Result<CheckedResponseGuard<'buffer, 'transport, T>, PreparedExecutionError<T::Error>>
204    where
205        T: BlockingTransport + BoundTransport + ResponseStorageSanitizer,
206    {
207        let mut response = ResponseBuffer::new(
208            response_storage,
209            self.response_policy.max_body_bytes(),
210            transport,
211        );
212        self.verify_endpoint(transport)
213            .map_err(map_endpoint_error)?;
214        transport
215            .send(self.request, response.writer())
216            .map_err(PreparedExecutionError::Transport)?;
217        self.response_policy
218            .validate(response)
219            .map_err(PreparedExecutionError::ResponsePolicy)
220    }
221
222    /// Async equivalent of [`Self::execute_blocking`] without owning an executor.
223    pub async fn execute_async<'transport, 'buffer, T>(
224        &'transport self,
225        transport: &'transport T,
226        response_storage: &'buffer mut [u8],
227    ) -> Result<CheckedResponseGuard<'buffer, 'transport, T>, PreparedExecutionError<T::Error>>
228    where
229        T: AsyncTransport + BoundTransport + ResponseStorageSanitizer,
230        'request: 'transport,
231    {
232        let mut response = ResponseBuffer::new(
233            response_storage,
234            self.response_policy.max_body_bytes(),
235            transport,
236        );
237        self.verify_endpoint(transport)
238            .map_err(map_endpoint_error)?;
239        transport
240            .send(self.request, response.writer())
241            .await
242            .map_err(PreparedExecutionError::Transport)?;
243        self.response_policy
244            .validate(response)
245            .map_err(PreparedExecutionError::ResponsePolicy)
246    }
247
248    fn verify_endpoint<T>(self, transport: &T) -> Result<(), EndpointCheckError>
249    where
250        T: BoundTransport,
251    {
252        let actual = transport
253            .endpoint_identity()
254            .map_err(EndpointCheckError::Invalid)?;
255        self.service
256            .endpoint_policy
257            .verify(actual)
258            .map_err(|_| EndpointCheckError::Mismatch)
259    }
260}
261
262impl fmt::Debug for PreparedRequest<'_> {
263    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
264        formatter
265            .debug_struct("PreparedRequest")
266            .field("request", &self.request)
267            .field("service", &self.service)
268            .field("metadata", &self.metadata)
269            .field("response_policy", &self.response_policy)
270            .field("operation_id", &self.operation_id)
271            .finish()
272    }
273}
274
275/// Prepared execution failure with transport details redacted from diagnostics.
276#[derive(Clone, Copy, Eq, PartialEq)]
277pub enum PreparedExecutionError<E> {
278    /// The bound transport returned invalid endpoint identity.
279    EndpointIdentity(EndpointIdentityError),
280    /// The bound endpoint differs from the prepared provider service.
281    EndpointMismatch,
282    /// The concrete transport failed.
283    Transport(E),
284    /// The response failed provider-neutral policy.
285    ResponsePolicy(ResponsePolicyError),
286}
287
288impl<E> fmt::Debug for PreparedExecutionError<E> {
289    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
290        match self {
291            Self::EndpointIdentity(error) => formatter
292                .debug_tuple("EndpointIdentity")
293                .field(error)
294                .finish(),
295            Self::EndpointMismatch => formatter.write_str("EndpointMismatch"),
296            Self::Transport(_) => formatter.write_str("Transport([redacted])"),
297            Self::ResponsePolicy(error) => formatter
298                .debug_tuple("ResponsePolicy")
299                .field(error)
300                .finish(),
301        }
302    }
303}
304
305impl<E> fmt::Display for PreparedExecutionError<E> {
306    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
307        formatter.write_str(match self {
308            Self::EndpointIdentity(_) => "transport endpoint identity is invalid",
309            Self::EndpointMismatch => "transport endpoint differs from prepared service",
310            Self::Transport(_) => "prepared request transport failed",
311            Self::ResponsePolicy(_) => "prepared response policy failed",
312        })
313    }
314}
315
316impl<E: fmt::Debug> core::error::Error for PreparedExecutionError<E> {}
317
318enum EndpointCheckError {
319    Invalid(EndpointIdentityError),
320    Mismatch,
321}
322
323fn map_endpoint_error<E>(error: EndpointCheckError) -> PreparedExecutionError<E> {
324    match error {
325        EndpointCheckError::Invalid(error) => PreparedExecutionError::EndpointIdentity(error),
326        EndpointCheckError::Mismatch => PreparedExecutionError::EndpointMismatch,
327    }
328}