use bytes::Bytes;
use http_body_util::{BodyExt, Full, Limited};
use hyper::{Method, Request, StatusCode};
use serde::Deserialize;
use std::fmt::{Debug, Formatter};
use time::OffsetDateTime;
use tracing::{instrument, trace};
use crate::api::Api;
use crate::error::{ApiError, Error};
use crate::http_client::{HttpClient, new_http_client};
use crate::tc3_hmac;
#[derive(Debug, Clone)]
pub struct Client {
region: String,
http_client: HttpClient,
auth: Auth,
response_size_limit: Option<usize>,
}
impl Client {
pub fn new(region: String, auth: Auth, response_size_limit: impl Into<Option<usize>>) -> Self {
Self {
region,
http_client: new_http_client(),
auth,
response_size_limit: response_size_limit.into(),
}
}
#[instrument(level = "trace", err)]
pub async fn send<A: Api>(
&self,
request: &A::Request<'_>,
) -> Result<(A::Response, String), Error> {
let payload = serde_json::to_vec(request)?;
trace!("marshal request done");
let request = self.create_request::<A>(payload)?;
trace!(?request, "create http request done");
let response = self.http_client.request(request).await?;
trace!(?response, "get http response done");
if response.status() != StatusCode::OK {
return Err(Error::Other(
format!("status code is not OK: {}", response.status()).into(),
));
}
let body = response.into_body();
let body = match self.response_size_limit {
None => body.collect().await?,
Some(limit) => Limited::new(body, limit)
.collect()
.await
.map_err(Error::Other)?,
}
.to_bytes();
trace!("read http body done");
let response = serde_json::from_slice::<Response<A::Response>>(&body)?;
trace!(?response, "unmarshal response done");
if let Some(err) = response.response.error {
return Err(Error::Api {
err,
request_id: response.response.request_id,
});
}
match response.response.response {
None => Err(Error::Other("miss response".into())),
Some(resp) => Ok((resp, response.response.request_id)),
}
}
#[instrument(level = "trace", err)]
fn create_request<A: Api>(&self, payload: Vec<u8>) -> Result<Request<Full<Bytes>>, Error> {
let now = OffsetDateTime::now_utc();
let authorization = tc3_hmac::calculate_authorization(
&self.auth.secret_id,
&self.auth.secret_key,
A::SERVICE,
A::HOST,
&payload,
&now,
)
.map_err(Error::Other)?;
let request = Request::builder()
.uri(format!("https://{}", A::HOST))
.method(Method::POST)
.header("Authorization", authorization)
.header("Content-Type", "application/json; charset=utf-8")
.header("X-TC-Action", A::ACTION)
.header("X-TC-Timestamp", now.unix_timestamp())
.header("X-TC-Version", A::VERSION)
.header("X-TC-Region", &self.region)
.body(Full::new(Bytes::from(payload)))
.map_err(|err| Error::Other(err.into()))?;
Ok(request)
}
}
#[derive(Clone)]
pub struct Auth {
secret_key: String,
secret_id: String,
}
impl Auth {
pub fn new(secret_key: String, secret_id: String) -> Self {
Self {
secret_key,
secret_id,
}
}
}
impl Debug for Auth {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Auth")
.field("secret_id", &self.secret_id)
.finish_non_exhaustive()
}
}
#[derive(Debug, Deserialize)]
struct Response<T> {
#[serde(rename = "Response")]
response: ResponseDetail<T>,
}
#[derive(Debug, Deserialize)]
struct ResponseDetail<T> {
#[serde(rename = "RequestId")]
request_id: String,
#[serde(flatten)]
response: Option<T>,
#[serde(rename = "Error")]
error: Option<ApiError>,
}