Skip to main content

openlark_core/
config.rs

1use std::{collections::HashMap, ops::Deref, sync::Arc, time::Duration};
2
3use crate::{
4    auth::token_provider::{NoOpTokenProvider, TokenProvider},
5    constants::{AppType, FEISHU_BASE_URL},
6    performance::OptimizedHttpConfig,
7};
8
9/// # 零拷贝配置共享实现
10///
11/// `Config` 内部使用 `Arc<ConfigInner>` 实现零拷贝共享:
12///
13/// ## 性能特性
14/// - **内存效率**: 所有克隆共享同一份配置数据(~300-500字节)
15/// - **克隆成本**: `Config::clone()` 只复制Arc指针(8字节 + 原子操作)
16/// - **线程安全**: Arc保证多线程安全的只读访问
17/// - **引用计数**: 自动管理内存,无泄漏风险
18///
19/// ## 使用建议
20/// ```rust
21/// // ✅ 推荐: 克隆Config传递给服务
22/// let service = MyService::new(config.clone());
23///
24/// // ✅ 推荐: 在Request中持有Config
25/// pub struct MyRequest {
26///     config: Config,  // 持有Arc指针,成本低
27/// }
28///
29/// // ⚠️ 不必要: 使用Arc<Config> (Config内部已经是Arc)
30/// // Arc<Arc<ConfigInner>> = 双重Arc,没有额外收益
31/// ```
32///
33/// ## 性能验证
34/// 运行 `cargo test config_arc` 查看基准测试:
35/// - 克隆速度: ~10-20纳秒
36/// - 内存开销: 每个克隆仅8字节
37/// - 引用计数: 自动维护
38#[derive(Debug, Clone)]
39pub struct Config {
40    /// 包装在 Arc 中的共享配置数据
41    ///
42    /// 所有 Config 实例通过 Arc 共享同一份 ConfigInner,
43    /// 实现零拷贝的配置共享。
44    inner: Arc<ConfigInner>,
45}
46
47/// 内部配置数据,被多个服务共享
48#[derive(Debug)]
49pub struct ConfigInner {
50    pub(crate) app_id: String,
51    pub(crate) app_secret: String,
52    /// 域名, 默认为 <https://open.feishu.cn>
53    pub(crate) base_url: String,
54    /// 是否允许 core 在缺少显式 token 时自动获取 token
55    pub(crate) enable_token_cache: bool,
56    /// 应用类型, 默认为自建应用
57    pub(crate) app_type: AppType,
58    pub(crate) http_client: reqwest::Client,
59    /// 客户端超时时间, 默认永不超时
60    pub(crate) req_timeout: Option<Duration>,
61    pub(crate) header: HashMap<String, String>,
62    /// Token 获取抽象(由业务 crate 实现,例如 openlark-auth)
63    pub(crate) token_provider: Arc<dyn TokenProvider>,
64}
65
66impl Default for ConfigInner {
67    fn default() -> Self {
68        Self {
69            app_id: "".to_string(),
70            app_secret: "".to_string(),
71            base_url: FEISHU_BASE_URL.to_string(),
72            enable_token_cache: true,
73            app_type: AppType::SelfBuild,
74            http_client: reqwest::Client::new(),
75            req_timeout: None,
76            header: Default::default(),
77            token_provider: Arc::new(NoOpTokenProvider),
78        }
79    }
80}
81
82impl Default for Config {
83    fn default() -> Self {
84        Self {
85            inner: Arc::new(ConfigInner::default()),
86        }
87    }
88}
89
90impl Deref for Config {
91    type Target = ConfigInner;
92
93    fn deref(&self) -> &Self::Target {
94        &self.inner
95    }
96}
97
98impl Config {
99    pub fn builder() -> ConfigBuilder {
100        ConfigBuilder::default()
101    }
102
103    /// 创建新的 Config 实例,直接从 ConfigInner
104    pub fn new(inner: ConfigInner) -> Self {
105        Self {
106            inner: Arc::new(inner),
107        }
108    }
109
110    /// 基于当前配置生成一个“替换 TokenProvider”的新配置
111    ///
112    /// 说明:
113    /// - 这是一个纯拷贝操作(`Config` 本身是 `Arc` 包装),不会修改原配置
114    /// - 推荐用法:先构建一个“基础 Config”(默认 `NoOpTokenProvider`),再用该基础 Config 构建业务 TokenProvider,
115    ///   最后调用此方法把 provider 注入到“业务 Config”中,避免循环引用。
116    pub fn with_token_provider(&self, provider: impl TokenProvider + 'static) -> Self {
117        Config::new(ConfigInner {
118            app_id: self.app_id.clone(),
119            app_secret: self.app_secret.clone(),
120            base_url: self.base_url.clone(),
121            enable_token_cache: self.enable_token_cache,
122            app_type: self.app_type,
123            http_client: self.http_client.clone(),
124            req_timeout: self.req_timeout,
125            header: self.header.clone(),
126            token_provider: Arc::new(provider),
127        })
128    }
129
130    /// 获取内部 Arc 的引用计数
131    pub fn reference_count(&self) -> usize {
132        Arc::strong_count(&self.inner)
133    }
134
135    /// 获取应用 ID
136    pub fn app_id(&self) -> &str {
137        &self.inner.app_id
138    }
139
140    /// 获取应用密钥
141    pub fn app_secret(&self) -> &str {
142        &self.inner.app_secret
143    }
144
145    /// 获取基础 URL
146    pub fn base_url(&self) -> &str {
147        &self.inner.base_url
148    }
149
150    /// 获取超时时间
151    pub fn req_timeout(&self) -> Option<Duration> {
152        self.inner.req_timeout
153    }
154
155    /// 是否启用令牌缓存
156    pub fn enable_token_cache(&self) -> bool {
157        self.inner.enable_token_cache
158    }
159
160    /// 获取应用类型
161    pub fn app_type(&self) -> AppType {
162        self.inner.app_type
163    }
164
165    /// 获取 HTTP 客户端引用
166    pub fn http_client(&self) -> &reqwest::Client {
167        &self.inner.http_client
168    }
169
170    /// 获取自定义 header 引用
171    pub fn header(&self) -> &HashMap<String, String> {
172        &self.inner.header
173    }
174
175    /// 获取 TokenProvider 引用
176    pub fn token_provider(&self) -> &Arc<dyn TokenProvider> {
177        &self.inner.token_provider
178    }
179}
180
181#[derive(Default, Clone)]
182pub struct ConfigBuilder {
183    app_id: Option<String>,
184    app_secret: Option<String>,
185    base_url: Option<String>,
186    enable_token_cache: Option<bool>,
187    app_type: Option<AppType>,
188    http_client: Option<reqwest::Client>,
189    req_timeout: Option<Duration>,
190    header: Option<HashMap<String, String>>,
191    token_provider: Option<Arc<dyn TokenProvider>>,
192}
193
194impl ConfigBuilder {
195    pub fn app_id(mut self, app_id: impl Into<String>) -> Self {
196        self.app_id = Some(app_id.into());
197        self
198    }
199
200    pub fn app_secret(mut self, app_secret: impl Into<String>) -> Self {
201        self.app_secret = Some(app_secret.into());
202        self
203    }
204
205    pub fn base_url(mut self, base_url: impl Into<String>) -> Self {
206        self.base_url = Some(base_url.into());
207        self
208    }
209
210    pub fn enable_token_cache(mut self, enable: bool) -> Self {
211        self.enable_token_cache = Some(enable);
212        self
213    }
214
215    pub fn app_type(mut self, app_type: AppType) -> Self {
216        self.app_type = Some(app_type);
217        self
218    }
219
220    pub fn http_client(mut self, client: reqwest::Client) -> Self {
221        self.http_client = Some(client);
222        self
223    }
224
225    /// 使用优化的HTTP配置构建客户端
226    pub fn optimized_http_client(
227        mut self,
228        config: OptimizedHttpConfig,
229    ) -> Result<Self, reqwest::Error> {
230        let client = config.build_client()?;
231        self.http_client = Some(client);
232        Ok(self)
233    }
234
235    /// 使用生产环境优化配置
236    pub fn production_http_client(self) -> Result<Self, reqwest::Error> {
237        let config = OptimizedHttpConfig::production();
238        self.optimized_http_client(config)
239    }
240
241    /// 使用高吞吐量配置
242    pub fn high_throughput_http_client(self) -> Result<Self, reqwest::Error> {
243        let config = OptimizedHttpConfig::high_throughput();
244        self.optimized_http_client(config)
245    }
246
247    /// 使用低延迟配置
248    pub fn low_latency_http_client(self) -> Result<Self, reqwest::Error> {
249        let config = OptimizedHttpConfig::low_latency();
250        self.optimized_http_client(config)
251    }
252
253    pub fn req_timeout(mut self, timeout: Duration) -> Self {
254        self.req_timeout = Some(timeout);
255        self
256    }
257
258    pub fn header(mut self, header: HashMap<String, String>) -> Self {
259        self.header = Some(header);
260        self
261    }
262
263    pub fn token_provider(mut self, provider: impl TokenProvider + 'static) -> Self {
264        self.token_provider = Some(Arc::new(provider));
265        self
266    }
267
268    pub fn build(self) -> Config {
269        let default = ConfigInner::default();
270        Config::new(ConfigInner {
271            app_id: self.app_id.unwrap_or(default.app_id),
272            app_secret: self.app_secret.unwrap_or(default.app_secret),
273            base_url: self.base_url.unwrap_or(default.base_url),
274            enable_token_cache: self
275                .enable_token_cache
276                .unwrap_or(default.enable_token_cache),
277            app_type: self.app_type.unwrap_or(default.app_type),
278            http_client: self.http_client.unwrap_or(default.http_client),
279            req_timeout: self.req_timeout.or(default.req_timeout),
280            header: self.header.unwrap_or(default.header),
281            token_provider: self.token_provider.unwrap_or(default.token_provider),
282        })
283    }
284}
285
286#[cfg(test)]
287mod tests {
288    use super::*;
289    use crate::auth::NoOpTokenProvider;
290    use crate::auth::TokenProvider;
291    use crate::auth::TokenRequest;
292    use crate::constants::{AppType, FEISHU_BASE_URL};
293    use std::time::Duration;
294    use std::{future::Future, pin::Pin};
295
296    #[test]
297    fn test_config_creation() {
298        let config = Config::new(ConfigInner {
299            app_id: "test_app_id".to_string(),
300            app_secret: "test_app_secret".to_string(),
301            base_url: "https://test.api.com".to_string(),
302            enable_token_cache: true,
303            app_type: AppType::SelfBuild,
304            http_client: reqwest::Client::new(),
305            req_timeout: Some(Duration::from_secs(30)),
306            header: HashMap::new(),
307            token_provider: Arc::new(NoOpTokenProvider),
308        });
309
310        assert_eq!(config.app_id, "test_app_id");
311        assert_eq!(config.app_secret, "test_app_secret");
312        assert_eq!(config.base_url, "https://test.api.com");
313        assert!(config.enable_token_cache);
314        assert_eq!(config.req_timeout, Some(Duration::from_secs(30)));
315    }
316
317    #[test]
318    fn test_config_default() {
319        let config = Config::default();
320
321        assert_eq!(config.app_id, "");
322        assert_eq!(config.app_secret, "");
323        assert_eq!(config.base_url, FEISHU_BASE_URL);
324        assert!(config.enable_token_cache);
325        assert_eq!(config.app_type, AppType::SelfBuild);
326        assert!(config.req_timeout.is_none());
327        assert!(config.header.is_empty());
328    }
329
330    #[test]
331    fn test_config_clone() {
332        let config = Config::new(ConfigInner {
333            app_id: "clone_test".to_string(),
334            app_secret: "clone_secret".to_string(),
335            base_url: "https://clone.test.com".to_string(),
336            enable_token_cache: false,
337            app_type: AppType::Marketplace,
338            http_client: reqwest::Client::new(),
339            req_timeout: Some(Duration::from_secs(60)),
340            header: {
341                let mut header = HashMap::new();
342                header.insert("Test-Header".to_string(), "test-value".to_string());
343                header
344            },
345            token_provider: Arc::new(NoOpTokenProvider),
346        });
347
348        let cloned_config = config.clone();
349
350        assert_eq!(config.app_id, cloned_config.app_id);
351        assert_eq!(config.app_secret, cloned_config.app_secret);
352        assert_eq!(config.base_url, cloned_config.base_url);
353        assert_eq!(config.enable_token_cache, cloned_config.enable_token_cache);
354        assert_eq!(config.app_type, cloned_config.app_type);
355        assert_eq!(config.req_timeout, cloned_config.req_timeout);
356        assert_eq!(config.header.len(), cloned_config.header.len());
357        assert_eq!(
358            config.header.get("Test-Header"),
359            cloned_config.header.get("Test-Header")
360        );
361
362        // Verify Arc clone efficiency - both should point to same memory
363        assert!(Arc::ptr_eq(&config.inner, &cloned_config.inner));
364
365        // Verify reference counting works
366        assert_eq!(config.reference_count(), 2);
367    }
368
369    #[test]
370    fn test_config_debug() {
371        let config = Config::default();
372        let debug_str = format!("{:?}", config);
373
374        assert!(debug_str.contains("Config"));
375        assert!(debug_str.contains("app_id"));
376        assert!(debug_str.contains("app_secret"));
377        assert!(debug_str.contains("base_url"));
378    }
379
380    #[test]
381    fn test_config_with_custom_header() {
382        let mut header = HashMap::new();
383        header.insert("Authorization".to_string(), "Bearer token".to_string());
384        header.insert("Content-Type".to_string(), "application/json".to_string());
385
386        let config = Config::new(ConfigInner {
387            header,
388            ..ConfigInner::default()
389        });
390
391        assert_eq!(config.header.len(), 2);
392        assert_eq!(
393            config.header.get("Authorization"),
394            Some(&"Bearer token".to_string())
395        );
396        assert_eq!(
397            config.header.get("Content-Type"),
398            Some(&"application/json".to_string())
399        );
400    }
401
402    #[test]
403    fn test_config_with_different_app_types() {
404        let self_build_config = Config::new(ConfigInner {
405            app_type: AppType::SelfBuild,
406            ..ConfigInner::default()
407        });
408
409        let marketplace_config = Config::new(ConfigInner {
410            app_type: AppType::Marketplace,
411            ..ConfigInner::default()
412        });
413
414        assert_eq!(self_build_config.app_type, AppType::SelfBuild);
415        assert_eq!(marketplace_config.app_type, AppType::Marketplace);
416        assert_ne!(self_build_config.app_type, marketplace_config.app_type);
417    }
418
419    #[test]
420    fn test_config_with_timeout_variations() {
421        let no_timeout_config = Config::default();
422
423        let short_timeout_config = Config::new(ConfigInner {
424            req_timeout: Some(Duration::from_secs(5)),
425            ..ConfigInner::default()
426        });
427
428        let long_timeout_config = Config::new(ConfigInner {
429            req_timeout: Some(Duration::from_secs(300)),
430            ..ConfigInner::default()
431        });
432
433        assert!(no_timeout_config.req_timeout.is_none());
434        assert_eq!(
435            short_timeout_config.req_timeout,
436            Some(Duration::from_secs(5))
437        );
438        assert_eq!(
439            long_timeout_config.req_timeout,
440            Some(Duration::from_secs(300))
441        );
442    }
443
444    #[test]
445    fn test_config_builders() {
446        let config = Config::builder()
447            .app_id("test_app")
448            .app_secret("test_secret")
449            .build();
450
451        assert_eq!(config.app_id, "test_app");
452        assert_eq!(config.app_secret, "test_secret");
453    }
454
455    #[test]
456    fn test_config_arc_efficiency() {
457        let config = Config::default();
458        assert_eq!(config.reference_count(), 1);
459
460        let config_clone = config.clone();
461        assert_eq!(config.reference_count(), 2);
462        assert_eq!(config_clone.reference_count(), 2);
463
464        // Both configs should point to the same inner data
465        assert!(Arc::ptr_eq(&config.inner, &config_clone.inner));
466    }
467
468    #[test]
469    fn test_arc_efficiency_simulation() {
470        // 模拟服务模块中的多次克隆
471        let config = Config::default();
472
473        // 模拟 PerformanceService::new() 中的4次clone
474        let service1_config = config.clone();
475        let service2_config = config.clone();
476        let service3_config = config.clone();
477        let service4_config = config.clone();
478
479        // 所有配置应该指向同一个内存位置
480        assert!(Arc::ptr_eq(&config.inner, &service1_config.inner));
481        assert!(Arc::ptr_eq(&config.inner, &service2_config.inner));
482        assert!(Arc::ptr_eq(&config.inner, &service3_config.inner));
483        assert!(Arc::ptr_eq(&config.inner, &service4_config.inner));
484
485        // 引用计数应该是5(原始 + 4个克隆)
486        assert_eq!(config.reference_count(), 5);
487
488        println!("Arc<Config> 改造成功:5个配置实例共享同一份内存!");
489    }
490
491    #[derive(Debug)]
492    struct TestTokenProvider;
493
494    impl TokenProvider for TestTokenProvider {
495        fn get_token(
496            &self,
497            _request: TokenRequest,
498        ) -> Pin<Box<dyn Future<Output = crate::SDKResult<String>> + Send + '_>> {
499            Box::pin(async { Ok("test_token".to_string()) })
500        }
501    }
502
503    #[tokio::test]
504    async fn test_with_token_provider() {
505        let base = Config::builder()
506            .app_id("test_app")
507            .app_secret("test_secret")
508            .build();
509
510        let config = base.with_token_provider(TestTokenProvider);
511
512        let token = config
513            .token_provider
514            .get_token(TokenRequest::app())
515            .await
516            .unwrap();
517        assert_eq!(token, "test_token");
518    }
519}