Skip to main content

cratestack_client_rust/client/
prepared_client.rs

1use super::compact::CompactCall;
2use super::prepared::{PreparedCall, bad};
3use crate::{
4    ClientError, CratestackClient, HeaderPair, HttpClientCodec, QueryPair, RouteRef, RuntimeHeader,
5    RuntimeResponseWire, TypedResponse,
6};
7use cratestack_core::compact::{COMPACT_CBOR, COMPACT_JSON};
8use reqwest::Method;
9use serde::{Serialize, de::DeserializeOwned};
10
11impl<C: HttpClientCodec> CratestackClient<C> {
12    /// Encode and freeze one keyed unary mutation before any network I/O.
13    /// Requires compact key tables, even when this first payload stays named.
14    /// Persist the descriptor before calling `execute_prepared`; the framework
15    /// does not enqueue it. Credentials come from the authorizer on each attempt.
16    pub fn prepare_call<Input: Serialize>(
17        &self,
18        method: &str,
19        route: RouteRef<'_>,
20        path: &str,
21        input: &Input,
22        query: &[QueryPair<'_>],
23        headers: &[HeaderPair<'_>],
24    ) -> Result<PreparedCall, ClientError> {
25        super::compact_identity::refuse_duplicates(headers)?;
26        if !matches!(C::CONTENT_TYPE, "application/cbor" | "application/json") {
27            return Err(bad("prepared calls require a CBOR or JSON codec"));
28        }
29        let method = Method::from_bytes(method.as_bytes()).map_err(|e| bad(e.to_string()))?;
30        let scoped = self.at(route);
31        let compact = scoped
32            .compact_call(&method, headers, true)
33            .ok_or_else(|| bad("prepare_call requires compact tables for this operation"))?;
34        let plain_accept = if self.is_sealed() {
35            self.codec.payload_accept()
36        } else {
37            self.codec.accept_header_value()
38        };
39        let sent = compact.prepare(
40            Some(self.codec.encode(input)?),
41            C::CONTENT_TYPE,
42            plain_accept,
43            false,
44        );
45        let call = PreparedCall {
46            version: 1,
47            base_url: self.config.base_url.to_string(),
48            method: method.to_string(),
49            path: path.to_owned(),
50            route: route.template().to_owned(),
51            params: route.params().iter().map(|s| (*s).to_owned()).collect(),
52            query: if query.is_empty() {
53                None
54            } else {
55                Some(serde_urlencoded::to_string(query).map_err(|e| bad(e.to_string()))?)
56            },
57            headers: headers
58                .iter()
59                .map(|(name, value)| RuntimeHeader {
60                    name: (*name).to_owned(),
61                    value: (*value).to_owned(),
62                })
63                .collect(),
64            body: sent.body.unwrap_or_default(),
65            content_type: sent.content_type.to_owned(),
66            plain_type: C::CONTENT_TYPE.to_owned(),
67            accept: sent.accept,
68            contract: compact.state.tables.ops[compact.index].digest,
69            key_tables: compact.state.tables.to_bytes(),
70            op: compact.op,
71            sealed: self.is_sealed(),
72        };
73        call.validate()?;
74        Ok(call)
75    }
76
77    /// Send original inner bytes, reauthorizing and (when configured) resealing
78    /// with fresh COSE claims. No compact negotiation or request transcoding runs.
79    /// A changed service, codec, seal mode, or op contract fails before sending.
80    pub async fn execute_prepared(
81        &self,
82        call: &PreparedCall,
83    ) -> Result<RuntimeResponseWire, ClientError> {
84        call.validate()?;
85        if call.base_url != self.config.base_url.as_str()
86            || call.plain_type != C::CONTENT_TYPE
87            || call.sealed != self.is_sealed()
88        {
89            return Err(bad(
90                "prepared service, codec or sealing mode differs from this client",
91            ));
92        }
93        let params = call.params.iter().map(String::as_str).collect::<Vec<_>>();
94        let scoped = self.at(RouteRef::new(&call.route, &params));
95        scoped.prepared_compact(call)?;
96        let headers = call
97            .headers
98            .iter()
99            .map(|h| (h.name.as_str(), h.value.as_str()))
100            .collect::<Vec<_>>();
101        scoped
102            .send_buffered(
103                Method::from_bytes(call.method.as_bytes()).map_err(|e| bad(e.to_string()))?,
104                &call.path,
105                Some(call.body.clone()),
106                call.query.as_deref(),
107                &headers,
108                None,
109                Some(call),
110            )
111            .await
112    }
113
114    /// Typed equivalent of `execute_prepared`, retaining replay/status headers.
115    pub async fn execute_prepared_typed<Output: DeserializeOwned>(
116        &self,
117        call: &PreparedCall,
118    ) -> Result<TypedResponse<Output>, ClientError> {
119        let wire = self.execute_prepared(call).await?;
120        super::decode::decode_typed_response_with_metadata(&self.codec, &wire)
121    }
122
123    pub(super) fn prepared_compact(&self, call: &PreparedCall) -> Result<CompactCall, ClientError> {
124        let state = self
125            .compact
126            .as_ref()
127            .ok_or_else(|| bad("prepared call needs its contract key tables"))?;
128        let index = state
129            .tables
130            .ops
131            .iter()
132            .position(|keys| keys.op == call.op && keys.digest == call.contract)
133            .ok_or_else(|| bad("prepared op contract differs; restore with its original schema"))?;
134        call.wire_type::<C>()?;
135        if call.key_tables != state.tables.to_bytes() {
136            return Err(bad("prepared key tables differ from this client"));
137        }
138        let compact = CompactCall {
139            state: state.clone(),
140            op: call.op.clone(),
141            index,
142            send_compact: false,
143            selector: cratestack_core::ContractSelector::of(&call.contract).to_header_value(),
144        };
145        let plain_accept = if self.is_sealed() {
146            self.codec.payload_accept()
147        } else {
148            self.codec.accept_header_value()
149        };
150        if call.accept
151            != compact
152                .prepare(None, C::CONTENT_TYPE, plain_accept, false)
153                .accept
154        {
155            return Err(bad("prepared accept differs from this client"));
156        }
157        Ok(compact)
158    }
159
160    pub(super) fn is_sealed(&self) -> bool {
161        #[cfg(feature = "cose")]
162        {
163            self.sealing.envelope.is_some()
164        }
165        #[cfg(not(feature = "cose"))]
166        {
167            false
168        }
169    }
170}
171
172impl PreparedCall {
173    pub(super) fn wire_type<C: HttpClientCodec>(&self) -> Result<&'static str, ClientError> {
174        if self.content_type == C::CONTENT_TYPE {
175            return Ok(C::CONTENT_TYPE);
176        }
177        match (C::CONTENT_TYPE, self.content_type.as_str()) {
178            ("application/cbor", COMPACT_CBOR) => Ok(COMPACT_CBOR),
179            ("application/json", COMPACT_JSON) => Ok(COMPACT_JSON),
180            _ => Err(bad("prepared payload type is incompatible with the codec")),
181        }
182    }
183}