Skip to main content

cratestack_client_rust/client/
prepared.rs

1//! Durable inner calls. Storage and commit-before-send ordering belong to the caller.
2
3use serde::{Deserialize, Serialize};
4
5use crate::{ClientError, RuntimeHeader};
6
7/// A prepared unary mutation, including its final inner payload and key mode.
8///
9/// Persist [`Self::to_bytes`] before the first `execute_prepared` attempt. Restore
10/// with [`Self::from_bytes`], then execute with the same service, codec, sealing
11/// mode and op contract. A changed contract is refused: this format is not an
12/// old-schema migration mechanism. Each sealed attempt gets fresh COSE claims;
13/// this object contains neither an outer seal nor authentication headers.
14/// URL userinfo is rejected; query and body data may still contain secrets.
15/// Protect journal contents like the business payloads they contain.
16#[derive(Clone, Serialize, Deserialize)]
17#[serde(deny_unknown_fields)]
18pub struct PreparedCall {
19    pub(super) version: u8,
20    pub(super) base_url: String,
21    pub(super) method: String,
22    pub(super) path: String,
23    pub(super) route: String,
24    pub(super) params: Vec<String>,
25    pub(super) query: Option<String>,
26    pub(super) headers: Vec<RuntimeHeader>,
27    pub(super) body: Vec<u8>,
28    pub(super) content_type: String,
29    pub(super) plain_type: String,
30    pub(super) accept: String,
31    pub(super) op: String,
32    pub(super) contract: [u8; 32],
33    pub(super) key_tables: Vec<u8>,
34    pub(super) sealed: bool,
35}
36
37pub(super) const MAX_SERIALIZED: usize = 16 * 1024 * 1024;
38pub(super) const MAX_PAYLOAD: usize = 8 * 1024 * 1024;
39
40impl PreparedCall {
41    /// Serialize a validated descriptor; no request has to be sent first.
42    pub fn to_bytes(&self) -> Result<Vec<u8>, ClientError> {
43        self.validate()?;
44        let bytes = serde_json::to_vec(self).map_err(|e| bad(e.to_string()))?;
45        if bytes.len() > MAX_SERIALIZED {
46            return Err(bad("prepared descriptor exceeds 16 MiB"));
47        }
48        Ok(bytes)
49    }
50
51    /// Restore a bounded, versioned descriptor. Execution validates its runtime
52    /// binding again, including when a caller uses serde directly instead.
53    pub fn from_bytes(bytes: &[u8]) -> Result<Self, ClientError> {
54        if bytes.len() > MAX_SERIALIZED {
55            return Err(bad("prepared descriptor exceeds 16 MiB"));
56        }
57        let call: Self = serde_json::from_slice(bytes).map_err(|e| bad(e.to_string()))?;
58        call.validate()?;
59        Ok(call)
60    }
61
62    pub(super) fn restore_identity_headers(
63        &self,
64        headers: &mut reqwest::header::HeaderMap,
65    ) -> Result<(), ClientError> {
66        for name in ["idempotency-key", "if-match"] {
67            headers.remove(name);
68            if let Some(saved) = self
69                .headers
70                .iter()
71                .find(|h| h.name.eq_ignore_ascii_case(name))
72            {
73                let value = reqwest::header::HeaderValue::from_str(&saved.value)
74                    .map_err(|e| bad(e.to_string()))?;
75                headers.insert(reqwest::header::HeaderName::from_static(name), value);
76            }
77        }
78        Ok(())
79    }
80
81    /// Final inner bytes; these are preserved across every attempt.
82    pub fn payload(&self) -> &[u8] {
83        &self.body
84    }
85    /// The original inner media type, including compact mode when negotiated.
86    pub fn payload_type(&self) -> &str {
87        &self.content_type
88    }
89    /// The contract under which the payload was prepared.
90    pub fn contract_sha(&self) -> &[u8; 32] {
91        &self.contract
92    }
93}
94
95pub(super) fn bad(message: impl Into<String>) -> ClientError {
96    ClientError::BadInput(message.into())
97}