use std::borrow::Cow;
use std::marker::PhantomData;
pub use http;
pub use serde_json;
#[cfg(feature = "hyper")]
pub use hyper;
#[cfg(feature = "hyper")]
pub use hyper_tls;
use http::{
header::{self, HeaderMap, HeaderValue},
Method, StatusCode,
};
use serde::{Deserialize, Serialize};
pub mod transport;
#[cfg(feature = "hyper")]
#[macro_export]
macro_rules! client {
(
$(#[$client_meta:meta])*
$client_vis:vis struct $Client:ident<B, T: $($(hapic::)?transport::)?Transport<B>>;
$(#[$trait_meta:meta])*
$trait_vis:vis trait $ApiCall:ident;
) => {
$(#[$client_meta])*
$client_vis struct $Client<B, T: $crate::transport::Transport<B>> {
pub client: $crate::Client<B, T>,
}
$(#[$trait_meta])*
$trait_vis trait $ApiCall: $crate::RawApiCall {}
impl $Client<$crate::hyper::Body, $crate::transport::HttpsTransport> {
pub fn new(endpoint: std::borrow::Cow<'static, str>) -> Self {
$Client {
client: $crate::Client::new_https_client(endpoint),
}
}
}
impl<B: Send + Sync, T: $crate::transport::Transport<B>> $Client<B, T> {
pub async fn call<C>(
&self,
api_call: C,
) -> std::result::Result<<C as $crate::RawApiCall>::Output, $crate::Error>
where
C: $ApiCall + Send,
B: From<<C as $crate::RawApiCall>::RequestBody>,
{
$crate::RawApiCall::request(api_call, &self.client).await
}
}
};
}
#[cfg(not(feature = "hyper"))]
#[macro_export]
macro_rules! client {
(
$(#[$client_meta:meta])*
$client_vis:vis struct $Client:ident<B, T: $($(hapic::)?transport::)?Transport<B>>;
$(#[$trait_meta:meta])*
$trait_vis:vis trait $ApiCall:ident;
) => {
$(#[$client_meta])*
$client_vis struct $Client<B, T: $crate::transport::Transport<B>> {
pub client: $crate::Client<B, T>,
}
$(#[$trait_meta])*
$trait_vis trait $ApiCall: $crate::RawApiCall {}
impl<B: Send + Sync, T: $crate::transport::Transport<B>> $Client<B, T> {
pub async fn call<C>(
&self,
api_call: C,
) -> std::result::Result<<C as $crate::RawApiCall>::Output, $crate::Error>
where
C: $ApiCall + Send,
B: From<<C as $crate::RawApiCall>::RequestBody>,
{
$crate::RawApiCall::request(api_call, &self.client).await
}
}
};
}
#[macro_export]
macro_rules! json_api {
(
$(#[$client_meta:meta])*
$client_vis:vis struct $Client:ident<B, T: $($(hapic::)?transport::)?Transport<B>>;
$(#[$trait_meta:meta])*
$trait_vis:vis trait $ApiCall:ident;
$($tt:tt)*
) => {
$crate::client!(
$(#[$client_meta])*
$client_vis struct $Client<B, T: Transport<B>>;
$(#[$trait_meta])*
$trait_vis trait $ApiCall;
);
$crate::json_api_call!(
ApiCall: $ApiCall;
$($tt)*
);
}
}
#[macro_export]
macro_rules! json_api_call {
(simple $(<$($a:lifetime),*>)? ($ApiCall:ty) $method:ident $url:literal : $Input:ty => $Output:ty) => {
impl$(<$($a),*>)? $ApiCall for $Input {}
$crate::json_api_call!(simple $(<$($a),*>)? $method $url : $Input => $Output);
};
(simple $(<$($a:lifetime),*>)? $method:ident $url:literal : $Input:ty => $Output:ty) => {
impl$(<$($a),*>)? $crate::SimpleApiCall for $Input {
type Output = $Output;
fn method(&self) -> $crate::http::Method {
$crate::http::Method::$method
}
fn uri(&self, endpoint: &str) -> std::string::String {
let uri = String::with_capacity(endpoint.len() + $url.len());
uri + endpoint + $url
}
}
};
(simple $(<$($a:lifetime),*>)? $(($ApiCall:ty))? $url:literal : $Input:ty => $Output:ty) => {
$crate::json_api_call!(simple$(<$($a),*>)? $(($ApiCall))? POST $url: $Input => $Output);
};
(json $(<$($a:lifetime),*>)? ($ApiCall:ty) $method:ident $url:literal : $Input:ty as $JsonInput:ty => $JsonOutput:ty as $Output:ty) => {
impl$(<$($a),*>)? $ApiCall for $Input {}
$crate::json_api_call!(
json$(<$($a),*>)? $method $url : $Input as $JsonInput => $JsonOutput as $Output
);
};
(json $(<$($a:lifetime),*>)? $method:ident $url:literal : $Input:ty as $JsonInput:ty => $JsonOutput:ty as $Output:ty) => {
impl$(<$($a),*>)? $crate::JsonApiCall for $Input {
type Output = $Output;
type JsonResponse = $JsonOutput;
type JsonRequest = $JsonInput;
fn method(&self) -> $crate::http::Method {
$crate::http::Method::$method
}
fn uri(&self, endpoint: &str) -> std::string::String {
let uri = String::with_capacity(endpoint.len() + $url.len());
uri + endpoint + $url
}
fn try_into_request(self) -> std::result::Result<$JsonInput, $crate::Error> {
Ok(self.try_into()?)
}
fn parse_json_response(
status: $crate::http::StatusCode,
content_type: Option<$crate::http::HeaderValue>,
raw_resp: Vec<u8>,
resp: $crate::serde_json::Result<Self::JsonResponse>,
) -> std::result::Result<$Output, $crate::Error> {
if status.is_success() {
Ok(resp?.try_into()?)
} else {
Err($crate::Error::HttpStatusNotSuccess{
status,
content_type,
body: raw_resp,
})
}
}
}
};
(json $(<$($a:lifetime),*>)? $(($ApiCall:ty))? $url:literal : $Input:ty as $JsonInput:ty => $JsonOutput:ty as $Output:ty) => {
$crate::json_api_call!(
json $(<$($a),*>)? $(($ApiCall))? POST $url: $Input as $JsonInput => $JsonOutput as $Output
);
};
(
ApiCall: $ApiCall:ident;
$(
$(
simple {$(
$(<$($a:lifetime),*>)? $($simple_method:ident)? $simple_url:literal : $SimpleInput:ty => $SimpleOutput:ty;
)*}
)?
$(
json {$(
$(<$($b:lifetime),*>)? $($json_method:ident)? $json_url:literal : $Input:ty as $JsonInput:ty => $JsonOutput:ty as $Output:ty;
)*}
)?
);*
) => {$(
$($(
$crate::json_api_call!(simple $(<$($a),*>)? ($ApiCall) $($simple_method)? $simple_url: $SimpleInput => $SimpleOutput);
)*)?
$($(
$crate::json_api_call!(json $(<$($b),*>)? ($ApiCall) $($json_method)? $json_url: $Input as $JsonInput => $JsonOutput as $Output);
)*)?
)*};
}
use transport::{ResponseBody, Transport};
pub struct Client<B, T: Transport<B>> {
pub transport: T,
pub phantom_body: PhantomData<B>,
pub endpoint: Cow<'static, str>,
pub authorization: Option<HeaderValue>,
pub extra_headers: HeaderMap,
}
#[cfg(feature = "hyper")]
impl Client<hyper::Body, transport::HttpsTransport> {
pub fn new_https_client(
endpoint: Cow<'static, str>,
) -> Client<hyper::Body, transport::HttpsTransport> {
Client {
transport: hyper::Client::builder().build(hyper_tls::HttpsConnector::new()),
phantom_body: PhantomData,
endpoint,
authorization: None,
extra_headers: HeaderMap::new(),
}
}
pub fn new_https_retry_client(
endpoint: Cow<'static, str>,
retry_config: transport::retry::RetryConfig,
) -> Client<Vec<u8>, transport::HttpsRetryTransport> {
Client {
transport: transport::RetryTransport::new(
hyper::Client::builder().build(hyper_tls::HttpsConnector::new()),
retry_config,
),
phantom_body: PhantomData,
endpoint,
authorization: None,
extra_headers: HeaderMap::new(),
}
}
}
impl<B, T: Transport<B>> Client<B, T> {
pub fn http_request_builder(&self) -> http::request::Builder {
let mut builder = http::Request::builder();
if let Some(authorization) = self.authorization.as_ref() {
builder = builder.header(header::AUTHORIZATION, authorization);
}
for (header, value) in self.extra_headers.iter() {
builder = builder.header(header, value);
}
builder
}
}
#[derive(Debug)]
pub enum Error {
Json(serde_json::Error),
#[cfg(feature = "hyper")]
Hyper(hyper::Error),
HttpStatusNotSuccess {
status: http::StatusCode,
content_type: Option<http::HeaderValue>,
body: Vec<u8>,
},
HttpNoBody,
Other(Cow<'static, str>),
}
macro_rules! error_from {
($(impl From<$type:ty> for $Error:ident :: $Variant:ident);* $(;)?) => {
$(
impl From<$type> for $Error {
fn from(err: $type) -> $Error {
$Error::$Variant(err)
}
}
)*
};
}
error_from!(
impl From<serde_json::Error> for Error::Json;
);
#[cfg(feature = "hyper")]
error_from!(
impl From<hyper::Error> for Error::Hyper;
);
impl From<std::convert::Infallible> for Error {
fn from(_: std::convert::Infallible) -> Error {
unreachable!()
}
}
pub trait SimpleApiCall: Serialize + Sized + Send {
type Output: for<'a> Deserialize<'a>;
fn method(&self) -> Method {
Method::POST
}
fn uri(&self, endpoint: &str) -> String;
fn modify_request(&self, request_builder: http::request::Builder) -> http::request::Builder {
request_builder
}
}
impl<T: SimpleApiCall> JsonApiCall for T {
type Output = <Self as SimpleApiCall>::Output;
type JsonResponse = <Self as SimpleApiCall>::Output;
type JsonRequest = Self;
fn method(&self) -> Method {
SimpleApiCall::method(self)
}
fn uri(&self, endpoint: &str) -> String {
SimpleApiCall::uri(self, endpoint)
}
fn modify_request(&self, request_builder: http::request::Builder) -> http::request::Builder {
SimpleApiCall::modify_request(self, request_builder)
}
fn try_into_request(self) -> Result<Self, Error> {
Ok(self)
}
fn parse_json_response(
status: StatusCode,
content_type: Option<http::HeaderValue>,
raw_resp: Vec<u8>,
resp: serde_json::Result<Self::Output>,
) -> Result<Self::Output, Error> {
if status.is_success() {
Ok(resp?)
} else {
Err(Error::HttpStatusNotSuccess {
status,
content_type,
body: raw_resp,
})
}
}
}
pub trait JsonApiCall: Sized + Send {
type Output;
type JsonResponse: for<'a> Deserialize<'a>;
type JsonRequest: Serialize;
fn method(&self) -> Method {
Method::POST
}
fn uri(&self, endpoint: &str) -> String;
fn modify_request(&self, request_builder: http::request::Builder) -> http::request::Builder {
request_builder
}
fn try_into_request(self) -> Result<Self::JsonRequest, Error>;
fn parse_json_response(
status: StatusCode,
content_type: Option<http::HeaderValue>,
raw_resp: Vec<u8>,
resp: serde_json::Result<Self::JsonResponse>,
) -> Result<Self::Output, Error>;
}
#[async_trait::async_trait]
impl<T: JsonApiCall> ApiCall for T {
type Output = <Self as JsonApiCall>::Output;
type RequestBody = Vec<u8>;
fn method(&self) -> Method {
JsonApiCall::method(self)
}
fn uri(&self, endpoint: &str) -> String {
JsonApiCall::uri(self, endpoint)
}
fn modify_request(&self, request_builder: http::request::Builder) -> http::request::Builder {
JsonApiCall::modify_request(
self,
request_builder.header(http::header::CONTENT_TYPE, "application/json"),
)
}
fn request_body(self) -> Result<Self::RequestBody, Error> {
let req = self.try_into_request()?;
Ok(serde_json::to_vec(&req)?)
}
async fn response<B: ResponseBody>(resp: http::Response<B>) -> Result<Self::Output, Error> {
let status = resp.status();
let content_type = resp.headers().get("Content-Type").cloned();
let body_bytes = resp
.into_body()
.read_all()
.await
.map_err(|err| err.into())?;
let resp: serde_json::Result<<Self as JsonApiCall>::JsonResponse> =
serde_json::from_slice(body_bytes.as_ref());
<Self as JsonApiCall>::parse_json_response(status, content_type, body_bytes.into(), resp)
}
}
#[async_trait::async_trait]
pub trait ApiCall: Sized + Send {
type RequestBody;
type Output;
fn method(&self) -> Method {
Method::POST
}
fn uri(&self, endpoint: &str) -> String;
fn modify_request(&self, request_builder: http::request::Builder) -> http::request::Builder {
request_builder
}
fn request_body(self) -> Result<Self::RequestBody, Error>;
async fn response<B: ResponseBody>(resp: http::Response<B>) -> Result<Self::Output, Error>;
}
#[async_trait::async_trait]
impl<C: ApiCall> RawApiCall for C {
type Output = <C as ApiCall>::Output;
type RequestBody = <C as ApiCall>::RequestBody;
async fn request<B: From<Self::RequestBody> + Send, T: Transport<B>>(
self,
client: &Client<B, T>,
) -> Result<Self::Output, Error>
where
B: From<Self::RequestBody> + Sync,
{
let method = self.method();
let uri = self.uri(&client.endpoint);
let request = self
.modify_request(client.http_request_builder().method(method).uri(uri))
.body::<B>(self.request_body()?.into())
.unwrap();
let resp = client
.transport
.request(request)
.await
.map_err(|err| err.into())?;
Self::response(resp).await
}
}
#[async_trait::async_trait]
pub trait RawApiCall: Sized {
type RequestBody;
type Output;
async fn request<B: From<Self::RequestBody> + Send, T: Transport<B>>(
self,
client: &Client<B, T>,
) -> Result<Self::Output, Error>
where
B: From<Self::RequestBody> + Sync;
}
#[cfg(test)]
mod test;