use reqwest::Client;
use std::marker::PhantomData;
use std::sync::Arc;
use std::time::Duration;
use crate::{GooseConfiguration, GooseError};
pub struct CookiesEnabled;
pub struct CookiesDisabled;
#[derive(Clone, Debug)]
pub struct GooseClientConfig {
pub timeout: Option<Duration>,
pub user_agent: String,
pub gzip: bool,
pub accept_invalid_certs: bool,
pub cookies_enabled: bool,
}
impl Default for GooseClientConfig {
fn default() -> Self {
Self {
timeout: Some(Duration::from_millis(60_000)),
user_agent: concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")).to_string(),
gzip: true,
accept_invalid_certs: false,
cookies_enabled: true,
}
}
}
impl From<&GooseConfiguration> for GooseClientConfig {
fn from(config: &GooseConfiguration) -> Self {
Self {
timeout: Some(config.request_timeout()),
user_agent: concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")).to_string(),
gzip: !config.no_gzip,
accept_invalid_certs: config.accept_invalid_certs,
cookies_enabled: true, }
}
}
#[cfg(test)]
thread_local! {
pub(crate) static SHARED_CLIENT_BUILDS: std::cell::Cell<usize> =
const { std::cell::Cell::new(0) };
}
impl GooseClientConfig {
pub(crate) fn into_strategy(self) -> Result<ClientStrategy, GooseError> {
if self.cookies_enabled {
Ok(ClientStrategy::Individual(self))
} else {
let client =
create_reqwest_client_without_cookies(&self).map_err(GooseError::Reqwest)?;
#[cfg(test)]
SHARED_CLIENT_BUILDS.with(|builds| builds.set(builds.get() + 1));
Ok(ClientStrategy::Shared(Arc::new(client)))
}
}
}
#[derive(Clone, Copy, Debug, Default)]
pub(crate) struct CommandLineClientOptions {
timeout: bool,
no_gzip: bool,
accept_invalid_certs: bool,
}
impl CommandLineClientOptions {
pub(crate) fn from_command_line(configuration: &GooseConfiguration) -> Self {
Self {
timeout: configuration.timeout.is_some(),
no_gzip: configuration.no_gzip,
accept_invalid_certs: configuration.accept_invalid_certs,
}
}
}
#[derive(Clone, Copy, Debug, Default)]
struct ExplicitSettings {
timeout: bool,
gzip: bool,
accept_invalid_certs: bool,
}
#[derive(Clone, Debug)]
pub(crate) struct ClientBuilderSettings {
config: GooseClientConfig,
explicit: ExplicitSettings,
}
impl ClientBuilderSettings {
pub(crate) fn resolve(
&self,
command_line: CommandLineClientOptions,
configuration: &GooseConfiguration,
) -> GooseClientConfig {
let mut config = GooseClientConfig {
user_agent: self.config.user_agent.clone(),
cookies_enabled: self.config.cookies_enabled,
..GooseClientConfig::from(configuration)
};
if self.explicit.timeout && !command_line.timeout {
config.timeout = self.config.timeout;
}
if self.explicit.gzip && !command_line.no_gzip {
config.gzip = self.config.gzip;
}
if self.explicit.accept_invalid_certs && !command_line.accept_invalid_certs {
config.accept_invalid_certs = self.config.accept_invalid_certs;
}
config
}
}
#[derive(Clone, Debug)]
pub enum ClientStrategy {
Individual(GooseClientConfig),
Shared(Arc<Client>),
}
impl ClientStrategy {
pub(crate) fn user_client(&self) -> Result<Client, reqwest::Error> {
match self {
ClientStrategy::Individual(config) => create_reqwest_client_with_cookies(config),
ClientStrategy::Shared(shared) => Ok(Client::clone(shared)),
}
}
}
pub struct GooseClientBuilder<State = CookiesEnabled> {
config: GooseClientConfig,
explicit: ExplicitSettings,
_state: PhantomData<State>,
}
impl GooseClientBuilder<CookiesEnabled> {
pub fn new() -> Self {
Self {
config: GooseClientConfig::default(),
explicit: ExplicitSettings::default(),
_state: PhantomData,
}
}
pub fn from_configuration(config: &GooseConfiguration) -> Self {
Self {
config: GooseClientConfig::from(config),
explicit: ExplicitSettings::default(),
_state: PhantomData,
}
}
pub fn without_cookies(self) -> GooseClientBuilder<CookiesDisabled> {
GooseClientBuilder {
config: GooseClientConfig {
cookies_enabled: false,
..self.config
},
explicit: self.explicit,
_state: PhantomData,
}
}
pub fn build_strategy(self) -> ClientStrategy {
ClientStrategy::Individual(self.config)
}
}
impl GooseClientBuilder<CookiesDisabled> {
pub fn with_cookies(self) -> GooseClientBuilder<CookiesEnabled> {
GooseClientBuilder {
config: GooseClientConfig {
cookies_enabled: true,
..self.config
},
explicit: self.explicit,
_state: PhantomData,
}
}
pub fn build_strategy(self) -> Result<ClientStrategy, GooseError> {
self.config.into_strategy()
}
}
impl<State> GooseClientBuilder<State> {
pub(crate) fn into_settings(self) -> ClientBuilderSettings {
ClientBuilderSettings {
config: self.config,
explicit: self.explicit,
}
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.config.timeout = Some(timeout);
self.explicit.timeout = true;
self
}
pub fn user_agent<T: Into<String>>(mut self, user_agent: T) -> Self {
self.config.user_agent = user_agent.into();
self
}
pub fn gzip(mut self, enabled: bool) -> Self {
self.config.gzip = enabled;
self.explicit.gzip = true;
self
}
pub fn accept_invalid_certs(mut self, enabled: bool) -> Self {
self.config.accept_invalid_certs = enabled;
self.explicit.accept_invalid_certs = true;
self
}
}
impl Default for GooseClientBuilder<CookiesEnabled> {
fn default() -> Self {
Self::new()
}
}
pub(crate) fn create_reqwest_client_without_cookies(
config: &GooseClientConfig,
) -> Result<Client, reqwest::Error> {
let mut client_builder = Client::builder()
.user_agent(&config.user_agent)
.gzip(config.gzip)
.danger_accept_invalid_certs(config.accept_invalid_certs);
if let Some(timeout) = config.timeout {
client_builder = client_builder.timeout(timeout);
}
client_builder.build()
}
pub(crate) fn create_reqwest_client_with_cookies(
config: &GooseClientConfig,
) -> Result<Client, reqwest::Error> {
let mut client_builder = Client::builder()
.user_agent(&config.user_agent)
.gzip(config.gzip)
.danger_accept_invalid_certs(config.accept_invalid_certs);
if let Some(timeout) = config.timeout {
client_builder = client_builder.timeout(timeout);
}
#[cfg(feature = "cookies")]
{
client_builder = client_builder.cookie_store(true);
}
client_builder.build()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_type_state_transitions() {
let builder = GooseClientBuilder::new();
let builder = builder.without_cookies();
let _builder = builder.with_cookies();
}
#[test]
fn test_shared_methods() {
let builder = GooseClientBuilder::new()
.timeout(Duration::from_secs(30))
.user_agent("test-agent")
.gzip(false)
.accept_invalid_certs(true);
assert_eq!(builder.config.timeout, Some(Duration::from_secs(30)));
assert_eq!(builder.config.user_agent, "test-agent");
assert!(!builder.config.gzip);
assert!(builder.config.accept_invalid_certs);
}
#[test]
fn test_configuration_from_goose_config() {
use crate::config::GooseConfiguration;
let args: Vec<&str> = vec![];
let config = GooseConfiguration::parse_args_default(&args).unwrap();
let builder = GooseClientBuilder::from_configuration(&config);
assert!(builder.config.cookies_enabled);
assert!(builder.config.gzip); }
#[test]
fn test_client_strategy_individual() {
let builder = GooseClientBuilder::new().timeout(Duration::from_secs(10));
let strategy = builder.build_strategy();
match strategy {
ClientStrategy::Individual(config) => {
assert_eq!(config.timeout, Some(Duration::from_secs(10)));
assert!(config.cookies_enabled);
}
ClientStrategy::Shared(_) => panic!("Expected Individual strategy"),
}
}
#[test]
fn test_client_strategy_shared() {
let builder = GooseClientBuilder::new()
.without_cookies()
.timeout(Duration::from_secs(10));
let strategy = builder.build_strategy().unwrap();
match strategy {
ClientStrategy::Shared(_) => {
}
ClientStrategy::Individual(_) => panic!("Expected Shared strategy"),
}
}
}