use std::{
fmt::{Debug, Formatter, Result as FmtResult},
time::Duration,
};
use reqwest::Client as HttpClient;
use super::{
config::{DEFAULT_TIMEOUT, Network},
hooks::Hook,
http::Client,
};
use crate::error::{Error, Result};
const APP_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"));
pub struct ClientBuilder {
network: Option<Network>,
timeout: Option<Duration>,
http_client: Option<HttpClient>,
hooks: Vec<Box<dyn Hook>>,
}
impl Debug for ClientBuilder {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
f.debug_struct("ClientBuilder")
.field("network", &self.network)
.field("timeout", &self.timeout)
.field("hooks_count", &self.hooks.len())
.finish()
}
}
impl ClientBuilder {
pub fn new() -> Self {
Self {
network: None,
timeout: None,
http_client: None,
hooks: Vec::new(),
}
}
pub fn network(mut self, network: Network) -> Self {
self.network = Some(network);
self
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = Some(timeout);
self
}
pub fn http_client(mut self, client: HttpClient) -> Self {
self.http_client = Some(client);
self
}
pub fn hook<H: Hook + 'static>(mut self, hook: H) -> Self {
self.hooks.push(Box::new(hook));
self
}
pub fn build(self) -> Result<Client> {
let network = self
.network
.ok_or_else(|| Error::invalid_parameter("network", "Network is required"))?;
let http_client = if let Some(client) = self.http_client {
client
} else {
let timeout = self.timeout.unwrap_or(DEFAULT_TIMEOUT);
reqwest::Client::builder()
.timeout(timeout)
.user_agent(APP_USER_AGENT)
.build()?
};
Client::new(network, http_client, self.hooks)
}
}
impl Default for ClientBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use super::*;
use crate::client::config::{LOCAL_URL, MAINNET_URL, TESTNET_URL};
#[test]
fn test_app_user_agent() {
let version = env!("CARGO_PKG_VERSION");
assert_eq!(APP_USER_AGENT, format!("onemoney-protocol/{}", version));
}
#[test]
fn test_builder_network_configuration() {
let networks = [
(Network::Mainnet, MAINNET_URL),
(Network::Testnet, TESTNET_URL),
(Network::Local, LOCAL_URL),
];
for (network, _expected_url) in networks {
let builder = ClientBuilder::new().network(network.clone());
assert_eq!(builder.network, Some(network));
let client = builder.build().expect("Network configuration should work");
let debug_str = format!("{:?}", client);
assert!(debug_str.contains("base_url"));
}
}
#[test]
fn test_builder_timeout_configuration() {
let test_timeouts = [
Duration::from_millis(1),
Duration::from_secs(5),
Duration::from_secs(30),
Duration::from_secs(120),
Duration::from_secs(3600),
];
for timeout in test_timeouts {
let builder = ClientBuilder::new().network(Network::Mainnet).timeout(timeout);
assert_eq!(builder.timeout, Some(timeout));
let client = builder.build();
assert!(client.is_ok(), "Timeout configuration should work for {:?}", timeout);
}
}
#[test]
fn test_builder_custom_base_url() {
let test_urls = [
"http://localhost:8080",
"https://api.example.com",
"http://127.0.0.1:3000",
"https://custom.domain.com:8443",
];
for url in test_urls {
let builder = ClientBuilder::new().network(Network::Custom(url.into()));
assert_eq!(builder.network, Some(Network::Custom(url.into())));
let client = builder.build();
assert!(client.is_ok(), "Custom base URL should work for {}", url);
}
}
#[test]
fn test_builder_http_client_configuration() {
let custom_client = reqwest::Client::builder()
.timeout(Duration::from_secs(10))
.build()
.expect("Custom HTTP client should build");
let builder = ClientBuilder::new()
.network(Network::Mainnet)
.http_client(custom_client);
assert!(builder.http_client.is_some());
let client = builder.build();
assert!(client.is_ok(), "Custom HTTP client configuration should work");
}
#[test]
fn test_builder_hooks_management() {
struct TestHook;
impl Hook for TestHook {
fn before_request(&self, _method: &str, _url: &str, _body: Option<&str>) {}
fn after_response(&self, _method: &str, _url: &str, _status: u16, _body: Option<&str>) {}
}
let builder = ClientBuilder::new()
.network(Network::Mainnet)
.hook(TestHook)
.hook(TestHook);
assert_eq!(builder.hooks.len(), 2);
let client = builder.build();
assert!(client.is_ok(), "Hook management should work");
}
#[test]
fn test_builder_validation_errors() {
let result = ClientBuilder::new()
.network(Network::Custom("invalid-url-format".into()))
.build();
assert!(result.is_err(), "Invalid URL should cause build error");
}
#[test]
fn test_builder_debug_implementation() {
let builder = ClientBuilder::new()
.network(Network::Custom("http://example.com".into()))
.timeout(Duration::from_secs(30));
let debug_str = format!("{:?}", builder);
assert!(debug_str.contains("ClientBuilder"));
assert!(debug_str.contains("network"));
assert!(debug_str.contains("timeout"));
assert!(debug_str.contains("hooks_count"));
assert!(debug_str.contains("Custom"));
assert!(debug_str.contains("example.com"));
assert!(debug_str.contains("30s"));
}
#[test]
fn test_builder_multiple_configurations() {
let builder = ClientBuilder::new()
.network(Network::Mainnet)
.network(Network::Testnet) .timeout(Duration::from_secs(10))
.timeout(Duration::from_secs(20));
assert_eq!(builder.network, Some(Network::Testnet));
assert_eq!(builder.timeout, Some(Duration::from_secs(20)));
let client = builder.build();
assert!(client.is_ok(), "Multiple configurations should work");
}
#[test]
fn test_builder_default_trait() {
let builder1 = ClientBuilder::default();
let builder2 = ClientBuilder::new();
assert_eq!(builder1.network, builder2.network);
assert_eq!(builder1.timeout, builder2.timeout);
assert_eq!(builder1.hooks.len(), builder2.hooks.len());
}
#[test]
fn test_builder_extreme_timeout_values() {
let client1 = ClientBuilder::new()
.network(Network::Mainnet)
.timeout(Duration::from_nanos(1))
.build();
assert!(client1.is_ok(), "Very small timeout should be accepted");
let client2 = ClientBuilder::new()
.network(Network::Mainnet)
.timeout(Duration::from_secs(u64::MAX / 1000)) .build();
assert!(client2.is_ok(), "Very large timeout should be accepted");
}
#[test]
fn test_builder_edge_case_urls() {
let edge_case_urls = [
"http://localhost",
"https://a.b",
"http://127.0.0.1",
"https://example.com:443",
];
for url in edge_case_urls {
let client = ClientBuilder::new().network(Network::Custom(url.into())).build();
assert!(client.is_ok(), "Edge case URL {} should work", url);
}
}
}