Skip to content

Commit

Permalink
Merge pull request #127 from Berrysoft/dev/http-client
Browse files Browse the repository at this point in the history
feat: HTTP client
  • Loading branch information
Berrysoft authored Nov 11, 2023
2 parents e8408d4 + e63749c commit 368fddb
Show file tree
Hide file tree
Showing 32 changed files with 1,911 additions and 89 deletions.
13 changes: 13 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ members = [
"compio-io",
"compio-tls",
"compio-log",
"compio-http",
"compio-http-client",
]
resolver = "2"

Expand All @@ -32,20 +34,31 @@ compio-io = { path = "./compio-io", version = "0.1.0" }
compio-net = { path = "./compio-net", version = "0.2.0" }
compio-signal = { path = "./compio-signal", version = "0.1.1" }
compio-dispatcher = { path = "./compio-dispatcher", version = "0.1.0" }
compio-http = { path = "./compio-http", version = "0.1.0" }
compio-log = { path = "./compio-log", version = "0.1.0" }
compio-tls = { path = "./compio-tls", version = "0.1.0" }


cfg-if = "1.0.0"
crossbeam-channel = "0.5.8"
crossbeam-queue = "0.3.8"
futures-channel = "0.3.29"
futures-util = "0.3.29"
libc = "0.2.149"
nix = "0.27.1"
once_cell = "1.18.0"
os_pipe = "1.1.4"
paste = "1.0.14"
send_wrapper = "0.6"
slab = "0.4.9"
socket2 = "0.5.5"
tempfile = "3.8.1"
tokio = "1.33.0"
widestring = "1.0.2"
windows-sys = "0.48.0"

native-tls = "0.2.11"
rustls = "0.21.8"
rustls-native-certs = "0.6.3"

hyper = "0.14"
2 changes: 1 addition & 1 deletion compio-buf/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
#![cfg_attr(feature = "allocator_api", feature(allocator_api))]
#![cfg_attr(feature = "read_buf", feature(read_buf))]
#![cfg_attr(feature = "read_buf", feature(read_buf, core_io_borrowed_buf))]
#![cfg_attr(feature = "try_trait_v2", feature(try_trait_v2, try_trait_v2_residual))]
#![warn(missing_docs)]

Expand Down
51 changes: 51 additions & 0 deletions compio-http-client/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
[package]
name = "compio-http-client"
version = "0.1.0"
categories = ["asynchronous", "network-programming"]
keywords = ["async", "net"]
edition = { workspace = true }
authors = { workspace = true }
readme = { workspace = true }
license = { workspace = true }
repository = { workspace = true }

[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg docsrs"]

[dependencies]
compio-buf = { workspace = true, features = ["bytes"] }
compio-runtime = { workspace = true, features = ["time"] }
compio-tls = { workspace = true }
compio-http = { workspace = true, features = ["client"] }

http = "0.2"
hyper = { workspace = true, features = ["client", "http1"] }

base64 = "0.21"
encoding_rs = "0.8"
mime = "0.3"
serde = "1"
serde_json = { version = "1", optional = true }
serde_urlencoded = "0.7"
thiserror = "1"
url = "2"

[dev-dependencies]
compio-net = { workspace = true, features = ["runtime"] }
compio-macros = { workspace = true }
compio-http = { workspace = true, features = ["server"] }

futures-channel = { workspace = true }
hyper = { workspace = true, features = ["server"] }

[features]
default = ["native-tls"]
native-tls = ["compio-http/native-tls"]
rustls = ["compio-http/rustls"]
vendored = ["compio-http/vendored"]
json = ["dep:serde_json"]
all = ["json", "native-tls", "rustls", "compio-http/all"]

read_buf = ["compio-http/read_buf"]
nightly = ["read_buf"]
8 changes: 8 additions & 0 deletions compio-http-client/examples/client.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
use compio_http_client::Client;

#[compio_macros::main]
async fn main() {
let client = Client::new();
let response = client.get("https://www.example.com/").send().await.unwrap();
println!("{}", response.text().await.unwrap());
}
159 changes: 159 additions & 0 deletions compio-http-client/src/client.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
use std::rc::Rc;

use compio_http::{CompioExecutor, Connector, TlsBackend};
use hyper::{Body, HeaderMap, Method, Uri};

use crate::{IntoUrl, Request, RequestBuilder, Response, Result};

/// An asynchronous `Client` to make Requests with.
#[derive(Debug, Clone)]
pub struct Client {
client: Rc<ClientInner>,
}

impl Client {
/// Create a client with default config.
#[allow(clippy::new_without_default)]
pub fn new() -> Self {
ClientBuilder::new().build()
}

/// Creates a `ClientBuilder` to configure a `Client`.
///
/// This is the same as `ClientBuilder::new()`.
pub fn builder() -> ClientBuilder {
ClientBuilder::new()
}

/// Send a request and wait for a response.
pub async fn execute(&self, request: Request) -> Result<Response> {
let (method, url, headers, body, timeout, version) = request.pieces();
let mut request = hyper::Request::builder()
.method(method)
.uri(
url.as_str()
.parse::<Uri>()
.expect("a parsed Url should always be a valid Uri"),
)
.version(version)
.body(body.unwrap_or_else(Body::empty))?;
*request.headers_mut() = self.client.headers.clone();
crate::util::replace_headers(request.headers_mut(), headers);

let future = self.client.client.request(request);
let res = if let Some(timeout) = timeout {
compio_runtime::time::timeout(timeout, future)
.await
.map_err(|_| crate::Error::Timeout)??
} else {
future.await?
};
Ok(Response::new(res, url))
}

/// Send a request with method and url.
pub fn request<U: IntoUrl>(&self, method: Method, url: U) -> RequestBuilder {
RequestBuilder::new(
self.clone(),
url.into_url().map(|url| Request::new(method, url)),
)
}

/// Convenience method to make a `GET` request to a URL.
pub fn get<U: IntoUrl>(&self, url: U) -> RequestBuilder {
self.request(Method::GET, url)
}

/// Convenience method to make a `POST` request to a URL.
pub fn post<U: IntoUrl>(&self, url: U) -> RequestBuilder {
self.request(Method::POST, url)
}

/// Convenience method to make a `PUT` request to a URL.
pub fn put<U: IntoUrl>(&self, url: U) -> RequestBuilder {
self.request(Method::PUT, url)
}

/// Convenience method to make a `PATCH` request to a URL.
pub fn patch<U: IntoUrl>(&self, url: U) -> RequestBuilder {
self.request(Method::PATCH, url)
}

/// Convenience method to make a `DELETE` request to a URL.
pub fn delete<U: IntoUrl>(&self, url: U) -> RequestBuilder {
self.request(Method::DELETE, url)
}

/// Convenience method to make a `HEAD` request to a URL.
pub fn head<U: IntoUrl>(&self, url: U) -> RequestBuilder {
self.request(Method::HEAD, url)
}
}

#[derive(Debug)]
struct ClientInner {
client: hyper::Client<Connector, Body>,
headers: HeaderMap,
}

/// A `ClientBuilder` can be used to create a `Client` with custom
/// configuration.
#[derive(Debug)]
#[must_use]
pub struct ClientBuilder {
headers: HeaderMap,
tls: TlsBackend,
}

impl Default for ClientBuilder {
fn default() -> Self {
Self::new()
}
}

impl ClientBuilder {
/// Constructs a new `ClientBuilder`.
pub fn new() -> Self {
Self {
headers: HeaderMap::new(),
tls: TlsBackend::default(),
}
}

/// Returns a `Client` that uses this `ClientBuilder` configuration.
pub fn build(self) -> Client {
let client = hyper::Client::builder()
.executor(CompioExecutor)
.set_host(true)
.build(Connector::new(self.tls));
let client_ref = ClientInner {
client,
headers: self.headers,
};
Client {
client: Rc::new(client_ref),
}
}

/// Set the default headers for every request.
pub fn default_headers(mut self, headers: HeaderMap) -> Self {
for (key, value) in headers.iter() {
self.headers.insert(key, value.clone());
}
self
}

/// Force using the native TLS backend.
#[cfg(feature = "native-tls")]
pub fn use_native_tls(mut self) -> Self {
self.tls = TlsBackend::NativeTls;
self
}

/// Force using the Rustls TLS backend.
#[cfg(feature = "rustls")]
pub fn use_rustls(mut self) -> Self {
self.tls = TlsBackend::Rustls;
self
}
}
36 changes: 36 additions & 0 deletions compio-http-client/src/into_url.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
use url::Url;

/// A trait to try to convert some type into a [`Url`].
pub trait IntoUrl {
/// Besides parsing as a valid [`Url`], the [`Url`] must be a valid
/// `http::Uri`, in that it makes sense to use in a network request.
fn into_url(self) -> crate::Result<Url>;
}

impl IntoUrl for Url {
fn into_url(self) -> crate::Result<Url> {
if self.has_host() {
Ok(self)
} else {
Err(crate::Error::BadScheme(self))
}
}
}

impl<'a> IntoUrl for &'a str {
fn into_url(self) -> crate::Result<Url> {
Ok(Url::parse(self)?)
}
}

impl<'a> IntoUrl for &'a String {
fn into_url(self) -> crate::Result<Url> {
(&**self).into_url()
}
}

impl IntoUrl for String {
fn into_url(self) -> crate::Result<Url> {
(&*self).into_url()
}
}
52 changes: 52 additions & 0 deletions compio-http-client/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
//! A high level HTTP client based on compio.

#![warn(missing_docs)]
#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]

mod client;
pub use client::*;

mod request;
pub use request::*;

mod response;
pub use response::*;

mod into_url;
pub use into_url::*;

mod util;

/// The error type used in `compio-http`.
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
/// The request is timeout.
#[error("request timeout")]
Timeout,
/// Bad scheme.
#[error("bad scheme: {0}")]
BadScheme(url::Url),
/// IO error occurs.
#[error("system error: {0}")]
System(#[from] std::io::Error),
/// HTTP related parse error.
#[error("`http` error: {0}")]
Http(#[from] http::Error),
/// Hyper error.
#[error("`hyper` error: {0}")]
Hyper(#[from] hyper::Error),
/// URL parse error.
#[error("url parse error: {0}")]
UrlParse(#[from] url::ParseError),
/// URL encoding error.
#[error("url encode error: {0}")]
UrlEncoded(#[from] serde_urlencoded::ser::Error),
/// JSON serialization error.
#[cfg(feature = "json")]
#[error("json error: {0}")]
Json(#[from] serde_json::Error),
}

/// The result type used in `compio-http`.
pub type Result<T> = std::result::Result<T, Error>;
Loading

0 comments on commit 368fddb

Please sign in to comment.