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