use std::{collections::HashMap, ops::Deref, sync::Arc, time::Duration};
use crate::{
auth::token_provider::{NoOpTokenProvider, TokenProvider},
constants::{AppType, FEISHU_BASE_URL},
performance::OptimizedHttpConfig,
};
#[derive(Debug, Clone)]
pub struct Config {
inner: Arc<ConfigInner>,
}
#[derive(Debug)]
pub struct ConfigInner {
pub(crate) app_id: String,
pub(crate) app_secret: String,
pub(crate) base_url: String,
pub(crate) enable_token_cache: bool,
pub(crate) app_type: AppType,
pub(crate) http_client: reqwest::Client,
pub(crate) req_timeout: Option<Duration>,
pub(crate) header: HashMap<String, String>,
pub(crate) token_provider: Arc<dyn TokenProvider>,
}
impl Default for ConfigInner {
fn default() -> Self {
Self {
app_id: "".to_string(),
app_secret: "".to_string(),
base_url: FEISHU_BASE_URL.to_string(),
enable_token_cache: true,
app_type: AppType::SelfBuild,
http_client: reqwest::Client::new(),
req_timeout: None,
header: Default::default(),
token_provider: Arc::new(NoOpTokenProvider),
}
}
}
impl Default for Config {
fn default() -> Self {
Self {
inner: Arc::new(ConfigInner::default()),
}
}
}
impl Deref for Config {
type Target = ConfigInner;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl Config {
pub fn builder() -> ConfigBuilder {
ConfigBuilder::default()
}
pub fn new(inner: ConfigInner) -> Self {
Self {
inner: Arc::new(inner),
}
}
pub fn with_token_provider(&self, provider: impl TokenProvider + 'static) -> Self {
Config::new(ConfigInner {
app_id: self.app_id.clone(),
app_secret: self.app_secret.clone(),
base_url: self.base_url.clone(),
enable_token_cache: self.enable_token_cache,
app_type: self.app_type,
http_client: self.http_client.clone(),
req_timeout: self.req_timeout,
header: self.header.clone(),
token_provider: Arc::new(provider),
})
}
pub fn reference_count(&self) -> usize {
Arc::strong_count(&self.inner)
}
pub fn app_id(&self) -> &str {
&self.inner.app_id
}
pub fn app_secret(&self) -> &str {
&self.inner.app_secret
}
pub fn base_url(&self) -> &str {
&self.inner.base_url
}
pub fn req_timeout(&self) -> Option<Duration> {
self.inner.req_timeout
}
pub fn enable_token_cache(&self) -> bool {
self.inner.enable_token_cache
}
pub fn app_type(&self) -> AppType {
self.inner.app_type
}
pub fn http_client(&self) -> &reqwest::Client {
&self.inner.http_client
}
pub fn header(&self) -> &HashMap<String, String> {
&self.inner.header
}
pub fn token_provider(&self) -> &Arc<dyn TokenProvider> {
&self.inner.token_provider
}
}
#[derive(Default, Clone)]
pub struct ConfigBuilder {
app_id: Option<String>,
app_secret: Option<String>,
base_url: Option<String>,
enable_token_cache: Option<bool>,
app_type: Option<AppType>,
http_client: Option<reqwest::Client>,
req_timeout: Option<Duration>,
header: Option<HashMap<String, String>>,
token_provider: Option<Arc<dyn TokenProvider>>,
}
impl ConfigBuilder {
pub fn app_id(mut self, app_id: impl Into<String>) -> Self {
self.app_id = Some(app_id.into());
self
}
pub fn app_secret(mut self, app_secret: impl Into<String>) -> Self {
self.app_secret = Some(app_secret.into());
self
}
pub fn base_url(mut self, base_url: impl Into<String>) -> Self {
self.base_url = Some(base_url.into());
self
}
pub fn enable_token_cache(mut self, enable: bool) -> Self {
self.enable_token_cache = Some(enable);
self
}
pub fn app_type(mut self, app_type: AppType) -> Self {
self.app_type = Some(app_type);
self
}
pub fn http_client(mut self, client: reqwest::Client) -> Self {
self.http_client = Some(client);
self
}
pub fn optimized_http_client(
mut self,
config: OptimizedHttpConfig,
) -> Result<Self, reqwest::Error> {
let client = config.build_client()?;
self.http_client = Some(client);
Ok(self)
}
pub fn production_http_client(self) -> Result<Self, reqwest::Error> {
let config = OptimizedHttpConfig::production();
self.optimized_http_client(config)
}
pub fn high_throughput_http_client(self) -> Result<Self, reqwest::Error> {
let config = OptimizedHttpConfig::high_throughput();
self.optimized_http_client(config)
}
pub fn low_latency_http_client(self) -> Result<Self, reqwest::Error> {
let config = OptimizedHttpConfig::low_latency();
self.optimized_http_client(config)
}
pub fn req_timeout(mut self, timeout: Duration) -> Self {
self.req_timeout = Some(timeout);
self
}
pub fn header(mut self, header: HashMap<String, String>) -> Self {
self.header = Some(header);
self
}
pub fn token_provider(mut self, provider: impl TokenProvider + 'static) -> Self {
self.token_provider = Some(Arc::new(provider));
self
}
pub fn build(self) -> Config {
let default = ConfigInner::default();
Config::new(ConfigInner {
app_id: self.app_id.unwrap_or(default.app_id),
app_secret: self.app_secret.unwrap_or(default.app_secret),
base_url: self.base_url.unwrap_or(default.base_url),
enable_token_cache: self
.enable_token_cache
.unwrap_or(default.enable_token_cache),
app_type: self.app_type.unwrap_or(default.app_type),
http_client: self.http_client.unwrap_or(default.http_client),
req_timeout: self.req_timeout.or(default.req_timeout),
header: self.header.unwrap_or(default.header),
token_provider: self.token_provider.unwrap_or(default.token_provider),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::auth::NoOpTokenProvider;
use crate::auth::TokenProvider;
use crate::auth::TokenRequest;
use crate::constants::{AppType, FEISHU_BASE_URL};
use std::time::Duration;
use std::{future::Future, pin::Pin};
#[test]
fn test_config_creation() {
let config = Config::new(ConfigInner {
app_id: "test_app_id".to_string(),
app_secret: "test_app_secret".to_string(),
base_url: "https://test.api.com".to_string(),
enable_token_cache: true,
app_type: AppType::SelfBuild,
http_client: reqwest::Client::new(),
req_timeout: Some(Duration::from_secs(30)),
header: HashMap::new(),
token_provider: Arc::new(NoOpTokenProvider),
});
assert_eq!(config.app_id, "test_app_id");
assert_eq!(config.app_secret, "test_app_secret");
assert_eq!(config.base_url, "https://test.api.com");
assert!(config.enable_token_cache);
assert_eq!(config.req_timeout, Some(Duration::from_secs(30)));
}
#[test]
fn test_config_default() {
let config = Config::default();
assert_eq!(config.app_id, "");
assert_eq!(config.app_secret, "");
assert_eq!(config.base_url, FEISHU_BASE_URL);
assert!(config.enable_token_cache);
assert_eq!(config.app_type, AppType::SelfBuild);
assert!(config.req_timeout.is_none());
assert!(config.header.is_empty());
}
#[test]
fn test_config_clone() {
let config = Config::new(ConfigInner {
app_id: "clone_test".to_string(),
app_secret: "clone_secret".to_string(),
base_url: "https://clone.test.com".to_string(),
enable_token_cache: false,
app_type: AppType::Marketplace,
http_client: reqwest::Client::new(),
req_timeout: Some(Duration::from_secs(60)),
header: {
let mut header = HashMap::new();
header.insert("Test-Header".to_string(), "test-value".to_string());
header
},
token_provider: Arc::new(NoOpTokenProvider),
});
let cloned_config = config.clone();
assert_eq!(config.app_id, cloned_config.app_id);
assert_eq!(config.app_secret, cloned_config.app_secret);
assert_eq!(config.base_url, cloned_config.base_url);
assert_eq!(config.enable_token_cache, cloned_config.enable_token_cache);
assert_eq!(config.app_type, cloned_config.app_type);
assert_eq!(config.req_timeout, cloned_config.req_timeout);
assert_eq!(config.header.len(), cloned_config.header.len());
assert_eq!(
config.header.get("Test-Header"),
cloned_config.header.get("Test-Header")
);
assert!(Arc::ptr_eq(&config.inner, &cloned_config.inner));
assert_eq!(config.reference_count(), 2);
}
#[test]
fn test_config_debug() {
let config = Config::default();
let debug_str = format!("{:?}", config);
assert!(debug_str.contains("Config"));
assert!(debug_str.contains("app_id"));
assert!(debug_str.contains("app_secret"));
assert!(debug_str.contains("base_url"));
}
#[test]
fn test_config_with_custom_header() {
let mut header = HashMap::new();
header.insert("Authorization".to_string(), "Bearer token".to_string());
header.insert("Content-Type".to_string(), "application/json".to_string());
let config = Config::new(ConfigInner {
header,
..ConfigInner::default()
});
assert_eq!(config.header.len(), 2);
assert_eq!(
config.header.get("Authorization"),
Some(&"Bearer token".to_string())
);
assert_eq!(
config.header.get("Content-Type"),
Some(&"application/json".to_string())
);
}
#[test]
fn test_config_with_different_app_types() {
let self_build_config = Config::new(ConfigInner {
app_type: AppType::SelfBuild,
..ConfigInner::default()
});
let marketplace_config = Config::new(ConfigInner {
app_type: AppType::Marketplace,
..ConfigInner::default()
});
assert_eq!(self_build_config.app_type, AppType::SelfBuild);
assert_eq!(marketplace_config.app_type, AppType::Marketplace);
assert_ne!(self_build_config.app_type, marketplace_config.app_type);
}
#[test]
fn test_config_with_timeout_variations() {
let no_timeout_config = Config::default();
let short_timeout_config = Config::new(ConfigInner {
req_timeout: Some(Duration::from_secs(5)),
..ConfigInner::default()
});
let long_timeout_config = Config::new(ConfigInner {
req_timeout: Some(Duration::from_secs(300)),
..ConfigInner::default()
});
assert!(no_timeout_config.req_timeout.is_none());
assert_eq!(
short_timeout_config.req_timeout,
Some(Duration::from_secs(5))
);
assert_eq!(
long_timeout_config.req_timeout,
Some(Duration::from_secs(300))
);
}
#[test]
fn test_config_builders() {
let config = Config::builder()
.app_id("test_app")
.app_secret("test_secret")
.build();
assert_eq!(config.app_id, "test_app");
assert_eq!(config.app_secret, "test_secret");
}
#[test]
fn test_config_arc_efficiency() {
let config = Config::default();
assert_eq!(config.reference_count(), 1);
let config_clone = config.clone();
assert_eq!(config.reference_count(), 2);
assert_eq!(config_clone.reference_count(), 2);
assert!(Arc::ptr_eq(&config.inner, &config_clone.inner));
}
#[test]
fn test_arc_efficiency_simulation() {
let config = Config::default();
let service1_config = config.clone();
let service2_config = config.clone();
let service3_config = config.clone();
let service4_config = config.clone();
assert!(Arc::ptr_eq(&config.inner, &service1_config.inner));
assert!(Arc::ptr_eq(&config.inner, &service2_config.inner));
assert!(Arc::ptr_eq(&config.inner, &service3_config.inner));
assert!(Arc::ptr_eq(&config.inner, &service4_config.inner));
assert_eq!(config.reference_count(), 5);
println!("Arc<Config> 改造成功:5个配置实例共享同一份内存!");
}
#[derive(Debug)]
struct TestTokenProvider;
impl TokenProvider for TestTokenProvider {
fn get_token(
&self,
_request: TokenRequest,
) -> Pin<Box<dyn Future<Output = crate::SDKResult<String>> + Send + '_>> {
Box::pin(async { Ok("test_token".to_string()) })
}
}
#[tokio::test]
async fn test_with_token_provider() {
let base = Config::builder()
.app_id("test_app")
.app_secret("test_secret")
.build();
let config = base.with_token_provider(TestTokenProvider);
let token = config
.token_provider
.get_token(TokenRequest::app())
.await
.unwrap();
assert_eq!(token, "test_token");
}
}