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