use std::{collections::HashMap, ops::Deref, sync::Arc, time::Duration};
use crate::{
auth::token_provider::{NoOpTokenProvider, TokenProvider},
constants::{AppType, FEISHU_BASE_URL},
error::CoreError,
performance::OptimizedHttpConfig,
};
fn parse_env_bool(value: &str) -> Option<bool> {
let s = value.trim().to_lowercase();
if s.is_empty() {
return None;
}
Some(!(s.starts_with('f') || s == "0"))
}
pub fn is_known_base_url(url: &str) -> bool {
let allowed_suffixes = ["feishu.cn", "larksuite.com", "larkoffice.com"];
if let Ok(parsed) = url::Url::parse(url)
&& let Some(host) = parsed.host_str()
{
return allowed_suffixes
.iter()
.any(|suffix| host == *suffix || host.ends_with(&format!(".{suffix}")));
}
false
}
#[derive(Debug, Clone)]
pub struct Config {
inner: Arc<ConfigInner>,
}
#[derive(Clone)]
pub struct ConfigInner {
pub(crate) app_id: String,
pub(crate) app_secret: String,
pub(crate) base_url: String,
pub(crate) enable_token_cache: bool,
pub(crate) app_type: AppType,
pub(crate) http_client: reqwest::Client,
pub(crate) req_timeout: Option<Duration>,
pub(crate) header: HashMap<String, String>,
pub(crate) token_provider: Arc<dyn TokenProvider>,
pub(crate) max_response_size: u64,
pub(crate) retry_count: u32,
pub(crate) enable_log: bool,
pub(crate) allow_custom_base_url: bool,
}
impl Default for ConfigInner {
fn default() -> Self {
Self {
app_id: "".to_string(),
app_secret: "".to_string(),
base_url: FEISHU_BASE_URL.to_string(),
enable_token_cache: true,
app_type: AppType::SelfBuild,
http_client: reqwest::Client::new(),
req_timeout: None,
header: Default::default(),
token_provider: Arc::new(NoOpTokenProvider),
max_response_size: 100 * 1024 * 1024, retry_count: 3,
enable_log: true,
allow_custom_base_url: false,
}
}
}
impl std::fmt::Debug for ConfigInner {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ConfigInner")
.field("app_id", &self.app_id)
.field("app_secret", &"***")
.field("base_url", &self.base_url)
.field("enable_token_cache", &self.enable_token_cache)
.field("app_type", &self.app_type)
.field("req_timeout", &self.req_timeout)
.field("max_response_size", &self.max_response_size)
.field("retry_count", &self.retry_count)
.field("enable_log", &self.enable_log)
.field("allow_custom_base_url", &self.allow_custom_base_url)
.field("header", &format!("{} headers", self.header.len()))
.finish()
}
}
impl Default for Config {
fn default() -> Self {
Self {
inner: Arc::new(ConfigInner::default()),
}
}
}
impl Deref for Config {
type Target = ConfigInner;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl Config {
pub fn builder() -> ConfigBuilder {
ConfigBuilder::default()
}
pub fn new(inner: ConfigInner) -> Self {
Self {
inner: Arc::new(inner),
}
}
pub fn with_token_provider(&self, provider: impl TokenProvider + 'static) -> Self {
let mut inner = (*self.inner).clone();
inner.token_provider = Arc::new(provider);
Config::new(inner)
}
pub fn reference_count(&self) -> usize {
Arc::strong_count(&self.inner)
}
pub fn app_id(&self) -> &str {
&self.inner.app_id
}
pub fn app_secret(&self) -> &str {
&self.inner.app_secret
}
pub fn base_url(&self) -> &str {
&self.inner.base_url
}
pub fn req_timeout(&self) -> Option<Duration> {
self.inner.req_timeout
}
pub fn enable_token_cache(&self) -> bool {
self.inner.enable_token_cache
}
pub fn app_type(&self) -> AppType {
self.inner.app_type
}
pub fn http_client(&self) -> &reqwest::Client {
&self.inner.http_client
}
pub fn header(&self) -> &HashMap<String, String> {
&self.inner.header
}
pub fn token_provider(&self) -> &Arc<dyn TokenProvider> {
&self.inner.token_provider
}
pub fn max_response_size(&self) -> u64 {
self.inner.max_response_size
}
pub fn retry_count(&self) -> u32 {
self.inner.retry_count
}
pub fn enable_log(&self) -> bool {
self.inner.enable_log
}
pub fn allow_custom_base_url(&self) -> bool {
self.inner.allow_custom_base_url
}
pub fn validate(&self) -> Result<(), CoreError> {
if self.app_id.is_empty() {
return Err(CoreError::validation_builder()
.field("app_id")
.message("app_id 不能为空")
.build());
}
if self.app_secret.is_empty() {
return Err(CoreError::validation_builder()
.field("app_secret")
.message("app_secret 不能为空")
.build());
}
if self.base_url.is_empty() {
return Err(CoreError::validation_builder()
.field("base_url")
.message("base_url 不能为空")
.build());
}
if !self.base_url.starts_with("http://") && !self.base_url.starts_with("https://") {
return Err(CoreError::validation_builder()
.field("base_url")
.message("base_url 必须以 http:// 或 https:// 开头")
.build());
}
if !self.allow_custom_base_url && !is_known_base_url(&self.base_url) {
tracing::warn!(
"base_url '{}' 不在飞书/Lark 已知域名白名单中。\
如需使用自定义域名,请设置 allow_custom_base_url(true)。",
self.base_url
);
return Err(CoreError::validation_builder()
.field("base_url")
.message(
"base_url 域名不在白名单中,已知域名: *.feishu.cn, *.larksuite.com, \
*.larkoffice.com。如需使用自定义域名,请设置 allow_custom_base_url(true)",
)
.build());
}
if self.retry_count > 10 {
return Err(CoreError::validation_builder()
.field("retry_count")
.message("retry_count 不能超过 10")
.build());
}
Ok(())
}
pub fn from_env() -> Config {
let mut inner = ConfigInner::default();
Self::apply_env_vars(&mut inner);
let config = Config::new(inner);
if let Err(e) = config.validate() {
tracing::warn!("from_env 加载的配置未通过校验: {e}");
}
config
}
pub fn load_from_env(&mut self) {
let inner = Arc::make_mut(&mut self.inner);
Self::apply_env_vars(inner);
}
fn apply_env_vars(inner: &mut ConfigInner) {
for (key, value) in std::env::vars() {
Self::apply_env_var(inner, &key, &value);
}
}
fn apply_env_var(inner: &mut ConfigInner, key: &str, value: &str) {
match key {
"OPENLARK_APP_ID" if !value.is_empty() => inner.app_id = value.to_string(),
"OPENLARK_APP_SECRET" if !value.is_empty() => inner.app_secret = value.to_string(),
"OPENLARK_APP_TYPE" => {
let v = value.trim().to_lowercase();
match v.as_str() {
"self_build" | "selfbuild" | "self" => inner.app_type = AppType::SelfBuild,
"marketplace" | "store" => inner.app_type = AppType::Marketplace,
_ => {}
}
}
"OPENLARK_BASE_URL" if !value.is_empty() => inner.base_url = value.to_string(),
"OPENLARK_ENABLE_TOKEN_CACHE" => {
if let Some(v) = parse_env_bool(value) {
inner.enable_token_cache = v;
}
}
"OPENLARK_TIMEOUT" => {
if let Ok(secs) = value.parse::<u64>() {
inner.req_timeout = Some(Duration::from_secs(secs));
}
}
"OPENLARK_RETRY_COUNT" => {
if let Ok(n) = value.parse::<u32>() {
inner.retry_count = n;
}
}
"OPENLARK_MAX_RESPONSE_SIZE" => {
if let Ok(size) = value.parse::<u64>() {
inner.max_response_size = size;
}
}
"OPENLARK_ENABLE_LOG" => {
if let Some(v) = parse_env_bool(value) {
inner.enable_log = v;
}
}
_ => {}
}
}
pub fn summary(&self) -> ConfigSummary {
ConfigSummary {
app_id: self.app_id.clone(),
app_secret_set: !self.app_secret.is_empty(),
app_type: self.app_type,
enable_token_cache: self.enable_token_cache,
base_url: self.base_url.clone(),
allow_custom_base_url: self.allow_custom_base_url,
req_timeout: self.req_timeout,
retry_count: self.retry_count,
enable_log: self.enable_log,
header_count: self.header.len(),
max_response_size: self.max_response_size,
}
}
}
#[derive(Clone, Default)]
pub struct ConfigBuilder {
inner: ConfigInner,
}
impl std::fmt::Debug for ConfigBuilder {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ConfigBuilder")
.field("inner", &self.inner)
.finish()
}
}
impl ConfigBuilder {
pub fn app_id(mut self, app_id: impl Into<String>) -> Self {
self.inner.app_id = app_id.into();
self
}
pub fn app_secret(mut self, app_secret: impl Into<String>) -> Self {
self.inner.app_secret = app_secret.into();
self
}
pub fn base_url(mut self, base_url: impl Into<String>) -> Self {
self.inner.base_url = base_url.into();
self
}
pub fn enable_token_cache(mut self, enable: bool) -> Self {
self.inner.enable_token_cache = enable;
self
}
pub fn app_type(mut self, app_type: AppType) -> Self {
self.inner.app_type = app_type;
self
}
pub fn http_client(mut self, client: reqwest::Client) -> Self {
self.inner.http_client = client;
self
}
pub fn optimized_http_client(
mut self,
config: OptimizedHttpConfig,
) -> Result<Self, reqwest::Error> {
let client = config.build_client()?;
self.inner.http_client = client;
Ok(self)
}
pub fn production_http_client(self) -> Result<Self, reqwest::Error> {
let config = OptimizedHttpConfig::production();
self.optimized_http_client(config)
}
pub fn high_throughput_http_client(self) -> Result<Self, reqwest::Error> {
let config = OptimizedHttpConfig::high_throughput();
self.optimized_http_client(config)
}
pub fn low_latency_http_client(self) -> Result<Self, reqwest::Error> {
let config = OptimizedHttpConfig::low_latency();
self.optimized_http_client(config)
}
pub fn req_timeout(mut self, timeout: Duration) -> Self {
self.inner.req_timeout = Some(timeout);
self
}
pub fn header(mut self, header: HashMap<String, String>) -> Self {
self.inner.header = header;
self
}
pub fn add_header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.inner.header.insert(key.into(), value.into());
self
}
pub fn load_from_env(mut self) -> Self {
Config::apply_env_vars(&mut self.inner);
self
}
pub fn token_provider(mut self, provider: impl TokenProvider + 'static) -> Self {
self.inner.token_provider = Arc::new(provider);
self
}
pub fn max_response_size(mut self, size: u64) -> Self {
self.inner.max_response_size = size;
self
}
pub fn retry_count(mut self, count: u32) -> Self {
self.inner.retry_count = count;
self
}
pub fn enable_log(mut self, enable: bool) -> Self {
self.inner.enable_log = enable;
self
}
pub fn allow_custom_base_url(mut self, allow: bool) -> Self {
self.inner.allow_custom_base_url = allow;
self
}
pub fn build(self) -> Config {
Config::new(self.inner)
}
}
#[derive(Debug, Clone)]
pub struct ConfigSummary {
pub app_id: String,
pub app_secret_set: bool,
pub app_type: AppType,
pub enable_token_cache: bool,
pub base_url: String,
pub allow_custom_base_url: bool,
pub req_timeout: Option<Duration>,
pub retry_count: u32,
pub enable_log: bool,
pub header_count: usize,
pub max_response_size: u64,
}
impl ConfigSummary {
pub fn friendly_description(&self) -> String {
format!(
"应用ID: {}, 基础URL: {}, 超时: {:?}, 重试: {}, 日志: {}, Headers: {}, 最大响应: {}",
self.app_id,
self.base_url,
self.req_timeout,
self.retry_count,
if self.enable_log { "启用" } else { "禁用" },
self.header_count,
self.max_response_size
)
}
}
impl std::fmt::Display for ConfigSummary {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Config {{ app_id: {}, app_secret_set: {}, base_url: {}, req_timeout: {:?}, retry_count: {}, enable_log: {}, header_count: {}, max_response_size: {} }}",
self.app_id,
self.app_secret_set,
self.base_url,
self.req_timeout,
self.retry_count,
self.enable_log,
self.header_count,
self.max_response_size
)
}
}
#[cfg(test)]
mod tests;