mirror of https://github.com/http-rs/surf
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:
parent
1ffaba8873
commit
6ca7ad8d55
|
@ -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"]
|
||||
|
|
21
README.md
21
README.md
|
@ -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 -
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
23
src/lib.rs
23
src/lib.rs
|
@ -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};
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue