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/// 内部配置数据,被多个服务共享
48pub struct ConfigInner {
49    pub(crate) app_id: String,
50    pub(crate) app_secret: String,
51    /// 域名, 默认为 <https://open.feishu.cn>
52    pub(crate) base_url: String,
53    /// 是否允许 core 在缺少显式 token 时自动获取 token
54    pub(crate) enable_token_cache: bool,
55    /// 应用类型, 默认为自建应用
56    pub(crate) app_type: AppType,
57    pub(crate) http_client: reqwest::Client,
58    /// 客户端超时时间, 默认永不超时
59    pub(crate) req_timeout: Option<Duration>,
60    pub(crate) header: HashMap<String, String>,
61    /// Token 获取抽象(由业务 crate 实现,例如 openlark-auth)
62    pub(crate) token_provider: Arc<dyn TokenProvider>,
63    /// 响应体最大大小(字节),超过返回 ResponseTooLarge 错误,默认 100MB
64    pub(crate) max_response_size: u64,
65    /// 默认重试次数,默认 3
66    pub(crate) retry_count: u32,
67    /// 是否启用日志记录,默认 true
68    pub(crate) enable_log: bool,
69}
70
71impl Default for ConfigInner {
72    fn default() -> Self {
73        Self {
74            app_id: "".to_string(),
75            app_secret: "".to_string(),
76            base_url: FEISHU_BASE_URL.to_string(),
77            enable_token_cache: true,
78            app_type: AppType::SelfBuild,
79            http_client: reqwest::Client::new(),
80            req_timeout: None,
81            header: Default::default(),
82            token_provider: Arc::new(NoOpTokenProvider),
83            max_response_size: 100 * 1024 * 1024, // 100MB
84            retry_count: 3,
85            enable_log: true,
86        }
87    }
88}
89
90impl std::fmt::Debug for ConfigInner {
91    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92        f.debug_struct("ConfigInner")
93            .field("app_id", &self.app_id)
94            .field("app_secret", &"***")
95            .field("base_url", &self.base_url)
96            .field("enable_token_cache", &self.enable_token_cache)
97            .field("app_type", &self.app_type)
98            .field("req_timeout", &self.req_timeout)
99            .field("max_response_size", &self.max_response_size)
100            .field("retry_count", &self.retry_count)
101            .field("enable_log", &self.enable_log)
102            .field("header", &format!("{} headers", self.header.len()))
103            .finish()
104    }
105}
106
107impl Default for Config {
108    fn default() -> Self {
109        Self {
110            inner: Arc::new(ConfigInner::default()),
111        }
112    }
113}
114
115impl Deref for Config {
116    type Target = ConfigInner;
117
118    fn deref(&self) -> &Self::Target {
119        &self.inner
120    }
121}
122
123impl Config {
124    /// 创建配置构建器
125    pub fn builder() -> ConfigBuilder {
126        ConfigBuilder::default()
127    }
128
129    /// 创建新的 Config 实例,直接从 ConfigInner
130    pub fn new(inner: ConfigInner) -> Self {
131        Self {
132            inner: Arc::new(inner),
133        }
134    }
135
136    /// 基于当前配置生成一个“替换 TokenProvider”的新配置
137    ///
138    /// 说明:
139    /// - 这是一个纯拷贝操作(`Config` 本身是 `Arc` 包装),不会修改原配置
140    /// - 推荐用法:先构建一个“基础 Config”(默认 `NoOpTokenProvider`),再用该基础 Config 构建业务 TokenProvider,
141    ///   最后调用此方法把 provider 注入到“业务 Config”中,避免循环引用。
142    pub fn with_token_provider(&self, provider: impl TokenProvider + 'static) -> Self {
143        Config::new(ConfigInner {
144            app_id: self.app_id.clone(),
145            app_secret: self.app_secret.clone(),
146            base_url: self.base_url.clone(),
147            enable_token_cache: self.enable_token_cache,
148            app_type: self.app_type,
149            http_client: self.http_client.clone(),
150            req_timeout: self.req_timeout,
151            header: self.header.clone(),
152            token_provider: Arc::new(provider),
153            max_response_size: self.max_response_size,
154            retry_count: self.retry_count,
155            enable_log: self.enable_log,
156        })
157    }
158
159    /// 获取内部 Arc 的引用计数
160    pub fn reference_count(&self) -> usize {
161        Arc::strong_count(&self.inner)
162    }
163
164    /// 获取应用 ID
165    pub fn app_id(&self) -> &str {
166        &self.inner.app_id
167    }
168
169    /// 获取应用密钥
170    pub fn app_secret(&self) -> &str {
171        &self.inner.app_secret
172    }
173
174    /// 获取基础 URL
175    pub fn base_url(&self) -> &str {
176        &self.inner.base_url
177    }
178
179    /// 获取超时时间
180    pub fn req_timeout(&self) -> Option<Duration> {
181        self.inner.req_timeout
182    }
183
184    /// 是否启用令牌缓存
185    pub fn enable_token_cache(&self) -> bool {
186        self.inner.enable_token_cache
187    }
188
189    /// 获取应用类型
190    pub fn app_type(&self) -> AppType {
191        self.inner.app_type
192    }
193
194    /// 获取 HTTP 客户端引用
195    pub fn http_client(&self) -> &reqwest::Client {
196        &self.inner.http_client
197    }
198
199    /// 获取自定义 header 引用
200    pub fn header(&self) -> &HashMap<String, String> {
201        &self.inner.header
202    }
203
204    /// 获取 TokenProvider 引用
205    pub fn token_provider(&self) -> &Arc<dyn TokenProvider> {
206        &self.inner.token_provider
207    }
208
209    /// 获取响应体最大大小限制
210    pub fn max_response_size(&self) -> u64 {
211        self.inner.max_response_size
212    }
213
214    /// 获取重试次数
215    pub fn retry_count(&self) -> u32 {
216        self.inner.retry_count
217    }
218
219    /// 是否启用日志记录
220    pub fn enable_log(&self) -> bool {
221        self.inner.enable_log
222    }
223}
224
225/// 配置构建器
226#[derive(Default, Clone)]
227pub struct ConfigBuilder {
228    app_id: Option<String>,
229    app_secret: Option<String>,
230    base_url: Option<String>,
231    enable_token_cache: Option<bool>,
232    app_type: Option<AppType>,
233    http_client: Option<reqwest::Client>,
234    req_timeout: Option<Duration>,
235    header: Option<HashMap<String, String>>,
236    token_provider: Option<Arc<dyn TokenProvider>>,
237    max_response_size: Option<u64>,
238    retry_count: Option<u32>,
239    enable_log: Option<bool>,
240}
241
242impl ConfigBuilder {
243    /// 设置应用 ID
244    pub fn app_id(mut self, app_id: impl Into<String>) -> Self {
245        self.app_id = Some(app_id.into());
246        self
247    }
248
249    /// 设置应用密钥
250    pub fn app_secret(mut self, app_secret: impl Into<String>) -> Self {
251        self.app_secret = Some(app_secret.into());
252        self
253    }
254
255    /// 设置基础 URL
256    pub fn base_url(mut self, base_url: impl Into<String>) -> Self {
257        self.base_url = Some(base_url.into());
258        self
259    }
260
261    /// 设置是否启用令牌缓存
262    pub fn enable_token_cache(mut self, enable: bool) -> Self {
263        self.enable_token_cache = Some(enable);
264        self
265    }
266
267    /// 设置应用类型
268    pub fn app_type(mut self, app_type: AppType) -> Self {
269        self.app_type = Some(app_type);
270        self
271    }
272
273    /// 设置 HTTP 客户端
274    pub fn http_client(mut self, client: reqwest::Client) -> Self {
275        self.http_client = Some(client);
276        self
277    }
278
279    /// 使用优化的HTTP配置构建客户端
280    pub fn optimized_http_client(
281        mut self,
282        config: OptimizedHttpConfig,
283    ) -> Result<Self, reqwest::Error> {
284        let client = config.build_client()?;
285        self.http_client = Some(client);
286        Ok(self)
287    }
288
289    /// 使用生产环境优化配置
290    pub fn production_http_client(self) -> Result<Self, reqwest::Error> {
291        let config = OptimizedHttpConfig::production();
292        self.optimized_http_client(config)
293    }
294
295    /// 使用高吞吐量配置
296    pub fn high_throughput_http_client(self) -> Result<Self, reqwest::Error> {
297        let config = OptimizedHttpConfig::high_throughput();
298        self.optimized_http_client(config)
299    }
300
301    /// 使用低延迟配置
302    pub fn low_latency_http_client(self) -> Result<Self, reqwest::Error> {
303        let config = OptimizedHttpConfig::low_latency();
304        self.optimized_http_client(config)
305    }
306
307    /// 设置请求超时时间
308    pub fn req_timeout(mut self, timeout: Duration) -> Self {
309        self.req_timeout = Some(timeout);
310        self
311    }
312
313    /// 设置自定义 HTTP 头
314    pub fn header(mut self, header: HashMap<String, String>) -> Self {
315        self.header = Some(header);
316        self
317    }
318
319    /// 设置令牌提供者
320    pub fn token_provider(mut self, provider: impl TokenProvider + 'static) -> Self {
321        self.token_provider = Some(Arc::new(provider));
322        self
323    }
324
325    /// 设置响应体最大大小限制(字节),默认 100MB
326    pub fn max_response_size(mut self, size: u64) -> Self {
327        self.max_response_size = Some(size);
328        self
329    }
330
331    /// 设置默认重试次数,默认 3
332    pub fn retry_count(mut self, count: u32) -> Self {
333        self.retry_count = Some(count);
334        self
335    }
336
337    /// 设置是否启用日志记录,默认 true
338    pub fn enable_log(mut self, enable: bool) -> Self {
339        self.enable_log = Some(enable);
340        self
341    }
342
343    /// 构建 Config 实例
344    pub fn build(self) -> Config {
345        let default = ConfigInner::default();
346        Config::new(ConfigInner {
347            app_id: self.app_id.unwrap_or(default.app_id),
348            app_secret: self.app_secret.unwrap_or(default.app_secret),
349            base_url: self.base_url.unwrap_or(default.base_url),
350            enable_token_cache: self
351                .enable_token_cache
352                .unwrap_or(default.enable_token_cache),
353            app_type: self.app_type.unwrap_or(default.app_type),
354            http_client: self.http_client.unwrap_or(default.http_client),
355            req_timeout: self.req_timeout.or(default.req_timeout),
356            header: self.header.unwrap_or(default.header),
357            token_provider: self.token_provider.unwrap_or(default.token_provider),
358            max_response_size: self.max_response_size.unwrap_or(default.max_response_size),
359            retry_count: self.retry_count.unwrap_or(default.retry_count),
360            enable_log: self.enable_log.unwrap_or(default.enable_log),
361        })
362    }
363}
364
365#[cfg(test)]
366mod tests {
367    use super::*;
368    use crate::auth::NoOpTokenProvider;
369    use crate::auth::TokenProvider;
370    use crate::auth::TokenRequest;
371    use crate::constants::{AppType, FEISHU_BASE_URL};
372    use std::time::Duration;
373    use std::{future::Future, pin::Pin};
374
375    #[test]
376    fn test_config_creation() {
377        let config = Config::new(ConfigInner {
378            app_id: "test_app_id".to_string(),
379            app_secret: "test_app_secret".to_string(),
380            base_url: "https://test.api.com".to_string(),
381            enable_token_cache: true,
382            app_type: AppType::SelfBuild,
383            http_client: reqwest::Client::new(),
384            req_timeout: Some(Duration::from_secs(30)),
385            header: HashMap::new(),
386            token_provider: Arc::new(NoOpTokenProvider),
387            max_response_size: 100 * 1024 * 1024,
388            retry_count: 3,
389            enable_log: true,
390        });
391
392        assert_eq!(config.app_id, "test_app_id");
393        assert_eq!(config.app_secret, "test_app_secret");
394        assert_eq!(config.base_url, "https://test.api.com");
395        assert!(config.enable_token_cache);
396        assert_eq!(config.req_timeout, Some(Duration::from_secs(30)));
397    }
398
399    #[test]
400    fn test_config_default() {
401        let config = Config::default();
402
403        assert_eq!(config.app_id, "");
404        assert_eq!(config.app_secret, "");
405        assert_eq!(config.base_url, FEISHU_BASE_URL);
406        assert!(config.enable_token_cache);
407        assert_eq!(config.app_type, AppType::SelfBuild);
408        assert!(config.req_timeout.is_none());
409        assert!(config.header.is_empty());
410    }
411
412    #[test]
413    fn test_config_clone() {
414        let config = Config::new(ConfigInner {
415            app_id: "clone_test".to_string(),
416            app_secret: "clone_secret".to_string(),
417            base_url: "https://clone.test.com".to_string(),
418            enable_token_cache: false,
419            app_type: AppType::Marketplace,
420            http_client: reqwest::Client::new(),
421            req_timeout: Some(Duration::from_secs(60)),
422            header: {
423                let mut header = HashMap::new();
424                header.insert("Test-Header".to_string(), "test-value".to_string());
425                header
426            },
427            token_provider: Arc::new(NoOpTokenProvider),
428            max_response_size: 100 * 1024 * 1024,
429            retry_count: 3,
430            enable_log: true,
431        });
432
433        let cloned_config = config.clone();
434
435        assert_eq!(config.app_id, cloned_config.app_id);
436        assert_eq!(config.app_secret, cloned_config.app_secret);
437        assert_eq!(config.base_url, cloned_config.base_url);
438        assert_eq!(config.enable_token_cache, cloned_config.enable_token_cache);
439        assert_eq!(config.app_type, cloned_config.app_type);
440        assert_eq!(config.req_timeout, cloned_config.req_timeout);
441        assert_eq!(config.header.len(), cloned_config.header.len());
442        assert_eq!(
443            config.header.get("Test-Header"),
444            cloned_config.header.get("Test-Header")
445        );
446
447        // Verify Arc clone efficiency - both should point to same memory
448        assert!(Arc::ptr_eq(&config.inner, &cloned_config.inner));
449
450        // Verify reference counting works
451        assert_eq!(config.reference_count(), 2);
452    }
453
454    #[test]
455    fn test_config_debug() {
456        let config = Config::default();
457        let debug_str = format!("{config:?}");
458
459        assert!(debug_str.contains("Config"));
460        assert!(debug_str.contains("app_id"));
461        assert!(debug_str.contains("app_secret"));
462        assert!(debug_str.contains("base_url"));
463    }
464
465    #[test]
466    fn test_config_with_custom_header() {
467        let mut header = HashMap::new();
468        header.insert("Authorization".to_string(), "Bearer token".to_string());
469        header.insert("Content-Type".to_string(), "application/json".to_string());
470
471        let config = Config::new(ConfigInner {
472            header,
473            ..ConfigInner::default()
474        });
475
476        assert_eq!(config.header.len(), 2);
477        assert_eq!(
478            config.header.get("Authorization"),
479            Some(&"Bearer token".to_string())
480        );
481        assert_eq!(
482            config.header.get("Content-Type"),
483            Some(&"application/json".to_string())
484        );
485    }
486
487    #[test]
488    fn test_config_with_different_app_types() {
489        let self_build_config = Config::new(ConfigInner {
490            app_type: AppType::SelfBuild,
491            ..ConfigInner::default()
492        });
493
494        let marketplace_config = Config::new(ConfigInner {
495            app_type: AppType::Marketplace,
496            ..ConfigInner::default()
497        });
498
499        assert_eq!(self_build_config.app_type, AppType::SelfBuild);
500        assert_eq!(marketplace_config.app_type, AppType::Marketplace);
501        assert_ne!(self_build_config.app_type, marketplace_config.app_type);
502    }
503
504    #[test]
505    fn test_config_with_timeout_variations() {
506        let no_timeout_config = Config::default();
507
508        let short_timeout_config = Config::new(ConfigInner {
509            req_timeout: Some(Duration::from_secs(5)),
510            ..ConfigInner::default()
511        });
512
513        let long_timeout_config = Config::new(ConfigInner {
514            req_timeout: Some(Duration::from_secs(300)),
515            ..ConfigInner::default()
516        });
517
518        assert!(no_timeout_config.req_timeout.is_none());
519        assert_eq!(
520            short_timeout_config.req_timeout,
521            Some(Duration::from_secs(5))
522        );
523        assert_eq!(
524            long_timeout_config.req_timeout,
525            Some(Duration::from_secs(300))
526        );
527    }
528
529    #[test]
530    fn test_config_builders() {
531        let config = Config::builder()
532            .app_id("test_app")
533            .app_secret("test_secret")
534            .build();
535
536        assert_eq!(config.app_id, "test_app");
537        assert_eq!(config.app_secret, "test_secret");
538    }
539
540    #[test]
541    fn test_config_arc_efficiency() {
542        let config = Config::default();
543        assert_eq!(config.reference_count(), 1);
544
545        let config_clone = config.clone();
546        assert_eq!(config.reference_count(), 2);
547        assert_eq!(config_clone.reference_count(), 2);
548
549        // Both configs should point to the same inner data
550        assert!(Arc::ptr_eq(&config.inner, &config_clone.inner));
551    }
552
553    #[test]
554    fn test_arc_efficiency_simulation() {
555        // 模拟服务模块中的多次克隆
556        let config = Config::default();
557
558        // 模拟 PerformanceService::new() 中的4次clone
559        let service1_config = config.clone();
560        let service2_config = config.clone();
561        let service3_config = config.clone();
562        let service4_config = config.clone();
563
564        // 所有配置应该指向同一个内存位置
565        assert!(Arc::ptr_eq(&config.inner, &service1_config.inner));
566        assert!(Arc::ptr_eq(&config.inner, &service2_config.inner));
567        assert!(Arc::ptr_eq(&config.inner, &service3_config.inner));
568        assert!(Arc::ptr_eq(&config.inner, &service4_config.inner));
569
570        // 引用计数应该是5(原始 + 4个克隆)
571        assert_eq!(config.reference_count(), 5);
572
573        println!("Arc<Config> 改造成功:5个配置实例共享同一份内存!");
574    }
575
576    #[derive(Debug)]
577    struct TestTokenProvider;
578
579    impl TokenProvider for TestTokenProvider {
580        fn get_token(
581            &self,
582            _request: TokenRequest,
583        ) -> Pin<Box<dyn Future<Output = crate::SDKResult<String>> + Send + '_>> {
584            Box::pin(async { Ok("test_token".to_string()) })
585        }
586    }
587
588    #[tokio::test]
589    async fn test_with_token_provider() {
590        let base = Config::builder()
591            .app_id("test_app")
592            .app_secret("test_secret")
593            .build();
594
595        let config = base.with_token_provider(TestTokenProvider);
596
597        let token = config
598            .token_provider
599            .get_token(TokenRequest::app())
600            .await
601            .unwrap();
602        assert_eq!(token, "test_token");
603    }
604}