-
Notifications
You must be signed in to change notification settings - Fork 42
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #127 from Berrysoft/dev/http-client
feat: HTTP client
- Loading branch information
Showing
32 changed files
with
1,911 additions
and
89 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>; |
Oops, something went wrong.