use std::future::{Future, IntoFuture};
use std::pin::Pin;
use headers::HeaderMapExt;
use http::header::{HeaderName, HeaderValue};
use http::{Method, Uri};
use reqwest::{Body, Request};
use tracing::debug;
use url::Url;
use super::{ApiCall, BODY_MAX_LENGTH, CollectorSender};
use crate::client::call_parameters::{CallParameters, OperationMetadata};
use crate::client::openapi::CalledOperation;
use crate::client::openapi::channel::CollectorMessage;
use crate::client::parameters::PathResolved;
use crate::client::response::ExpectedStatusCodes;
use crate::client::{ApiClientError, CallBody, CallPath, CallQuery, CallResult};
impl ApiCall {
pub(in crate::client) fn build(
client: reqwest::Client,
base_uri: Uri,
collector_sender: CollectorSender,
method: Method,
path: CallPath,
authentication: Option<crate::client::Authentication>,
default_security: Option<Vec<crate::client::security::SecurityRequirement>>,
) -> Result<Self, ApiClientError> {
let operation_id = slug::slugify(format!("{method} {}", path.path));
let result = Self {
client,
base_uri,
collector_sender,
method,
path,
query: CallQuery::default(),
headers: None,
body: None,
authentication,
cookies: None,
expected_status_codes: ExpectedStatusCodes::default(),
metadata: OperationMetadata {
operation_id,
tags: None,
description: None,
#[cfg(feature = "redaction")]
response_description: None,
},
#[cfg(feature = "redaction")]
response_description: None,
skip_collection: false,
security: default_security,
};
Ok(result)
}
}
impl ApiCall {
async fn exchange(self) -> Result<CallResult, ApiClientError> {
let Self {
client,
base_uri,
collector_sender,
method,
path,
query,
headers,
body,
authentication,
cookies,
expected_status_codes,
metadata,
#[cfg(feature = "redaction")]
response_description,
skip_collection,
security,
} = self;
let resolved_auth = Self::resolve_authentication(authentication).await?;
let url = Self::build_url(&base_uri, &path, &query)?;
let parameters = CallParameters::with_all(query.clone(), headers.clone(), cookies.clone());
let request = Self::build_request(method.clone(), url, ¶meters, &body, &resolved_auth)?;
let operation_id = metadata.operation_id.clone();
#[cfg(feature = "redaction")]
let mut operation = Self::build_operation(
metadata,
&method,
&path,
parameters.clone(),
&body,
response_description,
security,
);
#[cfg(not(feature = "redaction"))]
let mut operation = Self::build_operation(
metadata,
&method,
&path,
parameters.clone(),
&body,
security,
);
debug!(?request, "sending...");
let response = client.execute(request).await?;
debug!(?response, "...receiving");
let status_code = response.status().as_u16();
if !expected_status_codes.contains(status_code) {
let body = response
.text()
.await
.map(|text| {
if text.len() > BODY_MAX_LENGTH {
format!("{}... (truncated)", &text[..1024])
} else {
text
}
})
.unwrap_or_else(|e| format!("<unable to read response body: {e}>"));
return Err(ApiClientError::UnexpectedStatusCode { status_code, body });
}
let call_result = if skip_collection {
CallResult::new_without_collection(response).await?
} else {
let call_result =
CallResult::new(operation_id, collector_sender.clone(), response).await?;
operation.add_response(call_result.clone());
Self::collect_schemas_and_operation(
&collector_sender,
&path,
¶meters,
&body,
operation,
)
.await;
call_result
};
Ok(call_result)
}
pub(super) fn build_url(
base_uri: &Uri,
path: &CallPath,
query: &CallQuery,
) -> Result<Url, ApiClientError> {
let path_resolved = PathResolved::try_from(path.clone())?;
let base_uri = base_uri.to_string();
let url = format!(
"{}/{}",
base_uri.trim_end_matches('/'),
path_resolved.path.trim_start_matches('/')
);
let mut url = url.parse::<Url>()?;
if !query.is_empty() {
let query_string = query.to_query_string()?;
url.set_query(Some(&query_string));
}
Ok(url)
}
pub(super) fn build_request(
method: Method,
url: Url,
parameters: &CallParameters,
body: &Option<CallBody>,
authentication: &Option<crate::client::Authentication>,
) -> Result<Request, ApiClientError> {
let mut request = Request::new(method, url);
let req_headers = request.headers_mut();
if let Some(auth) = authentication {
let (header_name, header_value) = auth.to_header()?;
req_headers.insert(header_name, header_value);
}
for (name, value) in parameters.to_http_headers()? {
req_headers.insert(
HeaderName::from_bytes(name.as_bytes())?,
HeaderValue::from_str(&value)?,
);
}
let cookie_header = parameters.to_cookie_header()?;
if !cookie_header.is_empty() {
req_headers.insert(
HeaderName::from_static("cookie"),
HeaderValue::from_str(&cookie_header)?,
);
}
if let Some(body) = body {
req_headers.typed_insert(body.content_type.clone());
let req_body = request.body_mut();
*req_body = Some(Body::from(body.data.clone()));
}
Ok(request)
}
#[cfg(feature = "redaction")]
fn build_operation(
metadata: OperationMetadata,
method: &Method,
path: &CallPath,
parameters: CallParameters,
body: &Option<CallBody>,
response_description: Option<String>,
security: Option<Vec<crate::client::security::SecurityRequirement>>,
) -> CalledOperation {
let OperationMetadata {
operation_id,
tags,
description,
response_description: _,
} = metadata;
CalledOperation::build(
method.clone(),
&path.path,
path,
parameters,
body.as_ref(),
OperationMetadata {
operation_id: operation_id.to_string(),
tags,
description,
response_description,
},
security,
)
}
#[cfg(not(feature = "redaction"))]
fn build_operation(
metadata: OperationMetadata,
method: &Method,
path: &CallPath,
parameters: CallParameters,
body: &Option<CallBody>,
security: Option<Vec<crate::client::security::SecurityRequirement>>,
) -> CalledOperation {
CalledOperation::build(
method.clone(),
&path.path,
path,
parameters,
body.as_ref(),
metadata,
security,
)
}
async fn collect_schemas_and_operation(
sender: &CollectorSender,
path: &CallPath,
parameters: &CallParameters,
body: &Option<CallBody>,
operation: CalledOperation,
) {
sender
.send(CollectorMessage::AddSchemas(path.schemas().clone()))
.await;
sender
.send(CollectorMessage::AddSchemas(parameters.collect_schemas()))
.await;
if let Some(body) = body {
sender
.send(CollectorMessage::AddSchemaEntry(body.entry.clone()))
.await;
}
sender
.send(CollectorMessage::RegisterOperation(operation))
.await;
}
async fn resolve_authentication(
authentication: Option<crate::client::Authentication>,
) -> Result<Option<crate::client::Authentication>, ApiClientError> {
#[cfg(feature = "oauth2")]
{
use crate::client::Authentication;
match authentication {
Some(Authentication::OAuth2(ref config)) => {
let token = config
.0
.get_valid_token()
.await
.map_err(ApiClientError::oauth2_error)?;
Ok(Some(Authentication::Bearer(
token.access_token().to_string().into(),
)))
}
other => Ok(other),
}
}
#[cfg(not(feature = "oauth2"))]
{
Ok(authentication)
}
}
}
impl IntoFuture for ApiCall {
type Output = Result<CallResult, ApiClientError>;
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send>>;
fn into_future(self) -> Self::IntoFuture {
Box::pin(self.exchange())
}
}