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 error::CoreError,
7 performance::OptimizedHttpConfig,
8};
9
10fn parse_env_bool(value: &str) -> Option<bool> {
18 let s = value.trim().to_lowercase();
19 if s.is_empty() {
20 return None;
21 }
22 Some(!(s.starts_with('f') || s == "0"))
23}
24
25pub fn is_known_base_url(url: &str) -> bool {
32 let allowed_suffixes = ["feishu.cn", "larksuite.com", "larkoffice.com"];
33 if let Ok(parsed) = url::Url::parse(url)
34 && let Some(host) = parsed.host_str()
35 {
36 return allowed_suffixes
37 .iter()
38 .any(|suffix| host == *suffix || host.ends_with(&format!(".{suffix}")));
39 }
40 false
41}
42
43#[derive(Debug, Clone)]
73pub struct Config {
74 inner: Arc<ConfigInner>,
79}
80
81#[derive(Clone)]
83pub struct ConfigInner {
84 pub(crate) app_id: String,
85 pub(crate) app_secret: String,
86 pub(crate) base_url: String,
88 pub(crate) enable_token_cache: bool,
90 pub(crate) app_type: AppType,
92 pub(crate) http_client: reqwest::Client,
93 pub(crate) req_timeout: Option<Duration>,
95 pub(crate) header: HashMap<String, String>,
96 pub(crate) token_provider: Arc<dyn TokenProvider>,
98 pub(crate) max_response_size: u64,
100 pub(crate) retry_count: u32,
102 pub(crate) enable_log: bool,
104 pub(crate) allow_custom_base_url: bool,
106}
107
108impl Default for ConfigInner {
109 fn default() -> Self {
110 Self {
111 app_id: "".to_string(),
112 app_secret: "".to_string(),
113 base_url: FEISHU_BASE_URL.to_string(),
114 enable_token_cache: true,
115 app_type: AppType::SelfBuild,
116 http_client: reqwest::Client::new(),
117 req_timeout: None,
118 header: Default::default(),
119 token_provider: Arc::new(NoOpTokenProvider),
120 max_response_size: 100 * 1024 * 1024, retry_count: 3,
122 enable_log: true,
123 allow_custom_base_url: false,
124 }
125 }
126}
127
128impl std::fmt::Debug for ConfigInner {
129 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
130 f.debug_struct("ConfigInner")
131 .field("app_id", &self.app_id)
132 .field("app_secret", &"***")
133 .field("base_url", &self.base_url)
134 .field("enable_token_cache", &self.enable_token_cache)
135 .field("app_type", &self.app_type)
136 .field("req_timeout", &self.req_timeout)
137 .field("max_response_size", &self.max_response_size)
138 .field("retry_count", &self.retry_count)
139 .field("enable_log", &self.enable_log)
140 .field("allow_custom_base_url", &self.allow_custom_base_url)
141 .field("header", &format!("{} headers", self.header.len()))
142 .finish()
143 }
144}
145
146impl Default for Config {
147 fn default() -> Self {
148 Self {
149 inner: Arc::new(ConfigInner::default()),
150 }
151 }
152}
153
154impl Deref for Config {
155 type Target = ConfigInner;
156
157 fn deref(&self) -> &Self::Target {
158 &self.inner
159 }
160}
161
162impl Config {
163 pub fn builder() -> ConfigBuilder {
165 ConfigBuilder::default()
166 }
167
168 pub fn new(inner: ConfigInner) -> Self {
170 Self {
171 inner: Arc::new(inner),
172 }
173 }
174
175 pub fn with_token_provider(&self, provider: impl TokenProvider + 'static) -> Self {
182 let mut inner = (*self.inner).clone();
183 inner.token_provider = Arc::new(provider);
184 Config::new(inner)
185 }
186
187 pub fn reference_count(&self) -> usize {
189 Arc::strong_count(&self.inner)
190 }
191
192 pub fn app_id(&self) -> &str {
194 &self.inner.app_id
195 }
196
197 pub fn app_secret(&self) -> &str {
199 &self.inner.app_secret
200 }
201
202 pub fn base_url(&self) -> &str {
204 &self.inner.base_url
205 }
206
207 pub fn req_timeout(&self) -> Option<Duration> {
209 self.inner.req_timeout
210 }
211
212 pub fn enable_token_cache(&self) -> bool {
214 self.inner.enable_token_cache
215 }
216
217 pub fn app_type(&self) -> AppType {
219 self.inner.app_type
220 }
221
222 pub fn http_client(&self) -> &reqwest::Client {
224 &self.inner.http_client
225 }
226
227 pub fn header(&self) -> &HashMap<String, String> {
229 &self.inner.header
230 }
231
232 pub fn token_provider(&self) -> &Arc<dyn TokenProvider> {
234 &self.inner.token_provider
235 }
236
237 pub fn max_response_size(&self) -> u64 {
239 self.inner.max_response_size
240 }
241
242 pub fn retry_count(&self) -> u32 {
244 self.inner.retry_count
245 }
246
247 pub fn enable_log(&self) -> bool {
249 self.inner.enable_log
250 }
251
252 pub fn allow_custom_base_url(&self) -> bool {
254 self.inner.allow_custom_base_url
255 }
256
257 pub fn validate(&self) -> Result<(), CoreError> {
270 if self.app_id.is_empty() {
271 return Err(CoreError::validation_builder()
272 .field("app_id")
273 .message("app_id 不能为空")
274 .build());
275 }
276 if self.app_secret.is_empty() {
277 return Err(CoreError::validation_builder()
278 .field("app_secret")
279 .message("app_secret 不能为空")
280 .build());
281 }
282 if self.base_url.is_empty() {
283 return Err(CoreError::validation_builder()
284 .field("base_url")
285 .message("base_url 不能为空")
286 .build());
287 }
288 if !self.base_url.starts_with("http://") && !self.base_url.starts_with("https://") {
289 return Err(CoreError::validation_builder()
290 .field("base_url")
291 .message("base_url 必须以 http:// 或 https:// 开头")
292 .build());
293 }
294 if !self.allow_custom_base_url && !is_known_base_url(&self.base_url) {
295 tracing::warn!(
296 "base_url '{}' 不在飞书/Lark 已知域名白名单中。\
297 如需使用自定义域名,请设置 allow_custom_base_url(true)。",
298 self.base_url
299 );
300 return Err(CoreError::validation_builder()
301 .field("base_url")
302 .message(
303 "base_url 域名不在白名单中,已知域名: *.feishu.cn, *.larksuite.com, \
304 *.larkoffice.com。如需使用自定义域名,请设置 allow_custom_base_url(true)",
305 )
306 .build());
307 }
308 if self.retry_count > 10 {
309 return Err(CoreError::validation_builder()
310 .field("retry_count")
311 .message("retry_count 不能超过 10")
312 .build());
313 }
314 Ok(())
315 }
316
317 pub fn from_env() -> Config {
330 let mut inner = ConfigInner::default();
331 Self::apply_env_vars(&mut inner);
332 let config = Config::new(inner);
333 if let Err(e) = config.validate() {
334 tracing::warn!("from_env 加载的配置未通过校验: {e}");
335 }
336 config
337 }
338
339 pub fn load_from_env(&mut self) {
343 let inner = Arc::make_mut(&mut self.inner);
344 Self::apply_env_vars(inner);
345 }
346
347 fn apply_env_vars(inner: &mut ConfigInner) {
348 for (key, value) in std::env::vars() {
349 Self::apply_env_var(inner, &key, &value);
350 }
351 }
352
353 fn apply_env_var(inner: &mut ConfigInner, key: &str, value: &str) {
354 match key {
355 "OPENLARK_APP_ID" if !value.is_empty() => inner.app_id = value.to_string(),
356 "OPENLARK_APP_SECRET" if !value.is_empty() => inner.app_secret = value.to_string(),
357 "OPENLARK_APP_TYPE" => {
358 let v = value.trim().to_lowercase();
359 match v.as_str() {
360 "self_build" | "selfbuild" | "self" => inner.app_type = AppType::SelfBuild,
361 "marketplace" | "store" => inner.app_type = AppType::Marketplace,
362 _ => {}
363 }
364 }
365 "OPENLARK_BASE_URL" if !value.is_empty() => inner.base_url = value.to_string(),
366 "OPENLARK_ENABLE_TOKEN_CACHE" => {
367 if let Some(v) = parse_env_bool(value) {
368 inner.enable_token_cache = v;
369 }
370 }
371 "OPENLARK_TIMEOUT" => {
373 if let Ok(secs) = value.parse::<u64>() {
374 inner.req_timeout = Some(Duration::from_secs(secs));
375 }
376 }
377 "OPENLARK_RETRY_COUNT" => {
378 if let Ok(n) = value.parse::<u32>() {
379 inner.retry_count = n;
380 }
381 }
382 "OPENLARK_MAX_RESPONSE_SIZE" => {
383 if let Ok(size) = value.parse::<u64>() {
384 inner.max_response_size = size;
385 }
386 }
387 "OPENLARK_ENABLE_LOG" => {
388 if let Some(v) = parse_env_bool(value) {
389 inner.enable_log = v;
390 }
391 }
392 _ => {}
393 }
394 }
395
396 pub fn summary(&self) -> ConfigSummary {
400 ConfigSummary {
401 app_id: self.app_id.clone(),
402 app_secret_set: !self.app_secret.is_empty(),
403 app_type: self.app_type,
404 enable_token_cache: self.enable_token_cache,
405 base_url: self.base_url.clone(),
406 allow_custom_base_url: self.allow_custom_base_url,
407 req_timeout: self.req_timeout,
408 retry_count: self.retry_count,
409 enable_log: self.enable_log,
410 header_count: self.header.len(),
411 max_response_size: self.max_response_size,
412 }
413 }
414}
415
416#[derive(Clone, Default)]
423pub struct ConfigBuilder {
424 inner: ConfigInner,
425}
426
427impl std::fmt::Debug for ConfigBuilder {
428 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
429 f.debug_struct("ConfigBuilder")
431 .field("inner", &self.inner)
432 .finish()
433 }
434}
435
436impl ConfigBuilder {
437 pub fn app_id(mut self, app_id: impl Into<String>) -> Self {
439 self.inner.app_id = app_id.into();
440 self
441 }
442
443 pub fn app_secret(mut self, app_secret: impl Into<String>) -> Self {
445 self.inner.app_secret = app_secret.into();
446 self
447 }
448
449 pub fn base_url(mut self, base_url: impl Into<String>) -> Self {
451 self.inner.base_url = base_url.into();
452 self
453 }
454
455 pub fn enable_token_cache(mut self, enable: bool) -> Self {
457 self.inner.enable_token_cache = enable;
458 self
459 }
460
461 pub fn app_type(mut self, app_type: AppType) -> Self {
463 self.inner.app_type = app_type;
464 self
465 }
466
467 pub fn http_client(mut self, client: reqwest::Client) -> Self {
469 self.inner.http_client = client;
470 self
471 }
472
473 pub fn optimized_http_client(
475 mut self,
476 config: OptimizedHttpConfig,
477 ) -> Result<Self, reqwest::Error> {
478 let client = config.build_client()?;
479 self.inner.http_client = client;
480 Ok(self)
481 }
482
483 pub fn production_http_client(self) -> Result<Self, reqwest::Error> {
485 let config = OptimizedHttpConfig::production();
486 self.optimized_http_client(config)
487 }
488
489 pub fn high_throughput_http_client(self) -> Result<Self, reqwest::Error> {
491 let config = OptimizedHttpConfig::high_throughput();
492 self.optimized_http_client(config)
493 }
494
495 pub fn low_latency_http_client(self) -> Result<Self, reqwest::Error> {
497 let config = OptimizedHttpConfig::low_latency();
498 self.optimized_http_client(config)
499 }
500
501 pub fn req_timeout(mut self, timeout: Duration) -> Self {
503 self.inner.req_timeout = Some(timeout);
504 self
505 }
506
507 pub fn header(mut self, header: HashMap<String, String>) -> Self {
511 self.inner.header = header;
512 self
513 }
514
515 pub fn add_header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
517 self.inner.header.insert(key.into(), value.into());
518 self
519 }
520
521 pub fn load_from_env(mut self) -> Self {
526 Config::apply_env_vars(&mut self.inner);
527 self
528 }
529
530 pub fn token_provider(mut self, provider: impl TokenProvider + 'static) -> Self {
532 self.inner.token_provider = Arc::new(provider);
533 self
534 }
535
536 pub fn max_response_size(mut self, size: u64) -> Self {
538 self.inner.max_response_size = size;
539 self
540 }
541
542 pub fn retry_count(mut self, count: u32) -> Self {
544 self.inner.retry_count = count;
545 self
546 }
547
548 pub fn enable_log(mut self, enable: bool) -> Self {
550 self.inner.enable_log = enable;
551 self
552 }
553
554 pub fn allow_custom_base_url(mut self, allow: bool) -> Self {
556 self.inner.allow_custom_base_url = allow;
557 self
558 }
559
560 pub fn build(self) -> Config {
562 Config::new(self.inner)
563 }
564}
565
566#[derive(Debug, Clone)]
571pub struct ConfigSummary {
572 pub app_id: String,
574 pub app_secret_set: bool,
576 pub app_type: AppType,
578 pub enable_token_cache: bool,
580 pub base_url: String,
582 pub allow_custom_base_url: bool,
584 pub req_timeout: Option<Duration>,
586 pub retry_count: u32,
588 pub enable_log: bool,
590 pub header_count: usize,
592 pub max_response_size: u64,
594}
595
596impl ConfigSummary {
597 pub fn friendly_description(&self) -> String {
599 format!(
600 "应用ID: {}, 基础URL: {}, 超时: {:?}, 重试: {}, 日志: {}, Headers: {}, 最大响应: {}",
601 self.app_id,
602 self.base_url,
603 self.req_timeout,
604 self.retry_count,
605 if self.enable_log { "启用" } else { "禁用" },
606 self.header_count,
607 self.max_response_size
608 )
609 }
610}
611
612impl std::fmt::Display for ConfigSummary {
613 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
614 write!(
616 f,
617 "Config {{ app_id: {}, app_secret_set: {}, base_url: {}, req_timeout: {:?}, retry_count: {}, enable_log: {}, header_count: {}, max_response_size: {} }}",
618 self.app_id,
619 self.app_secret_set,
620 self.base_url,
621 self.req_timeout,
622 self.retry_count,
623 self.enable_log,
624 self.header_count,
625 self.max_response_size
626 )
627 }
628}
629
630#[cfg(test)]
631mod tests;