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, EndpointIdentity, EndpointIdentityError,
10    ResponseStorageSanitizer, TransportRequest,
11};
12use crate::{ApiFamily, Provider};
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 expected endpoint identity.
75#[derive(Clone, Copy, Debug, Eq, PartialEq)]
76pub struct ProviderService {
77    provider: Provider,
78    family: ApiFamily,
79    endpoint: EndpointIdentity<'static>,
80}
81
82impl ProviderService {
83    /// Binds a provider API family to one normalized expected endpoint.
84    #[must_use]
85    pub const fn new(
86        provider: Provider,
87        family: ApiFamily,
88        endpoint: EndpointIdentity<'static>,
89    ) -> Self {
90        Self {
91            provider,
92            family,
93            endpoint,
94        }
95    }
96
97    /// Returns the provider namespace.
98    #[must_use]
99    pub const fn provider(self) -> Provider {
100        self.provider
101    }
102
103    /// Returns the provider API family.
104    #[must_use]
105    pub const fn family(self) -> ApiFamily {
106        self.family
107    }
108
109    /// Returns the immutable expected endpoint identity.
110    #[must_use]
111    pub const fn endpoint(self) -> EndpointIdentity<'static> {
112        self.endpoint
113    }
114}
115
116/// Complete request, endpoint, operation metadata, and response policy.
117#[derive(Clone, Copy)]
118pub struct PreparedRequest<'request> {
119    request: TransportRequest<'request>,
120    service: ProviderService,
121    metadata: OperationMetadata,
122    response_policy: ResponsePolicy,
123    operation_id: Option<OperationId>,
124}
125
126impl<'request> PreparedRequest<'request> {
127    /// Creates a complete prepared request without policy defaults.
128    #[must_use]
129    pub const fn new(
130        request: TransportRequest<'request>,
131        service: ProviderService,
132        metadata: OperationMetadata,
133        response_policy: ResponsePolicy,
134    ) -> Self {
135        Self {
136            request,
137            service,
138            metadata,
139            response_policy,
140            operation_id: None,
141        }
142    }
143
144    /// Binds a validated provider operation identifier to this request.
145    #[must_use]
146    pub const fn with_operation_id(mut self, operation_id: OperationId) -> Self {
147        self.operation_id = Some(operation_id);
148        self
149    }
150
151    /// Returns the validated transport request.
152    #[must_use]
153    pub const fn transport_request(self) -> TransportRequest<'request> {
154        self.request
155    }
156
157    /// Returns the bound provider service.
158    #[must_use]
159    pub const fn service(self) -> ProviderService {
160        self.service
161    }
162
163    /// Returns complete safety and retry metadata.
164    #[must_use]
165    pub const fn metadata(self) -> OperationMetadata {
166        self.metadata
167    }
168
169    /// Returns complete checked-response policy.
170    #[must_use]
171    pub const fn response_policy(self) -> ResponsePolicy {
172        self.response_policy
173    }
174
175    /// Returns the provider operation identifier when one was bound.
176    #[must_use]
177    pub const fn operation_id(self) -> Option<OperationId> {
178        self.operation_id
179    }
180
181    /// Applies the complete prepared response policy without executing transport.
182    pub fn validate_response<'buffer>(
183        self,
184        response: crate::transport::TransportResponse<'buffer>,
185    ) -> Result<CheckedResponse<'buffer>, ResponsePolicyError> {
186        self.response_policy.validate(response)
187    }
188
189    /// Verifies endpoint identity, executes once, and validates the response.
190    pub fn execute_blocking<'buffer, T>(
191        self,
192        transport: &T,
193        response_storage: &'buffer mut [u8],
194    ) -> Result<CheckedResponse<'buffer>, PreparedExecutionError<T::Error>>
195    where
196        T: BlockingTransport + BoundTransport + ResponseStorageSanitizer,
197    {
198        transport.sanitize_response_storage(response_storage);
199        self.verify_endpoint(transport)
200            .map_err(map_endpoint_error)?;
201        let admitted = self.admit_response_storage(response_storage)?;
202        let response = transport
203            .send(self.request, admitted)
204            .map_err(PreparedExecutionError::Transport)?;
205        self.response_policy
206            .validate(response)
207            .map_err(PreparedExecutionError::ResponsePolicy)
208    }
209
210    /// Async equivalent of [`Self::execute_blocking`] without owning an executor.
211    pub async fn execute_async<'transport, 'buffer, T>(
212        &'transport self,
213        transport: &'transport T,
214        response_storage: &'buffer mut [u8],
215    ) -> Result<CheckedResponse<'buffer>, PreparedExecutionError<T::Error>>
216    where
217        T: AsyncTransport + BoundTransport + ResponseStorageSanitizer,
218        'request: 'transport,
219        'buffer: 'transport,
220    {
221        transport.sanitize_response_storage(response_storage);
222        self.verify_endpoint(transport)
223            .map_err(map_endpoint_error)?;
224        let admitted = self.admit_response_storage(response_storage)?;
225        let response = transport
226            .send(self.request, admitted)
227            .await
228            .map_err(PreparedExecutionError::Transport)?;
229        self.response_policy
230            .validate(response)
231            .map_err(PreparedExecutionError::ResponsePolicy)
232    }
233
234    fn verify_endpoint<T>(self, transport: &T) -> Result<(), EndpointCheckError>
235    where
236        T: BoundTransport,
237    {
238        let actual = transport
239            .endpoint_identity()
240            .map_err(EndpointCheckError::Invalid)?;
241        if actual != self.service.endpoint {
242            return Err(EndpointCheckError::Mismatch);
243        }
244        Ok(())
245    }
246
247    fn admit_response_storage<E>(
248        self,
249        storage: &mut [u8],
250    ) -> Result<&mut [u8], PreparedExecutionError<E>> {
251        let admitted_len = core::cmp::min(storage.len(), self.response_policy.max_body_bytes());
252        storage
253            .get_mut(..admitted_len)
254            .ok_or(PreparedExecutionError::ResponseStorageUnavailable)
255    }
256}
257
258impl fmt::Debug for PreparedRequest<'_> {
259    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
260        formatter
261            .debug_struct("PreparedRequest")
262            .field("request", &self.request)
263            .field("service", &self.service)
264            .field("metadata", &self.metadata)
265            .field("response_policy", &self.response_policy)
266            .field("operation_id", &self.operation_id)
267            .finish()
268    }
269}
270
271/// Prepared execution failure with transport details redacted from diagnostics.
272#[derive(Clone, Copy, Eq, PartialEq)]
273pub enum PreparedExecutionError<E> {
274    /// The bound transport returned invalid endpoint identity.
275    EndpointIdentity(EndpointIdentityError),
276    /// The bound endpoint differs from the prepared provider service.
277    EndpointMismatch,
278    /// Response storage could not be structurally limited.
279    ResponseStorageUnavailable,
280    /// The concrete transport failed.
281    Transport(E),
282    /// The response failed provider-neutral policy.
283    ResponsePolicy(ResponsePolicyError),
284}
285
286impl<E> fmt::Debug for PreparedExecutionError<E> {
287    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
288        match self {
289            Self::EndpointIdentity(error) => formatter
290                .debug_tuple("EndpointIdentity")
291                .field(error)
292                .finish(),
293            Self::EndpointMismatch => formatter.write_str("EndpointMismatch"),
294            Self::ResponseStorageUnavailable => formatter.write_str("ResponseStorageUnavailable"),
295            Self::Transport(_) => formatter.write_str("Transport([redacted])"),
296            Self::ResponsePolicy(error) => formatter
297                .debug_tuple("ResponsePolicy")
298                .field(error)
299                .finish(),
300        }
301    }
302}
303
304impl<E> fmt::Display for PreparedExecutionError<E> {
305    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
306        formatter.write_str(match self {
307            Self::EndpointIdentity(_) => "transport endpoint identity is invalid",
308            Self::EndpointMismatch => "transport endpoint differs from prepared service",
309            Self::ResponseStorageUnavailable => "response storage is unavailable",
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}