pub mod client {
use http::{Method, Uri};
use crate::{
backend::{Backend, MockBackend},
config::ClientConfig,
error::Error,
request::Request,
response::Response,
};
#[derive(Clone, Debug)]
pub struct Client<B = MockBackend> {
backend: B,
config: ClientConfig,
}
impl Client {
pub fn new() -> Self {
Self::with_backend(MockBackend::default())
}
}
impl<B> Client<B>
where
B: Backend + Clone,
{
pub fn with_backend(backend: B) -> Self {
Self {
backend,
config: ClientConfig::default(),
}
}
pub fn config(&self) -> &ClientConfig {
&self.config
}
pub fn config_mut(&mut self) -> &mut ClientConfig {
&mut self.config
}
pub async fn send(&self, request: Request) -> Result<Response, Error> {
self.backend.execute(request, &self.config).await
}
pub async fn get(&self, uri: Uri) -> Result<Response, Error> {
let request = Request::new(Method::GET, uri);
self.send(request).await
}
}
}
pub mod request {
use http::{HeaderMap, Method, Uri};
#[derive(Debug, Clone)]
pub struct Request {
pub method: Method,
pub uri: Uri,
pub headers: HeaderMap,
pub body: Vec<u8>,
}
impl Request {
pub fn new(method: Method, uri: Uri) -> Self {
Self {
method,
uri,
headers: HeaderMap::new(),
body: Vec::new(),
}
}
pub fn with_body(method: Method, uri: Uri, body: Vec<u8>) -> Self {
Self {
method,
uri,
headers: HeaderMap::new(),
body,
}
}
}
}
pub mod response {
use http::{HeaderMap, StatusCode};
#[derive(Debug, Clone)]
pub struct Response {
pub status: StatusCode,
pub headers: HeaderMap,
pub body: Vec<u8>,
}
impl Response {
pub fn new(status: StatusCode, headers: HeaderMap, body: Vec<u8>) -> Self {
Self { status, headers, body }
}
pub fn is_success(&self) -> bool {
self.status.is_success()
}
}
}
pub mod backend {
use async_trait::async_trait;
use http::{HeaderMap, StatusCode};
use std::fmt;
use hyper::{
body::{to_bytes, Body as HyperBody},
client::HttpConnector,
Client as HyperClient,
};
use crate::{config::ClientConfig, error::Error, request::Request, response::Response};
#[async_trait]
pub trait Backend: Send + Sync {
async fn execute(&self, request: Request, config: &ClientConfig) -> Result<Response, Error>;
}
#[derive(Debug, Clone, Default)]
pub struct MockBackend;
#[async_trait]
impl Backend for MockBackend {
async fn execute(&self, _request: Request, _config: &ClientConfig) -> Result<Response, Error> {
Ok(Response::new(
StatusCode::NOT_IMPLEMENTED,
HeaderMap::new(),
Vec::new(),
))
}
}
pub struct HyperBackend {
client: HyperClient<HttpConnector, HyperBody>,
}
impl HyperBackend {
pub fn new() -> Self {
let client = HyperClient::new();
Self { client }
}
}
impl Clone for HyperBackend {
fn clone(&self) -> Self {
Self {
client: self.client.clone(),
}
}
}
impl fmt::Debug for HyperBackend {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("HyperBackend").finish()
}
}
#[async_trait]
impl Backend for HyperBackend {
async fn execute(&self, request: Request, _config: &ClientConfig) -> Result<Response, Error> {
let mut req = http::Request::new(HyperBody::from(request.body));
*req.method_mut() = request.method;
*req.uri_mut() = request.uri;
*req.headers_mut() = request.headers;
let res = self
.client
.request(req)
.await
.map_err(|e| Error::Transport(e.to_string()))?;
let status = res.status();
let headers = res.headers().clone();
let body_bytes = to_bytes(res.into_body())
.await
.map_err(|e| Error::Transport(e.to_string()))?
.to_vec();
Ok(Response::new(status, headers, body_bytes))
}
}
}
pub type MockClient = client::Client<backend::MockBackend>;
pub type HyperClient = client::Client<backend::HyperBackend>;
pub mod error {
use thiserror::Error;
#[derive(Debug, Error)]
pub enum Error {
#[error("invalid request: {0}")]
InvalidRequest(String),
#[error("transport error: {0}")]
Transport(String),
#[error("timeout")]
Timeout,
#[error("configuration error: {0}")]
Config(String),
#[error("unexpected error: {0}")]
Other(String),
}
}
pub mod config {
use std::time::Duration;
#[derive(Debug, Clone)]
pub struct ClientConfig {
pub connect_timeout: Option<Duration>,
pub request_timeout: Option<Duration>,
pub max_redirects: u32,
}
impl Default for ClientConfig {
fn default() -> Self {
Self {
connect_timeout: Some(Duration::from_secs(10)),
request_timeout: Some(Duration::from_secs(30)),
max_redirects: 5,
}
}
}
}
#[cfg(test)]
mod tests {
use super::client::Client;
use http::Uri;
#[tokio::test]
async fn client_get_uses_mock_backend() {
let client = Client::new();
let uri: Uri = "http://example.com".parse().expect("valid URI");
let response = client.get(uri).await.expect("request should succeed");
assert_eq!(response.status.as_u16(), 501);
}
}