feat: add Config for surf

Finally adds top-level configuration to surf, built on top of http-client's
newly stabilized `Config`.

Related to https://github.com/http-rs/http-client/pull/86

Closes https://github.com/http-rs/surf/issues/274
Closes https://github.com/http-rs/surf/issues/277
This commit is contained in:
Jeremiah Senkpiel 2021-07-05 15:08:20 -07:00
parent 1ffaba8873
commit 6ca7ad8d55
7 changed files with 311 additions and 12 deletions

View File

@ -56,7 +56,7 @@ log = { version = "0.4.7", features = ["kv_unstable"] }
mime_guess = "2.0.3"
serde = "1.0.97"
serde_json = "1.0.40"
http-client = { version = "6.3.4", default-features = false }
http-client = { version = "6.5.0", default-features = false }
http-types = "2.5.0"
async-std = { version = "1.6.0", default-features = false, features = ["std"] }
async-trait = "0.1.36"
@ -74,6 +74,7 @@ async-std = { version = "1.6.0", features = ["attributes"] }
femme = "1.1.0"
serde = { version = "1.0.97", features = ["derive"] }
mockito = "0.23.3"
tide = "0.16.0"
[workspace]
members = ["wasm-test"]

View File

@ -54,6 +54,7 @@ quick script, or a cross-platform SDK, Surf will make it work.
- Multi-platform out of the box
- Extensible through a powerful middleware system
- Reuses connections through the `Client` interface
- Has `Client` configurability
- Fully streaming requests and responses
- TLS/SSL enabled by default
- Swappable HTTP backends
@ -118,6 +119,26 @@ async fn main() -> surf::Result<()> {
}
```
Setting configuration on a client is also straightforward.
```rust
# #[async_std::main]
# async fn main() -> surf::Result<()> {
use std::convert::TryInto;
use std::time::Duration;
use surf::{Client, Config};
use surf::Url;
let client: Client = Config::new()
.set_base_url(Url::parse("http://example.org")?)
.set_timeout(Some(Duration::from_secs(5)))
.try_into()?;
let mut res = client.get("/").await?;
println!("{}", res.body_string().await?);
# Ok(()) }
```
## Installation
Install OpenSSL -

View File

@ -1,9 +1,10 @@
use std::convert::TryFrom;
use std::fmt;
use std::sync::Arc;
use crate::http::{Method, Url};
use crate::middleware::{Middleware, Next};
use crate::{HttpClient, Request, RequestBuilder, Response, Result};
use crate::{Config, HttpClient, Request, RequestBuilder, Response, Result};
use cfg_if::cfg_if;
@ -41,7 +42,7 @@ cfg_if! {
/// # Ok(()) }
/// ```
pub struct Client {
base_url: Option<Url>,
config: Config,
http_client: Arc<dyn HttpClient>,
/// Holds the middleware stack.
///
@ -58,12 +59,12 @@ impl Clone for Client {
/// Clones the Client.
///
/// This copies the middleware stack from the original, but shares
/// the `HttpClient` of the original.
/// the `HttpClient` and http client config of the original.
/// Note that individual middleware in the middleware stack are
/// still shared by reference.
fn clone(&self) -> Self {
Self {
base_url: self.base_url.clone(),
config: self.config.clone(),
http_client: self.http_client.clone(),
middleware: Arc::new(self.middleware.iter().cloned().collect()),
}
@ -129,7 +130,7 @@ impl Client {
fn with_http_client_internal(http_client: Arc<dyn HttpClient>) -> Self {
let client = Self {
base_url: None,
config: Config::default(),
http_client,
middleware: Arc::new(vec![]),
};
@ -212,7 +213,7 @@ impl Client {
});
let client = Self {
base_url: self.base_url.clone(),
config: self.config.clone(),
http_client,
// Erase the middleware stack for the Client accessible from within middleware.
// This avoids gratuitous circular borrow & logic issues.
@ -562,28 +563,61 @@ impl Client {
/// client.get("posts.json").recv_json().await?; /// http://example.com/api/v1/posts.json
/// # Ok(()) }) }
/// ```
#[deprecated(since = "6.5.0", note = "Please use `Config` instead")]
pub fn set_base_url(&mut self, base: Url) {
self.base_url = Some(base);
self.config.base_url = Some(base);
}
/// Get the current configuration.
pub fn config(&self) -> &Config {
&self.config
}
// private function to generate a url based on the base_path
fn url(&self, uri: impl AsRef<str>) -> Url {
match &self.base_url {
match &self.config.base_url {
None => uri.as_ref().parse().unwrap(),
Some(base) => base.join(uri.as_ref()).unwrap(),
}
}
}
impl TryFrom<Config> for Client {
#[cfg(feature = "default-client")]
type Error = <DefaultClient as TryFrom<http_client::Config>>::Error;
#[cfg(not(feature = "default-client"))]
type Error = std::convert::Infallible;
fn try_from(mut config: Config) -> std::result::Result<Self, Self::Error> {
let http_client = match config.http_client.take() {
Some(client) => client,
#[cfg(feature = "default-client")]
None => Arc::new(DefaultClient::try_from(config.http_config.clone())?),
#[cfg(not(feature = "default-client"))]
None => panic!("Config without an http client provided to Surf configured without a default client.")
};
Ok(Client {
config,
http_client,
middleware: Arc::new(vec![]),
})
}
}
#[cfg(test)]
mod client_tests {
use std::convert::TryInto;
use super::Client;
use super::Config;
use crate::Url;
#[test]
fn base_url() {
let mut client = Client::new();
client.set_base_url(Url::parse("http://example.com/api/v1/").unwrap());
let base_url = Url::parse("http://example.com/api/v1/").unwrap();
let client: Client = Config::new().set_base_url(base_url).try_into().unwrap();
let url = client.url("posts.json");
assert_eq!(url.as_str(), "http://example.com/api/v1/posts.json");
}

185
src/config.rs Normal file
View File

@ -0,0 +1,185 @@
//! Configuration for `HttpClient`s.
use std::sync::Arc;
use std::{collections::HashMap, fmt::Debug, time::Duration};
use http_client::{Config as HttpConfig, HttpClient};
use http_types::headers::{HeaderName, HeaderValues, ToHeaderValues};
use crate::http::Url;
use crate::Result;
/// Configuration for `surf::Client`s and their underlying HTTP clients.
///
/// ```
/// use std::convert::TryInto;
/// use surf::{Client, Config, Url};
///
/// # #[async_std::main]
/// # async fn main() -> surf::Result<()> {
/// let client: Client = Config::new()
/// .set_base_url(Url::parse("https://example.org")?)
/// .try_into()?;
///
/// let mut response = client.get("/").await?;
///
/// println!("{}", response.body_string().await?);
/// # Ok(())
/// # }
/// ```
#[non_exhaustive]
#[derive(Clone, Debug)]
pub struct Config {
/// The base URL for a client. All request URLs will be relative to this URL.
///
/// Note: a trailing slash is significant.
/// Without it, the last path component is considered to be a “file” name
/// to be removed to get at the “directory” that is used as the base.
pub base_url: Option<Url>,
/// Headers to be applied to every request made by this client.
pub headers: HashMap<HeaderName, HeaderValues>,
/// Underlying HTTP client config.
pub http_config: HttpConfig,
/// Optional custom http client.
pub http_client: Option<Arc<dyn HttpClient>>,
}
impl Config {
/// Construct new empty config.
pub fn new() -> Self {
Self::default()
}
}
impl Default for Config {
fn default() -> Self {
HttpConfig::default().into()
}
}
impl Config {
/// Sets the base URL for this client. All request URLs will be relative to this URL.
///
/// Note: a trailing slash is significant.
/// Without it, the last path component is considered to be a “file” name
/// to be removed to get at the “directory” that is used as the base.
///
/// ```
/// use std::convert::TryInto;
/// use surf::{Client, Config, Url};
///
/// # fn main() -> surf::Result<()> {
/// let client: Client = Config::new()
/// .set_base_url(Url::parse("https://example.org")?)
/// .try_into()?;
/// # Ok(())
/// # }
/// ```
pub fn set_base_url(mut self, base: Url) -> Self {
self.base_url = Some(base);
self
}
/// Adds a header to be added to every request by this client.
///
/// ```
/// use std::convert::TryInto;
/// use surf::{Client, Config};
/// use surf::http::auth::BasicAuth;
///
/// # fn main() -> surf::Result<()> {
/// let auth = BasicAuth::new("Username", "Password");
///
/// let client: Client = Config::new()
/// .add_header(auth.name(), auth.value())?
/// .try_into()?;
/// # Ok(())
/// # }
/// ```
pub fn add_header(
mut self,
name: impl Into<HeaderName>,
values: impl ToHeaderValues,
) -> Result<Self> {
self.headers
.insert(name.into(), values.to_header_values()?.collect());
Ok(self)
}
/// Set HTTP/1.1 `keep-alive` (connection pooling).
pub fn set_http_keep_alive(mut self, keep_alive: bool) -> Self {
self.http_config.http_keep_alive = keep_alive;
self
}
/// Set TCP `NO_DELAY`.
pub fn set_tcp_no_delay(mut self, no_delay: bool) -> Self {
self.http_config.tcp_no_delay = no_delay;
self
}
/// Set connection timeout duration.
///
/// ```
/// use std::convert::TryInto;
/// use std::time::Duration;
/// use surf::{Client, Config};
///
/// # fn main() -> surf::Result<()> {
/// let client: Client = Config::new()
/// .set_timeout(Some(Duration::from_secs(5)))
/// .try_into()?;
/// # Ok(())
/// # }
/// ```
pub fn set_timeout(mut self, timeout: Option<Duration>) -> Self {
self.http_config.timeout = timeout;
self
}
/// Set the maximum number of simultaneous connections that this client is allowed to keep open to individual hosts at one time.
pub fn set_max_connections_per_host(mut self, max_connections_per_host: usize) -> Self {
self.http_config.max_connections_per_host = max_connections_per_host;
self
}
/// Override the http client entirely.
///
/// When using this, any underlying `http_client::Config` http configuration will be ignored.
///
/// ```
/// use std::convert::TryInto;
/// use surf::{Client, Config};
///
/// # fn main() -> surf::Result<()> {
/// // Connect directly to a Tide server, e.g. for testing.
/// let server = tide::new();
///
/// let client: Client = Config::new()
/// .set_http_client(server)
/// .try_into()?;
/// # Ok(())
/// # }
/// ```
pub fn set_http_client(mut self, http_client: impl HttpClient) -> Self {
self.http_client = Some(Arc::new(http_client));
self
}
}
impl AsRef<HttpConfig> for Config {
fn as_ref(&self) -> &HttpConfig {
&self.http_config
}
}
impl From<HttpConfig> for Config {
fn from(http_config: HttpConfig) -> Self {
Self {
base_url: None,
headers: HashMap::new(),
http_config,
http_client: None,
}
}
}

View File

@ -7,6 +7,7 @@
//! - Multi-platform out of the box
//! - Extensible through a powerful middleware system
//! - Reuses connections through the `Client` interface
//! - Has `Client` configurability
//! - Fully streaming requests and responses
//! - TLS/SSL enabled by default
//! - Swappable HTTP backends
@ -61,6 +62,26 @@
//! # Ok(()) }
//! ```
//!
//! Setting configuration on a client is also straightforward.
//!
//! ```no_run
//! # #[async_std::main]
//! # async fn main() -> surf::Result<()> {
//! use std::convert::TryInto;
//! use std::time::Duration;
//! use surf::{Client, Config};
//! use surf::Url;
//!
//! let client: Client = Config::new()
//! .set_base_url(Url::parse("http://example.org")?)
//! .set_timeout(Some(Duration::from_secs(5)))
//! .try_into()?;
//!
//! let mut res = client.get("/").await?;
//! println!("{}", res.body_string().await?);
//! # Ok(()) }
//! ```
//!
//! # Features
//! The following features are available. The default features are
//! `curl-client`, `middleware-logger`, and `encoding`
@ -80,6 +101,7 @@
#![doc(html_logo_url = "https://yoshuawuyts.com/assets/http-rs/logo-rounded.png")]
mod client;
mod config;
mod request;
mod request_builder;
mod response;
@ -92,6 +114,7 @@ pub use http_types::{self as http, Body, Error, Status, StatusCode, Url};
pub use http_client::HttpClient;
pub use client::Client;
pub use config::Config;
pub use request::Request;
pub use request_builder::RequestBuilder;
pub use response::{DecodeError, Response};

View File

@ -86,6 +86,12 @@ impl RequestBuilder {
}
pub(crate) fn with_client(mut self, client: Client) -> Self {
let req = self.req.as_mut().unwrap();
for (header_name, header_values) in client.config().headers.iter() {
req.append_header(header_name, header_values);
}
self.client = Some(client);
self
}

View File

@ -1,9 +1,11 @@
use std::convert::TryInto;
use mockito::mock;
use futures_util::future::BoxFuture;
use http_types::Body;
use surf::{middleware::Next, Client, Request, Response};
use surf::{middleware::Next, Client, Config, Request, Response};
#[async_std::test]
async fn post_json() -> Result<(), http_types::Error> {
@ -169,3 +171,30 @@ fn mw_2(
Ok(res)
})
}
#[async_std::test]
async fn config_client_headers() -> Result<(), http_types::Error> {
femme::start(log::LevelFilter::Trace).ok();
let mut server = tide::new();
server.at("/").get(|req: tide::Request<()>| async {
let mut res = tide::Response::new(200);
for (header_name, header_values) in req {
res.append_header(header_name, &header_values);
}
Ok(res)
});
let client: Client = Config::new()
.set_http_client(server)
.add_header("X-Header-Name", "X-Header-Values")?
.try_into()?;
let res = client.get("http://example.org/").await?;
assert_eq!(res["X-Header-Name"], "X-Header-Values");
Ok(())
}