clawspec_core/client/call/
execution.rs1use std::future::{Future, IntoFuture};
2use std::pin::Pin;
3
4use headers::HeaderMapExt;
5use http::header::{HeaderName, HeaderValue};
6use http::{Method, Uri};
7use reqwest::{Body, Request};
8use tracing::debug;
9use url::Url;
10
11use super::{ApiCall, BODY_MAX_LENGTH, CollectorSender};
12use crate::client::call_parameters::{CallParameters, OperationMetadata};
13use crate::client::openapi::CalledOperation;
14use crate::client::openapi::channel::CollectorMessage;
15use crate::client::parameters::PathResolved;
16use crate::client::response::ExpectedStatusCodes;
17use crate::client::{ApiClientError, CallBody, CallPath, CallQuery, CallResult};
18
19impl ApiCall {
20 pub(in crate::client) fn build(
21 client: reqwest::Client,
22 base_uri: Uri,
23 collector_sender: CollectorSender,
24 method: Method,
25 path: CallPath,
26 authentication: Option<crate::client::Authentication>,
27 ) -> Result<Self, ApiClientError> {
28 let operation_id = slug::slugify(format!("{method} {}", path.path));
29
30 let result = Self {
31 client,
32 base_uri,
33 collector_sender,
34 method,
35 path,
36 query: CallQuery::default(),
37 headers: None,
38 body: None,
39 authentication,
40 cookies: None,
41 expected_status_codes: ExpectedStatusCodes::default(),
42 metadata: OperationMetadata {
43 operation_id,
44 tags: None,
45 description: None,
46 response_description: None,
47 },
48 response_description: None,
49 skip_collection: false,
50 };
51 Ok(result)
52 }
53}
54
55impl ApiCall {
56 async fn exchange(self) -> Result<CallResult, ApiClientError> {
116 let Self {
117 client,
118 base_uri,
119 collector_sender,
120 method,
121 path,
122 query,
123 headers,
124 body,
125 authentication,
126 cookies,
127 expected_status_codes,
128 metadata,
129 response_description,
130 skip_collection,
131 } = self;
132
133 let url = Self::build_url(&base_uri, &path, &query)?;
135 let parameters = CallParameters::with_all(query.clone(), headers.clone(), cookies.clone());
136 let request =
137 Self::build_request(method.clone(), url, ¶meters, &body, &authentication)?;
138
139 let operation_id = metadata.operation_id.clone();
141 let mut operation = Self::build_operation(
142 metadata,
143 &method,
144 &path,
145 parameters.clone(),
146 &body,
147 response_description,
148 );
149
150 debug!(?request, "sending...");
152 let response = client.execute(request).await?;
153 debug!(?response, "...receiving");
154
155 let status_code = response.status().as_u16();
157 if !expected_status_codes.contains(status_code) {
158 let body = response
160 .text()
161 .await
162 .map(|text| {
163 if text.len() > BODY_MAX_LENGTH {
164 format!("{}... (truncated)", &text[..1024])
165 } else {
166 text
167 }
168 })
169 .unwrap_or_else(|e| format!("<unable to read response body: {e}>"));
170 return Err(ApiClientError::UnexpectedStatusCode { status_code, body });
171 }
172
173 let call_result = if skip_collection {
175 CallResult::new_without_collection(response).await?
176 } else {
177 let call_result =
178 CallResult::new(operation_id, collector_sender.clone(), response).await?;
179 operation.add_response(call_result.clone());
180 Self::collect_schemas_and_operation(
181 &collector_sender,
182 &path,
183 ¶meters,
184 &body,
185 operation,
186 )
187 .await;
188 call_result
189 };
190
191 Ok(call_result)
192 }
193
194 pub(super) fn build_url(
195 base_uri: &Uri,
196 path: &CallPath,
197 query: &CallQuery,
198 ) -> Result<Url, ApiClientError> {
199 let path_resolved = PathResolved::try_from(path.clone())?;
200 let base_uri = base_uri.to_string();
201 let url = format!(
202 "{}/{}",
203 base_uri.trim_end_matches('/'),
204 path_resolved.path.trim_start_matches('/')
205 );
206 let mut url = url.parse::<Url>()?;
207
208 if !query.is_empty() {
209 let query_string = query.to_query_string()?;
210 url.set_query(Some(&query_string));
211 }
212
213 Ok(url)
214 }
215
216 pub(super) fn build_request(
217 method: Method,
218 url: Url,
219 parameters: &CallParameters,
220 body: &Option<CallBody>,
221 authentication: &Option<crate::client::Authentication>,
222 ) -> Result<Request, ApiClientError> {
223 let mut request = Request::new(method, url);
224 let req_headers = request.headers_mut();
225
226 if let Some(auth) = authentication {
228 let (header_name, header_value) = auth.to_header()?;
229 req_headers.insert(header_name, header_value);
230 }
231
232 for (name, value) in parameters.to_http_headers()? {
234 req_headers.insert(
235 HeaderName::from_bytes(name.as_bytes())?,
236 HeaderValue::from_str(&value)?,
237 );
238 }
239
240 let cookie_header = parameters.to_cookie_header()?;
242 if !cookie_header.is_empty() {
243 req_headers.insert(
244 HeaderName::from_static("cookie"),
245 HeaderValue::from_str(&cookie_header)?,
246 );
247 }
248
249 if let Some(body) = body {
251 req_headers.typed_insert(body.content_type.clone());
252 let req_body = request.body_mut();
253 *req_body = Some(Body::from(body.data.clone()));
254 }
255
256 Ok(request)
257 }
258
259 fn build_operation(
260 metadata: OperationMetadata,
261 method: &Method,
262 path: &CallPath,
263 parameters: CallParameters,
264 body: &Option<CallBody>,
265 response_description: Option<String>,
266 ) -> CalledOperation {
267 let OperationMetadata {
268 operation_id,
269 tags,
270 description,
271 response_description: _,
272 } = metadata;
273
274 CalledOperation::build(
275 method.clone(),
276 &path.path,
277 path,
278 parameters,
279 body.as_ref(),
280 OperationMetadata {
281 operation_id: operation_id.to_string(),
282 tags,
283 description,
284 response_description,
285 },
286 )
287 }
288
289 async fn collect_schemas_and_operation(
290 sender: &CollectorSender,
291 path: &CallPath,
292 parameters: &CallParameters,
293 body: &Option<CallBody>,
294 operation: CalledOperation,
295 ) {
296 sender
298 .send(CollectorMessage::AddSchemas(path.schemas().clone()))
299 .await;
300
301 sender
303 .send(CollectorMessage::AddSchemas(parameters.collect_schemas()))
304 .await;
305
306 if let Some(body) = body {
308 sender
309 .send(CollectorMessage::AddSchemaEntry(body.entry.clone()))
310 .await;
311 }
312
313 sender
315 .send(CollectorMessage::RegisterOperation(operation))
316 .await;
317 }
318}
319
320impl IntoFuture for ApiCall {
331 type Output = Result<CallResult, ApiClientError>;
332 type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send>>;
333
334 fn into_future(self) -> Self::IntoFuture {
335 Box::pin(self.exchange())
336 }
337}