Skip to main content

openlark_client/
config.rs

1//! OpenLark Client 配置管理
2//!
3//! 提供灵活的配置系统,支持环境变量、验证和默认值
4
5use crate::Result;
6use std::time::Duration;
7
8use openlark_core::config::Config as CoreConfig;
9use openlark_core::constants::AppType;
10
11/// 🔧 OpenLark客户端配置
12///
13/// 支持从环境变量自动加载配置
14///
15/// # 环境变量
16/// - `OPENLARK_APP_ID`: 应用ID(必需)
17/// - `OPENLARK_APP_SECRET`: 应用密钥(必需)
18/// - `OPENLARK_APP_TYPE`: 应用类型(可选:self_build / marketplace,默认 self_build)
19/// - `OPENLARK_BASE_URL`: API基础URL(可选,默认:`https://open.feishu.cn`,国际版 Lark 使用 `https://open.larksuite.com`)
20/// - `OPENLARK_ENABLE_TOKEN_CACHE`: 是否允许自动获取 token(可选,默认 true)
21///
22/// # 示例
23/// ```rust,no_run
24/// use openlark_client::Config;
25///
26/// // 从环境变量创建配置
27/// let config = Config::from_env();
28///
29/// // 手动构建配置
30/// let config = Config::builder()
31///     .app_id("your_app_id")
32///     .app_secret("your_app_secret")
33///     .base_url("https://open.feishu.cn")  // 默认值,国际版 Lark 使用 https://open.larksuite.com
34///     .build();
35/// ```
36#[derive(Debug, Clone)]
37pub struct Config {
38    /// 🆔 飞书应用ID
39    pub app_id: String,
40    /// 🔑 飞书应用密钥
41    pub app_secret: String,
42    /// 🏷️ 应用类型(自建 / 商店)
43    pub app_type: AppType,
44    /// 🔐 是否允许 SDK 自动获取 token(通过 openlark-core 的 TokenProvider)
45    pub enable_token_cache: bool,
46    /// 🌐 API基础URL
47    pub base_url: String,
48    /// ⏱️ 请求超时时间
49    pub timeout: Duration,
50    /// 🔄 默认重试次数
51    pub retry_count: u32,
52    /// 📝 是否启用日志记录
53    pub enable_log: bool,
54    /// 📋 自定义HTTP headers
55    pub headers: std::collections::HashMap<String, String>,
56    /// 🔧 底层 core 配置(按需生成)
57    #[doc(hidden)]
58    pub(crate) core_config: Option<CoreConfig>,
59}
60
61impl Default for Config {
62    fn default() -> Self {
63        Self {
64            app_id: String::new(),
65            app_secret: String::new(),
66            app_type: AppType::SelfBuild,
67            enable_token_cache: true,
68            base_url: "https://open.feishu.cn".to_string(),
69            timeout: Duration::from_secs(30),
70            retry_count: 3,
71            enable_log: true,
72            headers: std::collections::HashMap::new(),
73            core_config: None,
74        }
75    }
76}
77
78impl Config {
79    /// 🆕 创建新的配置实例
80    pub fn new() -> Self {
81        Self::default()
82    }
83
84    /// 🌍 从环境变量创建配置
85    ///
86    /// # 环境变量
87    /// - `OPENLARK_APP_ID`: 应用ID(必需)
88    /// - `OPENLARK_APP_SECRET`: 应用密钥(必需)
89    /// - `OPENLARK_APP_TYPE`: 应用类型(可选:self_build / marketplace)
90    /// - `OPENLARK_BASE_URL`: API基础URL(可选,默认 `https://open.feishu.cn`,国际版 Lark 使用 `https://open.larksuite.com`)
91    /// - `OPENLARK_ENABLE_TOKEN_CACHE`: 是否允许自动获取 token(可选)
92    ///
93    /// # 返回值
94    /// 返回配置实例,环境变量缺失时使用默认值
95    pub fn from_env() -> Self {
96        let mut config = Self::default();
97        config.load_from_env();
98        config
99    }
100
101    /// 📥 从环境变量加载配置到当前实例
102    ///
103    /// 只设置存在且非空的环境变量
104    pub fn load_from_env(&mut self) {
105        for (key, value) in std::env::vars() {
106            self.apply_env_var(&key, &value);
107        }
108    }
109
110    fn apply_env_var(&mut self, key: &str, value: &str) {
111        match key {
112            "OPENLARK_APP_ID" => {
113                if !value.is_empty() {
114                    self.app_id = value.to_string();
115                }
116            }
117            "OPENLARK_APP_SECRET" => {
118                if !value.is_empty() {
119                    self.app_secret = value.to_string();
120                }
121            }
122            "OPENLARK_APP_TYPE" => {
123                let v = value.trim().to_lowercase();
124                match v.as_str() {
125                    "self_build" | "selfbuild" | "self" => self.app_type = AppType::SelfBuild,
126                    "marketplace" | "store" => self.app_type = AppType::Marketplace,
127                    _ => {}
128                }
129            }
130            "OPENLARK_BASE_URL" => {
131                if !value.is_empty() {
132                    self.base_url = value.to_string();
133                }
134            }
135            "OPENLARK_ENABLE_TOKEN_CACHE" => {
136                let s = value.trim().to_lowercase();
137                if !s.is_empty() {
138                    self.enable_token_cache = !(s.starts_with('f') || s == "0");
139                }
140            }
141            "OPENLARK_TIMEOUT" => {
142                if let Ok(timeout_secs) = value.parse::<u64>() {
143                    self.timeout = Duration::from_secs(timeout_secs);
144                }
145            }
146            "OPENLARK_RETRY_COUNT" => {
147                if let Ok(retry_count) = value.parse::<u32>() {
148                    self.retry_count = retry_count;
149                }
150            }
151            // 日志开关(默认启用,只有设置为"false"时才禁用)
152            "OPENLARK_ENABLE_LOG" => {
153                self.enable_log = !value.to_lowercase().starts_with('f');
154            }
155            _ => {}
156        }
157    }
158
159    /// ✅ 验证配置的有效性
160    ///
161    /// # 验证规则
162    /// - app_id和app_secret不能为空
163    /// - base_url必须是有效的HTTP/HTTPS URL
164    /// - timeout必须大于0
165    /// - retry_count不能超过合理的范围
166    ///
167    /// # 错误
168    /// 返回验证失败的详细错误信息
169    pub fn validate(&self) -> Result<()> {
170        if self.app_id.is_empty() {
171            return Err(crate::error::validation_error("app_id", "app_id不能为空"));
172        }
173
174        if self.app_secret.is_empty() {
175            return Err(crate::error::validation_error(
176                "app_secret",
177                "app_secret不能为空",
178            ));
179        }
180
181        if self.base_url.is_empty() {
182            return Err(crate::error::validation_error(
183                "base_url",
184                "base_url不能为空",
185            ));
186        }
187
188        // 验证URL格式
189        if !self.base_url.starts_with("http://") && !self.base_url.starts_with("https://") {
190            return Err(crate::error::validation_error(
191                "base_url",
192                "base_url必须以http://或https://开头",
193            ));
194        }
195
196        // 验证超时时间
197        if self.timeout.is_zero() {
198            return Err(crate::error::validation_error(
199                "timeout",
200                "timeout必须大于0",
201            ));
202        }
203
204        // 验证重试次数
205        if self.retry_count > 10 {
206            return Err(crate::error::validation_error(
207                "retry_count",
208                "retry_count不能超过10",
209            ));
210        }
211
212        Ok(())
213    }
214
215    /// 🏗️ 创建配置构建器
216    pub fn builder() -> ConfigBuilder {
217        ConfigBuilder::new()
218    }
219
220    /// 🔧 添加自定义HTTP header
221    pub fn add_header<K, V>(&mut self, key: K, value: V)
222    where
223        K: Into<String>,
224        V: Into<String>,
225    {
226        self.headers.insert(key.into(), value.into());
227    }
228
229    /// 🧹 清除所有自定义headers
230    pub fn clear_headers(&mut self) {
231        self.headers.clear();
232    }
233
234    /// 🔍 检查配置是否完整(可用于API调用)
235    pub fn is_complete(&self) -> bool {
236        !self.app_id.is_empty() && !self.app_secret.is_empty()
237    }
238
239    /// 📋 获取配置摘要(不包含敏感信息)
240    pub fn summary(&self) -> ConfigSummary {
241        ConfigSummary {
242            app_id: self.app_id.clone(),
243            app_secret_set: !self.app_secret.is_empty(),
244            app_type: self.app_type,
245            enable_token_cache: self.enable_token_cache,
246            base_url: self.base_url.clone(),
247            timeout: self.timeout,
248            retry_count: self.retry_count,
249            enable_log: self.enable_log,
250            header_count: self.headers.len(),
251        }
252    }
253
254    /// 🔄 更新配置,只更新非空字段
255    pub fn update_with(&mut self, other: &Config) {
256        if !other.app_id.is_empty() {
257            self.app_id = other.app_id.clone();
258        }
259        if !other.app_secret.is_empty() {
260            self.app_secret = other.app_secret.clone();
261        }
262        if other.app_type != AppType::SelfBuild {
263            self.app_type = other.app_type;
264        }
265        if other.enable_token_cache != self.enable_token_cache {
266            self.enable_token_cache = other.enable_token_cache;
267        }
268        if !other.base_url.is_empty() {
269            self.base_url = other.base_url.clone();
270        }
271        if !other.timeout.is_zero() {
272            self.timeout = other.timeout;
273        }
274        if other.retry_count != 3 {
275            self.retry_count = other.retry_count;
276        }
277        if other.enable_log != self.enable_log {
278            self.enable_log = other.enable_log;
279        }
280        // 合并headers
281        for (key, value) in &other.headers {
282            self.headers.insert(key.clone(), value.clone());
283        }
284    }
285
286    /// 🔧 构建底层 core 配置(不含 TokenProvider)
287    pub fn build_core_config(&self) -> CoreConfig {
288        CoreConfig::builder()
289            .app_id(self.app_id.clone())
290            .app_secret(self.app_secret.clone())
291            .base_url(self.base_url.clone())
292            .app_type(self.app_type)
293            .enable_token_cache(self.enable_token_cache)
294            .req_timeout(self.timeout)
295            .header(self.headers.clone())
296            .build()
297    }
298
299    /// 🔧 构建带有默认 TokenProvider 的 core 配置
300    #[cfg(feature = "auth")]
301    pub fn build_core_config_with_token_provider(&self) -> CoreConfig {
302        use openlark_auth::AuthTokenProvider;
303        let base_config = self.build_core_config();
304        let provider = AuthTokenProvider::new(base_config.clone());
305        base_config.with_token_provider(provider)
306    }
307
308    /// 🔧 获取或构建 core 配置
309    pub fn get_or_build_core_config(&self) -> CoreConfig {
310        if let Some(ref core_config) = self.core_config {
311            return core_config.clone();
312        }
313        self.build_core_config()
314    }
315
316    /// 🔧 获取或构建带有 TokenProvider 的 core 配置
317    #[cfg(feature = "auth")]
318    pub fn get_or_build_core_config_with_token_provider(&self) -> CoreConfig {
319        if let Some(ref core_config) = self.core_config {
320            return core_config.clone();
321        }
322        self.build_core_config_with_token_provider()
323    }
324}
325
326/// 🏗️ 配置构建器 - 提供流畅的API
327///
328/// # 示例
329/// ```rust,no_run
330/// use openlark_client::Config;
331/// use std::time::Duration;
332///
333/// let config = Config::builder()
334///     .app_id("your_app_id")
335///     .app_secret("your_app_secret")
336///     .base_url("https://open.feishu.cn")
337///     .timeout(Duration::from_secs(60))
338///     .retry_count(5)
339///     .enable_log(true)
340///     .add_header("X-Custom-Header", "value")
341///     .build();
342/// ```
343#[derive(Debug, Clone)]
344pub struct ConfigBuilder {
345    config: Config,
346}
347
348impl ConfigBuilder {
349    /// 🆕 创建新的构建器
350    pub fn new() -> Self {
351        Self {
352            config: Config::default(),
353        }
354    }
355
356    /// 🆔 设置应用ID
357    pub fn app_id<S: Into<String>>(mut self, app_id: S) -> Self {
358        self.config.app_id = app_id.into();
359        self
360    }
361
362    /// 🔑 设置应用密钥
363    pub fn app_secret<S: Into<String>>(mut self, app_secret: S) -> Self {
364        self.config.app_secret = app_secret.into();
365        self
366    }
367
368    /// 🏷️ 设置应用类型(自建 / 商店)
369    pub fn app_type(mut self, app_type: AppType) -> Self {
370        self.config.app_type = app_type;
371        self
372    }
373
374    /// 🔐 设置是否允许自动获取 token(默认 true)
375    pub fn enable_token_cache(mut self, enable: bool) -> Self {
376        self.config.enable_token_cache = enable;
377        self
378    }
379
380    /// 🌐 设置API基础URL
381    pub fn base_url<S: Into<String>>(mut self, base_url: S) -> Self {
382        self.config.base_url = base_url.into();
383        self
384    }
385
386    /// ⏱️ 设置请求超时时间
387    pub fn timeout(mut self, timeout: Duration) -> Self {
388        self.config.timeout = timeout;
389        self
390    }
391
392    /// 🔄 设置重试次数
393    pub fn retry_count(mut self, count: u32) -> Self {
394        self.config.retry_count = count;
395        self
396    }
397
398    /// 📝 设置是否启用日志
399    pub fn enable_log(mut self, enable: bool) -> Self {
400        self.config.enable_log = enable;
401        self
402    }
403
404    /// 🔧 添加自定义HTTP header
405    pub fn add_header<K, V>(mut self, key: K, value: V) -> Self
406    where
407        K: Into<String>,
408        V: Into<String>,
409    {
410        self.config.add_header(key, value);
411        self
412    }
413
414    /// 🌍 从环境变量加载配置
415    pub fn from_env(mut self) -> Self {
416        self.config.load_from_env();
417        self
418    }
419
420    /// 🔨 构建最终的配置实例
421    ///
422    /// # 错误
423    /// 如果配置验证失败,返回相应的错误
424    pub fn build(self) -> Result<Config> {
425        self.config.validate()?;
426        Ok(self.config)
427    }
428
429    /// 🔨 构建配置实例(跳过验证)
430    ///
431    /// 注意:使用此方法时配置可能无效,后续使用时需要验证
432    pub fn build_unvalidated(self) -> Config {
433        self.config
434    }
435}
436
437impl Default for ConfigBuilder {
438    fn default() -> Self {
439        Self::new()
440    }
441}
442
443/// 📋 配置摘要(不包含敏感信息)
444#[derive(Debug, Clone)]
445pub struct ConfigSummary {
446    /// 🆔 应用ID
447    pub app_id: String,
448    /// 🔑 应用密钥是否已设置
449    pub app_secret_set: bool,
450    /// 🏷️ 应用类型
451    pub app_type: AppType,
452    /// 🔐 是否允许自动获取 token
453    pub enable_token_cache: bool,
454    /// 🌐 API基础URL
455    pub base_url: String,
456    /// ⏱️ 请求超时时间
457    pub timeout: Duration,
458    /// 🔄 重试次数
459    pub retry_count: u32,
460    /// 📝 是否启用日志
461    pub enable_log: bool,
462    /// 📋 自定义headers数量
463    pub header_count: usize,
464}
465
466impl ConfigSummary {
467    /// 📋 获取友好的配置描述
468    pub fn friendly_description(&self) -> String {
469        format!(
470            "应用ID: {}, 基础URL: {}, 超时: {:?}, 重试: {}, 日志: {}, Headers: {}",
471            self.app_id,
472            self.base_url,
473            self.timeout,
474            self.retry_count,
475            if self.enable_log { "启用" } else { "禁用" },
476            self.header_count
477        )
478    }
479}
480
481impl std::fmt::Display for ConfigSummary {
482    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
483        write!(
484            f,
485            "Config {{ app_id: {}, app_secret_set: {}, base_url: {}, timeout: {:?}, retry_count: {}, enable_log: {}, header_count: {} }}",
486            self.app_id,
487            self.app_secret_set,
488            self.base_url,
489            self.timeout,
490            self.retry_count,
491            self.enable_log,
492            self.header_count
493        )
494    }
495}
496
497/// 从环境变量创建配置的便利方法
498impl From<std::env::Vars> for Config {
499    fn from(env_vars: std::env::Vars) -> Self {
500        let mut config = Config::default();
501        for (key, value) in env_vars {
502            config.apply_env_var(&key, &value);
503        }
504        config
505    }
506}
507
508#[cfg(test)]
509#[allow(unused_imports)]
510mod tests {
511    use super::*;
512    use std::time::Duration;
513
514    #[test]
515    fn test_config_default() {
516        let config = Config::default();
517        assert_eq!(config.app_id, "");
518        assert_eq!(config.app_secret, "");
519        assert_eq!(config.base_url, "https://open.feishu.cn");
520        assert_eq!(config.timeout, Duration::from_secs(30));
521        assert_eq!(config.retry_count, 3);
522        assert!(config.enable_log);
523        assert!(config.headers.is_empty());
524    }
525
526    #[test]
527    fn test_config_builder() {
528        let config = Config::builder()
529            .app_id("test_app_id")
530            .app_secret("test_app_secret")
531            .base_url("https://test.feishu.cn")
532            .timeout(Duration::from_secs(60))
533            .retry_count(5)
534            .enable_log(false)
535            .build();
536
537        assert!(config.is_ok());
538        let config = config.unwrap();
539        assert_eq!(config.app_id, "test_app_id");
540        assert_eq!(config.app_secret, "test_app_secret");
541        assert_eq!(config.base_url, "https://test.feishu.cn");
542        assert_eq!(config.timeout, Duration::from_secs(60));
543        assert_eq!(config.retry_count, 5);
544        assert!(!config.enable_log);
545    }
546
547    #[test]
548    fn test_config_from_env() {
549        crate::test_utils::with_env_vars(
550            &[
551                ("OPENLARK_APP_ID", Some("test_app_id")),
552                ("OPENLARK_APP_SECRET", Some("test_app_secret")),
553                ("OPENLARK_APP_TYPE", Some("marketplace")),
554                ("OPENLARK_BASE_URL", Some("https://test.feishu.cn")),
555                ("OPENLARK_ENABLE_TOKEN_CACHE", Some("false")),
556                ("OPENLARK_TIMEOUT", Some("60")),
557                ("OPENLARK_RETRY_COUNT", Some("5")),
558                ("OPENLARK_ENABLE_LOG", Some("false")),
559            ],
560            || {
561                let config = Config::from_env();
562                assert_eq!(config.app_id, "test_app_id");
563                assert_eq!(config.app_secret, "test_app_secret");
564                assert_eq!(config.app_type, AppType::Marketplace);
565                assert_eq!(config.base_url, "https://test.feishu.cn");
566                assert!(!config.enable_token_cache);
567                assert_eq!(config.timeout, Duration::from_secs(60));
568                assert_eq!(config.retry_count, 5);
569                assert!(!config.enable_log);
570            },
571        );
572    }
573
574    #[test]
575    fn test_config_validation() {
576        // 有效配置
577        let config = Config {
578            app_id: "test_app_id".to_string(),
579            app_secret: "test_app_secret".to_string(),
580            app_type: AppType::SelfBuild,
581            enable_token_cache: true,
582            base_url: "https://open.feishu.cn".to_string(),
583            timeout: Duration::from_secs(30),
584            retry_count: 3,
585            enable_log: true,
586            headers: std::collections::HashMap::new(),
587            core_config: None,
588        };
589        assert!(config.validate().is_ok());
590
591        // 无效配置 - 空app_id
592        let invalid_config = Config {
593            app_id: String::new(),
594            ..config.clone()
595        };
596        assert!(invalid_config.validate().is_err());
597
598        // 无效配置 - 空app_secret
599        let invalid_config = Config {
600            app_secret: String::new(),
601            ..config
602        };
603        assert!(invalid_config.validate().is_err());
604    }
605
606    #[test]
607    fn test_config_headers() {
608        let mut config = Config::default();
609
610        // 添加header
611        config.add_header("X-Custom-Header", "custom-value");
612        assert_eq!(config.headers.len(), 1);
613        assert_eq!(
614            config.headers.get("X-Custom-Header"),
615            Some(&"custom-value".to_string())
616        );
617
618        // 清除headers
619        config.clear_headers();
620        assert!(config.headers.is_empty());
621    }
622
623    #[test]
624    fn test_config_update_with() {
625        let mut base_config = Config::default();
626        let update_config = Config {
627            app_id: "updated_app_id".to_string(),
628            app_secret: "updated_app_secret".to_string(),
629            timeout: Duration::from_secs(60),
630            ..Default::default()
631        };
632
633        base_config.update_with(&update_config);
634        assert_eq!(base_config.app_id, "updated_app_id");
635        assert_eq!(base_config.app_secret, "updated_app_secret");
636        assert_eq!(base_config.timeout, Duration::from_secs(60));
637        // 其他字段保持默认值
638        assert_eq!(base_config.base_url, "https://open.feishu.cn");
639    }
640
641    #[test]
642    fn test_config_summary() {
643        let config = Config {
644            app_id: "test_app_id".to_string(),
645            app_secret: "test_app_secret".to_string(),
646            app_type: AppType::SelfBuild,
647            enable_token_cache: true,
648            base_url: "https://open.feishu.cn".to_string(),
649            timeout: Duration::from_secs(30),
650            retry_count: 3,
651            enable_log: true,
652            headers: std::collections::HashMap::new(),
653            core_config: None,
654        };
655
656        let summary = config.summary();
657        assert_eq!(summary.app_id, "test_app_id");
658        assert!(summary.app_secret_set);
659        assert_eq!(summary.base_url, "https://open.feishu.cn");
660        assert_eq!(summary.timeout, Duration::from_secs(30));
661        assert_eq!(summary.retry_count, 3);
662        assert!(summary.enable_log);
663        assert_eq!(summary.header_count, 0);
664    }
665
666    #[test]
667    fn test_config_is_complete() {
668        let mut config = Config::default();
669        assert!(!config.is_complete());
670
671        config.app_id = "test_app_id".to_string();
672        assert!(!config.is_complete());
673
674        config.app_secret = "test_app_secret".to_string();
675        assert!(config.is_complete());
676    }
677}