cratestack_client_rust/client/
prepared_client.rs1use 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 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 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, ¶ms));
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 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}