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 default_security: Option<Vec<crate::client::security::SecurityRequirement>>,
28 ) -> Result<Self, ApiClientError> {
29 let operation_id = slug::slugify(format!("{method} {}", path.path));
30
31 let result = Self {
32 client,
33 base_uri,
34 collector_sender,
35 method,
36 path,
37 query: CallQuery::default(),
38 headers: None,
39 body: None,
40 authentication,
41 cookies: None,
42 expected_status_codes: ExpectedStatusCodes::default(),
43 metadata: OperationMetadata {
44 operation_id,
45 tags: None,
46 description: None,
47 #[cfg(feature = "redaction")]
48 response_description: None,
49 },
50 #[cfg(feature = "redaction")]
51 response_description: None,
52 skip_collection: false,
53 security: default_security,
54 };
55 Ok(result)
56 }
57}
58
59impl ApiCall {
60 async fn exchange(self) -> Result<CallResult, ApiClientError> {
120 let Self {
121 client,
122 base_uri,
123 collector_sender,
124 method,
125 path,
126 query,
127 headers,
128 body,
129 authentication,
130 cookies,
131 expected_status_codes,
132 metadata,
133 #[cfg(feature = "redaction")]
134 response_description,
135 skip_collection,
136 security,
137 } = self;
138
139 let resolved_auth = Self::resolve_authentication(authentication).await?;
141
142 let url = Self::build_url(&base_uri, &path, &query)?;
144 let parameters = CallParameters::with_all(query.clone(), headers.clone(), cookies.clone());
145 let request = Self::build_request(method.clone(), url, ¶meters, &body, &resolved_auth)?;
146
147 let operation_id = metadata.operation_id.clone();
149 #[cfg(feature = "redaction")]
150 let mut operation = Self::build_operation(
151 metadata,
152 &method,
153 &path,
154 parameters.clone(),
155 &body,
156 response_description,
157 security,
158 );
159 #[cfg(not(feature = "redaction"))]
160 let mut operation = Self::build_operation(
161 metadata,
162 &method,
163 &path,
164 parameters.clone(),
165 &body,
166 security,
167 );
168
169 debug!(?request, "sending...");
171 let response = client.execute(request).await?;
172 debug!(?response, "...receiving");
173
174 let status_code = response.status().as_u16();
176 if !expected_status_codes.contains(status_code) {
177 let body = response
179 .text()
180 .await
181 .map(|text| {
182 if text.len() > BODY_MAX_LENGTH {
183 format!("{}... (truncated)", &text[..1024])
184 } else {
185 text
186 }
187 })
188 .unwrap_or_else(|e| format!("<unable to read response body: {e}>"));
189 return Err(ApiClientError::UnexpectedStatusCode { status_code, body });
190 }
191
192 let call_result = if skip_collection {
194 CallResult::new_without_collection(response).await?
195 } else {
196 let call_result =
197 CallResult::new(operation_id, collector_sender.clone(), response).await?;
198 operation.add_response(call_result.clone());
199 Self::collect_schemas_and_operation(
200 &collector_sender,
201 &path,
202 ¶meters,
203 &body,
204 operation,
205 )
206 .await;
207 call_result
208 };
209
210 Ok(call_result)
211 }
212
213 pub(super) fn build_url(
214 base_uri: &Uri,
215 path: &CallPath,
216 query: &CallQuery,
217 ) -> Result<Url, ApiClientError> {
218 let path_resolved = PathResolved::try_from(path.clone())?;
219 let base_uri = base_uri.to_string();
220 let url = format!(
221 "{}/{}",
222 base_uri.trim_end_matches('/'),
223 path_resolved.path.trim_start_matches('/')
224 );
225 let mut url = url.parse::<Url>()?;
226
227 if !query.is_empty() {
228 let query_string = query.to_query_string()?;
229 url.set_query(Some(&query_string));
230 }
231
232 Ok(url)
233 }
234
235 pub(super) fn build_request(
236 method: Method,
237 url: Url,
238 parameters: &CallParameters,
239 body: &Option<CallBody>,
240 authentication: &Option<crate::client::Authentication>,
241 ) -> Result<Request, ApiClientError> {
242 let mut request = Request::new(method, url);
243 let req_headers = request.headers_mut();
244
245 if let Some(auth) = authentication {
247 let (header_name, header_value) = auth.to_header()?;
248 req_headers.insert(header_name, header_value);
249 }
250
251 for (name, value) in parameters.to_http_headers()? {
253 req_headers.insert(
254 HeaderName::from_bytes(name.as_bytes())?,
255 HeaderValue::from_str(&value)?,
256 );
257 }
258
259 let cookie_header = parameters.to_cookie_header()?;
261 if !cookie_header.is_empty() {
262 req_headers.insert(
263 HeaderName::from_static("cookie"),
264 HeaderValue::from_str(&cookie_header)?,
265 );
266 }
267
268 if let Some(body) = body {
270 req_headers.typed_insert(body.content_type.clone());
271 let req_body = request.body_mut();
272 *req_body = Some(Body::from(body.data.clone()));
273 }
274
275 Ok(request)
276 }
277
278 #[cfg(feature = "redaction")]
279 fn build_operation(
280 metadata: OperationMetadata,
281 method: &Method,
282 path: &CallPath,
283 parameters: CallParameters,
284 body: &Option<CallBody>,
285 response_description: Option<String>,
286 security: Option<Vec<crate::client::security::SecurityRequirement>>,
287 ) -> CalledOperation {
288 let OperationMetadata {
289 operation_id,
290 tags,
291 description,
292 response_description: _,
293 } = metadata;
294
295 CalledOperation::build(
296 method.clone(),
297 &path.path,
298 path,
299 parameters,
300 body.as_ref(),
301 OperationMetadata {
302 operation_id: operation_id.to_string(),
303 tags,
304 description,
305 response_description,
306 },
307 security,
308 )
309 }
310
311 #[cfg(not(feature = "redaction"))]
312 fn build_operation(
313 metadata: OperationMetadata,
314 method: &Method,
315 path: &CallPath,
316 parameters: CallParameters,
317 body: &Option<CallBody>,
318 security: Option<Vec<crate::client::security::SecurityRequirement>>,
319 ) -> CalledOperation {
320 CalledOperation::build(
321 method.clone(),
322 &path.path,
323 path,
324 parameters,
325 body.as_ref(),
326 metadata,
327 security,
328 )
329 }
330
331 async fn collect_schemas_and_operation(
332 sender: &CollectorSender,
333 path: &CallPath,
334 parameters: &CallParameters,
335 body: &Option<CallBody>,
336 operation: CalledOperation,
337 ) {
338 sender
340 .send(CollectorMessage::AddSchemas(path.schemas().clone()))
341 .await;
342
343 sender
345 .send(CollectorMessage::AddSchemas(parameters.collect_schemas()))
346 .await;
347
348 if let Some(body) = body {
350 sender
351 .send(CollectorMessage::AddSchemaEntry(body.entry.clone()))
352 .await;
353 }
354
355 sender
357 .send(CollectorMessage::RegisterOperation(operation))
358 .await;
359 }
360
361 async fn resolve_authentication(
367 authentication: Option<crate::client::Authentication>,
368 ) -> Result<Option<crate::client::Authentication>, ApiClientError> {
369 #[cfg(feature = "oauth2")]
370 {
371 use crate::client::Authentication;
372
373 match authentication {
374 Some(Authentication::OAuth2(ref config)) => {
375 let token = config
377 .0
378 .get_valid_token()
379 .await
380 .map_err(ApiClientError::oauth2_error)?;
381
382 Ok(Some(Authentication::Bearer(
384 token.access_token().to_string().into(),
385 )))
386 }
387 other => Ok(other),
388 }
389 }
390
391 #[cfg(not(feature = "oauth2"))]
392 {
393 Ok(authentication)
394 }
395 }
396}
397
398impl IntoFuture for ApiCall {
409 type Output = Result<CallResult, ApiClientError>;
410 type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send>>;
411
412 fn into_future(self) -> Self::IntoFuture {
413 Box::pin(self.exchange())
414 }
415}