use std::collections::HashMap;
use std::sync::Arc;
use async_trait::async_trait;
use azure_core::error::{Error, ErrorKind};
use azure_core::http::headers::{HeaderName, HeaderValue, Headers};
use azure_core::http::request::{Body, Request};
use azure_core::http::response::PinnedStream;
use azure_core::http::{AsyncRawResponse, HttpClient as HttpClientTrait};
use bytesbuf::BytesView;
use futures::{StreamExt as _, TryStreamExt as _};
use layered::Service as _;
#[derive(Debug, Clone)]
pub struct HttpClient {
client: fetch::HttpClient,
}
impl HttpClient {
#[must_use]
pub const fn new(client: fetch::HttpClient) -> Self {
Self { client }
}
fn to_fetch_request(&self, request: &Request) -> azure_core::Result<fetch::HttpRequest> {
let mut builder = self.client.request(request.method().as_str(), request.url().as_str());
for (name, value) in request.headers().iter() {
builder = builder.header(name.as_str(), value.as_str());
}
builder.body(self.to_fetch_body(request.body())).build().map_err(|error| {
Error::with_error(
ErrorKind::DataConversion,
error,
"failed to convert the Azure request into a fetch request",
)
})
}
#[cfg_attr(test, mutants::skip)]
fn to_fetch_body(&self, body: &Body) -> fetch::HttpBody {
let builder: &fetch::HttpBodyBuilder = self.client.as_ref();
match body {
Body::Bytes(bytes) if bytes.is_empty() => builder.empty(),
Body::Bytes(bytes) => builder.bytes(BytesView::from(bytes.clone())),
Body::SeekableStream(stream) => {
let stream = stream.clone().map(|chunk| {
chunk
.map(BytesView::from)
.map_err(|error| fetch::HttpError::unavailable(format!("failed to read the Azure request body: {error}")))
});
builder.stream(stream, &fetch::options::HttpBodyOptions::default())
}
}
}
}
impl From<fetch::HttpClient> for HttpClient {
fn from(client: fetch::HttpClient) -> Self {
Self::new(client)
}
}
impl From<HttpClient> for Arc<dyn HttpClientTrait> {
fn from(client: HttpClient) -> Self {
Arc::new(client)
}
}
#[async_trait]
impl HttpClientTrait for HttpClient {
async fn execute_request(&self, request: &Request) -> azure_core::Result<AsyncRawResponse> {
let request = self.to_fetch_request(request)?;
let response = self
.client
.execute(request)
.await
.map_err(|error| Error::with_error(ErrorKind::Io, error, "the fetch HTTP client failed to execute the request"))?;
Ok(to_async_raw_response(response))
}
}
fn to_async_raw_response(response: fetch::HttpResponse) -> AsyncRawResponse {
let (parts, body) = response.into_parts();
let status = parts.status.as_u16().into();
let headers = to_headers(&parts.headers);
let body = body
.into_stream()
.map_ok(|view| view.to_bytes())
.map_err(|error| Error::with_error(ErrorKind::Io, error, "failed to read the response body"));
let body: PinnedStream = Box::pin(body);
AsyncRawResponse::new(status, headers, body)
}
fn to_headers(map: &http::HeaderMap) -> Headers {
let headers = map
.iter()
.filter_map(|(name, value)| {
value
.to_str()
.ok()
.map(|value| (HeaderName::from(name.as_str().to_owned()), HeaderValue::from(value.to_owned())))
})
.collect::<HashMap<_, _>>();
Headers::from(headers)
}