use std::sync::Arc;
use crate::client::ClientRef;
use crate::config::Config;
use crate::error::Error;
use crate::oauth2::jwk::cache::JwtKeyCache;
use crate::Client;
pub struct ClientBuilder {
pub(crate) config: Option<Config>,
pub(crate) reqwest_client: Option<reqwest::Client>,
pub(crate) user_agent: Option<String>,
pub(crate) client_id: Option<String>,
pub(crate) client_secret: Option<String>,
pub(crate) callback_url: Option<String>,
}
impl Default for ClientBuilder {
fn default() -> Self {
Self::new()
}
}
impl ClientBuilder {
pub fn new() -> Self {
Self {
config: None,
user_agent: None,
reqwest_client: None,
client_id: None,
client_secret: None,
callback_url: None,
}
}
pub fn build(self) -> Result<Client, Error> {
let mut builder = self;
let config = match builder.config.take() {
Some(config) => config,
None => Config::new()?,
};
let reqwest_client =
get_or_default_reqwest_client(builder.reqwest_client.take(), &builder.user_agent)?;
let oauth_client = if builder.client_id.is_some()
|| builder.client_secret.is_some()
|| builder.callback_url.is_some()
{
Some(builder.setup_oauth_client(&config)?)
} else {
None
};
let jwt_key_cache = JwtKeyCache::new(&config);
let client_ref = ClientRef {
reqwest_client,
esi_url: config.esi_url,
esi_validate_token_before_request: config.esi_validate_token_before_request,
oauth2_client: oauth_client,
jwt_key_cache,
jwt_issuers: config.jwt_issuers,
jwt_audience: config.jwt_audience,
};
Ok(Client {
inner: Arc::new(client_ref),
})
}
pub fn config(mut self, config: Config) -> Self {
self.config = Some(config);
self
}
pub fn reqwest_client(mut self, client: reqwest::Client) -> Self {
self.reqwest_client = Some(client);
self
}
pub fn user_agent(mut self, user_agent: &str) -> Self {
self.user_agent = Some(user_agent.to_string());
self
}
pub fn client_id(mut self, client_id: &str) -> Self {
self.client_id = Some(client_id.to_string());
self
}
pub fn client_secret(mut self, client_secret: &str) -> Self {
self.client_secret = Some(client_secret.to_string());
self
}
pub fn callback_url(mut self, callback_url: &str) -> Self {
self.callback_url = Some(callback_url.to_string());
self
}
}
fn get_or_default_reqwest_client(
client: Option<reqwest::Client>,
user_agent: &Option<String>,
) -> Result<reqwest::Client, Error> {
if user_agent.is_some() && client.is_some() {
warn!(
"user_agent is set on `ClientBuilder` but so is reqwest_client. The user_agent will not be applied and should be instead applied to the provided reqwest client if not done so already."
);
}
match client {
Some(client) => Ok(client),
None => {
let mut client_builder = reqwest::Client::builder();
if let Some(agent) = user_agent {
client_builder = client_builder.user_agent(agent.clone());
}
Ok(client_builder.build()?)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{constant::DEFAULT_ESI_URL, ConfigError};
#[test]
fn test_default_builder_values() {
let builder = ClientBuilder::default();
assert!(builder.config.is_none());
assert!(builder.reqwest_client.is_none());
assert!(builder.user_agent.is_none());
assert!(builder.client_id.is_none());
assert!(builder.client_secret.is_none());
assert!(builder.callback_url.is_none());
}
#[test]
fn test_builder_setter_methods() {
let custom_reqwest_client = reqwest::Client::new();
let custom_config = Config::new().expect("Failed to create a default Config");
let builder = ClientBuilder::new()
.config(custom_config)
.user_agent("MyApp/1.0 (contact@example.com)")
.reqwest_client(custom_reqwest_client)
.client_id("client_id")
.client_secret("client_secret")
.callback_url("http://localhost:8000/callback");
assert!(builder.config.is_some());
assert!(builder.reqwest_client.is_some());
assert_eq!(
builder.user_agent,
Some("MyApp/1.0 (contact@example.com)".to_string())
);
assert_eq!(builder.client_id, Some("client_id".to_string()));
assert_eq!(builder.client_secret, Some("client_secret".to_string()));
assert_eq!(
builder.callback_url,
Some("http://localhost:8000/callback".to_string())
);
}
#[test]
fn test_successful_build_minimal() {
let result = ClientBuilder::new()
.user_agent("MyApp/1.0 (contact@example.com)")
.build();
assert!(result.is_ok());
let client = result.unwrap();
assert_eq!(client.inner.esi_url, DEFAULT_ESI_URL);
assert!(client.inner.oauth2_client.is_none());
}
#[test]
fn test_successful_build_with_oauth() {
let result = ClientBuilder::new()
.user_agent("MyApp/1.0 (contact@example.com)")
.client_id("client_id")
.client_secret("client_secret")
.callback_url("http://localhost:8080/callback")
.build();
assert!(result.is_ok());
let client = result.unwrap();
assert!(client.inner.oauth2_client.is_some());
}
#[test]
fn test_build_with_custom_config() {
let config = Config::builder()
.esi_url("https://example.com")
.build()
.expect("Failed to create a default Config");
let result = ClientBuilder::new()
.config(config)
.build()
.expect("Failed to build Client");
assert_ne!(result.inner.esi_url, DEFAULT_ESI_URL);
}
#[test]
fn test_build_with_partial_oauth_config() {
let result = ClientBuilder::new()
.user_agent("MyApp/1.0 (contact@example.com)")
.client_id("client_id")
.build();
assert!(result.is_err());
assert!(matches!(
result,
Err(Error::ConfigError(ConfigError::MissingClientSecret))
));
}
}
#[cfg(test)]
mod get_or_default_reqwest_client_tests {
use crate::builder::get_or_default_reqwest_client;
#[test]
fn test_custom_client_and_agent() {
let user_agent = "MyApp/1.0 (contact@example.com)".to_string();
let timeout = std::time::Duration::from_secs(10);
let client = reqwest::Client::builder()
.user_agent(&user_agent)
.timeout(timeout)
.build()
.expect("Failed to build reqwest::Client");
let result = get_or_default_reqwest_client(Some(client), &Some(user_agent));
assert!(result.is_ok());
}
#[test]
fn test_default_with_agent() {
let result = get_or_default_reqwest_client(None, &Some("Agent".to_string()));
assert!(result.is_ok());
}
}