1use crate::Result;
6use std::time::Duration;
7
8use openlark_core::config::Config as CoreConfig;
9use openlark_core::constants::AppType;
10
11#[derive(Debug, Clone)]
37pub struct Config {
38 pub app_id: String,
40 pub app_secret: String,
42 pub app_type: AppType,
44 pub enable_token_cache: bool,
46 pub base_url: String,
48 pub timeout: Duration,
50 pub retry_count: u32,
52 pub enable_log: bool,
54 pub headers: std::collections::HashMap<String, String>,
56 #[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 pub fn new() -> Self {
81 Self::default()
82 }
83
84 pub fn from_env() -> Self {
96 let mut config = Self::default();
97 config.load_from_env();
98 config
99 }
100
101 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 "OPENLARK_ENABLE_LOG" => {
153 self.enable_log = !value.to_lowercase().starts_with('f');
154 }
155 _ => {}
156 }
157 }
158
159 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 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 if self.timeout.is_zero() {
198 return Err(crate::error::validation_error(
199 "timeout",
200 "timeout必须大于0",
201 ));
202 }
203
204 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 pub fn builder() -> ConfigBuilder {
217 ConfigBuilder::new()
218 }
219
220 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 pub fn clear_headers(&mut self) {
231 self.headers.clear();
232 }
233
234 pub fn is_complete(&self) -> bool {
236 !self.app_id.is_empty() && !self.app_secret.is_empty()
237 }
238
239 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 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 for (key, value) in &other.headers {
282 self.headers.insert(key.clone(), value.clone());
283 }
284 }
285
286 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 #[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 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 #[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#[derive(Debug, Clone)]
344pub struct ConfigBuilder {
345 config: Config,
346}
347
348impl ConfigBuilder {
349 pub fn new() -> Self {
351 Self {
352 config: Config::default(),
353 }
354 }
355
356 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 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 pub fn app_type(mut self, app_type: AppType) -> Self {
370 self.config.app_type = app_type;
371 self
372 }
373
374 pub fn enable_token_cache(mut self, enable: bool) -> Self {
376 self.config.enable_token_cache = enable;
377 self
378 }
379
380 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 pub fn timeout(mut self, timeout: Duration) -> Self {
388 self.config.timeout = timeout;
389 self
390 }
391
392 pub fn retry_count(mut self, count: u32) -> Self {
394 self.config.retry_count = count;
395 self
396 }
397
398 pub fn enable_log(mut self, enable: bool) -> Self {
400 self.config.enable_log = enable;
401 self
402 }
403
404 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 pub fn from_env(mut self) -> Self {
416 self.config.load_from_env();
417 self
418 }
419
420 pub fn build(self) -> Result<Config> {
425 self.config.validate()?;
426 Ok(self.config)
427 }
428
429 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#[derive(Debug, Clone)]
445pub struct ConfigSummary {
446 pub app_id: String,
448 pub app_secret_set: bool,
450 pub app_type: AppType,
452 pub enable_token_cache: bool,
454 pub base_url: String,
456 pub timeout: Duration,
458 pub retry_count: u32,
460 pub enable_log: bool,
462 pub header_count: usize,
464}
465
466impl ConfigSummary {
467 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
497impl 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 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 let invalid_config = Config {
593 app_id: String::new(),
594 ..config.clone()
595 };
596 assert!(invalid_config.validate().is_err());
597
598 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 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 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 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}