use crate::Result;
use std::time::Duration;
use openlark_core::config::Config as CoreConfig;
use openlark_core::constants::AppType;
#[derive(Debug, Clone)]
pub struct Config {
pub app_id: String,
pub app_secret: String,
pub app_type: AppType,
pub enable_token_cache: bool,
pub base_url: String,
pub timeout: Duration,
pub retry_count: u32,
pub enable_log: bool,
pub headers: std::collections::HashMap<String, String>,
#[doc(hidden)]
pub(crate) core_config: Option<CoreConfig>,
}
impl Default for Config {
fn default() -> Self {
Self {
app_id: String::new(),
app_secret: String::new(),
app_type: AppType::SelfBuild,
enable_token_cache: true,
base_url: "https://open.feishu.cn".to_string(),
timeout: Duration::from_secs(30),
retry_count: 3,
enable_log: true,
headers: std::collections::HashMap::new(),
core_config: None,
}
}
}
impl Config {
pub fn new() -> Self {
Self::default()
}
pub fn from_env() -> Self {
let mut config = Self::default();
config.load_from_env();
config
}
pub fn load_from_env(&mut self) {
for (key, value) in std::env::vars() {
self.apply_env_var(&key, &value);
}
}
fn apply_env_var(&mut self, key: &str, value: &str) {
match key {
"OPENLARK_APP_ID" => {
if !value.is_empty() {
self.app_id = value.to_string();
}
}
"OPENLARK_APP_SECRET" => {
if !value.is_empty() {
self.app_secret = value.to_string();
}
}
"OPENLARK_APP_TYPE" => {
let v = value.trim().to_lowercase();
match v.as_str() {
"self_build" | "selfbuild" | "self" => self.app_type = AppType::SelfBuild,
"marketplace" | "store" => self.app_type = AppType::Marketplace,
_ => {}
}
}
"OPENLARK_BASE_URL" => {
if !value.is_empty() {
self.base_url = value.to_string();
}
}
"OPENLARK_ENABLE_TOKEN_CACHE" => {
let s = value.trim().to_lowercase();
if !s.is_empty() {
self.enable_token_cache = !(s.starts_with('f') || s == "0");
}
}
"OPENLARK_TIMEOUT" => {
if let Ok(timeout_secs) = value.parse::<u64>() {
self.timeout = Duration::from_secs(timeout_secs);
}
}
"OPENLARK_RETRY_COUNT" => {
if let Ok(retry_count) = value.parse::<u32>() {
self.retry_count = retry_count;
}
}
"OPENLARK_ENABLE_LOG" => {
self.enable_log = !value.to_lowercase().starts_with('f');
}
_ => {}
}
}
pub fn validate(&self) -> Result<()> {
if self.app_id.is_empty() {
return Err(crate::error::validation_error("app_id", "app_id不能为空"));
}
if self.app_secret.is_empty() {
return Err(crate::error::validation_error(
"app_secret",
"app_secret不能为空",
));
}
if self.base_url.is_empty() {
return Err(crate::error::validation_error(
"base_url",
"base_url不能为空",
));
}
if !self.base_url.starts_with("http://") && !self.base_url.starts_with("https://") {
return Err(crate::error::validation_error(
"base_url",
"base_url必须以http://或https://开头",
));
}
if self.timeout.is_zero() {
return Err(crate::error::validation_error(
"timeout",
"timeout必须大于0",
));
}
if self.retry_count > 10 {
return Err(crate::error::validation_error(
"retry_count",
"retry_count不能超过10",
));
}
Ok(())
}
pub fn builder() -> ConfigBuilder {
ConfigBuilder::new()
}
pub fn add_header<K, V>(&mut self, key: K, value: V)
where
K: Into<String>,
V: Into<String>,
{
self.headers.insert(key.into(), value.into());
}
pub fn clear_headers(&mut self) {
self.headers.clear();
}
pub fn is_complete(&self) -> bool {
!self.app_id.is_empty() && !self.app_secret.is_empty()
}
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(),
timeout: self.timeout,
retry_count: self.retry_count,
enable_log: self.enable_log,
header_count: self.headers.len(),
}
}
pub fn update_with(&mut self, other: &Config) {
if !other.app_id.is_empty() {
self.app_id = other.app_id.clone();
}
if !other.app_secret.is_empty() {
self.app_secret = other.app_secret.clone();
}
if other.app_type != AppType::SelfBuild {
self.app_type = other.app_type;
}
if other.enable_token_cache != self.enable_token_cache {
self.enable_token_cache = other.enable_token_cache;
}
if !other.base_url.is_empty() {
self.base_url = other.base_url.clone();
}
if !other.timeout.is_zero() {
self.timeout = other.timeout;
}
if other.retry_count != 3 {
self.retry_count = other.retry_count;
}
if other.enable_log != self.enable_log {
self.enable_log = other.enable_log;
}
for (key, value) in &other.headers {
self.headers.insert(key.clone(), value.clone());
}
}
pub fn build_core_config(&self) -> CoreConfig {
CoreConfig::builder()
.app_id(self.app_id.clone())
.app_secret(self.app_secret.clone())
.base_url(self.base_url.clone())
.app_type(self.app_type)
.enable_token_cache(self.enable_token_cache)
.req_timeout(self.timeout)
.header(self.headers.clone())
.build()
}
#[cfg(feature = "auth")]
pub fn build_core_config_with_token_provider(&self) -> CoreConfig {
use openlark_auth::AuthTokenProvider;
let base_config = self.build_core_config();
let provider = AuthTokenProvider::new(base_config.clone());
base_config.with_token_provider(provider)
}
pub fn get_or_build_core_config(&self) -> CoreConfig {
if let Some(ref core_config) = self.core_config {
return core_config.clone();
}
self.build_core_config()
}
#[cfg(feature = "auth")]
pub fn get_or_build_core_config_with_token_provider(&self) -> CoreConfig {
if let Some(ref core_config) = self.core_config {
return core_config.clone();
}
self.build_core_config_with_token_provider()
}
}
#[derive(Debug, Clone)]
pub struct ConfigBuilder {
config: Config,
}
impl ConfigBuilder {
pub fn new() -> Self {
Self {
config: Config::default(),
}
}
pub fn app_id<S: Into<String>>(mut self, app_id: S) -> Self {
self.config.app_id = app_id.into();
self
}
pub fn app_secret<S: Into<String>>(mut self, app_secret: S) -> Self {
self.config.app_secret = app_secret.into();
self
}
pub fn app_type(mut self, app_type: AppType) -> Self {
self.config.app_type = app_type;
self
}
pub fn enable_token_cache(mut self, enable: bool) -> Self {
self.config.enable_token_cache = enable;
self
}
pub fn base_url<S: Into<String>>(mut self, base_url: S) -> Self {
self.config.base_url = base_url.into();
self
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.config.timeout = timeout;
self
}
pub fn retry_count(mut self, count: u32) -> Self {
self.config.retry_count = count;
self
}
pub fn enable_log(mut self, enable: bool) -> Self {
self.config.enable_log = enable;
self
}
pub fn add_header<K, V>(mut self, key: K, value: V) -> Self
where
K: Into<String>,
V: Into<String>,
{
self.config.add_header(key, value);
self
}
pub fn from_env(mut self) -> Self {
self.config.load_from_env();
self
}
pub fn build(self) -> Result<Config> {
self.config.validate()?;
Ok(self.config)
}
pub fn build_unvalidated(self) -> Config {
self.config
}
}
impl Default for ConfigBuilder {
fn default() -> Self {
Self::new()
}
}
#[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 timeout: Duration,
pub retry_count: u32,
pub enable_log: bool,
pub header_count: usize,
}
impl ConfigSummary {
pub fn friendly_description(&self) -> String {
format!(
"应用ID: {}, 基础URL: {}, 超时: {:?}, 重试: {}, 日志: {}, Headers: {}",
self.app_id,
self.base_url,
self.timeout,
self.retry_count,
if self.enable_log { "启用" } else { "禁用" },
self.header_count
)
}
}
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: {}, timeout: {:?}, retry_count: {}, enable_log: {}, header_count: {} }}",
self.app_id,
self.app_secret_set,
self.base_url,
self.timeout,
self.retry_count,
self.enable_log,
self.header_count
)
}
}
impl From<std::env::Vars> for Config {
fn from(env_vars: std::env::Vars) -> Self {
let mut config = Config::default();
for (key, value) in env_vars {
config.apply_env_var(&key, &value);
}
config
}
}
#[cfg(test)]
#[allow(unused_imports)]
mod tests {
use super::*;
use std::time::Duration;
#[test]
fn test_config_default() {
let config = Config::default();
assert_eq!(config.app_id, "");
assert_eq!(config.app_secret, "");
assert_eq!(config.base_url, "https://open.feishu.cn");
assert_eq!(config.timeout, Duration::from_secs(30));
assert_eq!(config.retry_count, 3);
assert!(config.enable_log);
assert!(config.headers.is_empty());
}
#[test]
fn test_config_builder() {
let config = Config::builder()
.app_id("test_app_id")
.app_secret("test_app_secret")
.base_url("https://test.feishu.cn")
.timeout(Duration::from_secs(60))
.retry_count(5)
.enable_log(false)
.build();
assert!(config.is_ok());
let config = config.unwrap();
assert_eq!(config.app_id, "test_app_id");
assert_eq!(config.app_secret, "test_app_secret");
assert_eq!(config.base_url, "https://test.feishu.cn");
assert_eq!(config.timeout, Duration::from_secs(60));
assert_eq!(config.retry_count, 5);
assert!(!config.enable_log);
}
#[test]
fn test_config_from_env() {
crate::test_utils::with_env_vars(
&[
("OPENLARK_APP_ID", Some("test_app_id")),
("OPENLARK_APP_SECRET", Some("test_app_secret")),
("OPENLARK_APP_TYPE", Some("marketplace")),
("OPENLARK_BASE_URL", Some("https://test.feishu.cn")),
("OPENLARK_ENABLE_TOKEN_CACHE", Some("false")),
("OPENLARK_TIMEOUT", Some("60")),
("OPENLARK_RETRY_COUNT", Some("5")),
("OPENLARK_ENABLE_LOG", Some("false")),
],
|| {
let config = Config::from_env();
assert_eq!(config.app_id, "test_app_id");
assert_eq!(config.app_secret, "test_app_secret");
assert_eq!(config.app_type, AppType::Marketplace);
assert_eq!(config.base_url, "https://test.feishu.cn");
assert!(!config.enable_token_cache);
assert_eq!(config.timeout, Duration::from_secs(60));
assert_eq!(config.retry_count, 5);
assert!(!config.enable_log);
},
);
}
#[test]
fn test_config_validation() {
let config = Config {
app_id: "test_app_id".to_string(),
app_secret: "test_app_secret".to_string(),
app_type: AppType::SelfBuild,
enable_token_cache: true,
base_url: "https://open.feishu.cn".to_string(),
timeout: Duration::from_secs(30),
retry_count: 3,
enable_log: true,
headers: std::collections::HashMap::new(),
core_config: None,
};
assert!(config.validate().is_ok());
let invalid_config = Config {
app_id: String::new(),
..config.clone()
};
assert!(invalid_config.validate().is_err());
let invalid_config = Config {
app_secret: String::new(),
..config
};
assert!(invalid_config.validate().is_err());
}
#[test]
fn test_config_headers() {
let mut config = Config::default();
config.add_header("X-Custom-Header", "custom-value");
assert_eq!(config.headers.len(), 1);
assert_eq!(
config.headers.get("X-Custom-Header"),
Some(&"custom-value".to_string())
);
config.clear_headers();
assert!(config.headers.is_empty());
}
#[test]
fn test_config_update_with() {
let mut base_config = Config::default();
let update_config = Config {
app_id: "updated_app_id".to_string(),
app_secret: "updated_app_secret".to_string(),
timeout: Duration::from_secs(60),
..Default::default()
};
base_config.update_with(&update_config);
assert_eq!(base_config.app_id, "updated_app_id");
assert_eq!(base_config.app_secret, "updated_app_secret");
assert_eq!(base_config.timeout, Duration::from_secs(60));
assert_eq!(base_config.base_url, "https://open.feishu.cn");
}
#[test]
fn test_config_summary() {
let config = Config {
app_id: "test_app_id".to_string(),
app_secret: "test_app_secret".to_string(),
app_type: AppType::SelfBuild,
enable_token_cache: true,
base_url: "https://open.feishu.cn".to_string(),
timeout: Duration::from_secs(30),
retry_count: 3,
enable_log: true,
headers: std::collections::HashMap::new(),
core_config: None,
};
let summary = config.summary();
assert_eq!(summary.app_id, "test_app_id");
assert!(summary.app_secret_set);
assert_eq!(summary.base_url, "https://open.feishu.cn");
assert_eq!(summary.timeout, Duration::from_secs(30));
assert_eq!(summary.retry_count, 3);
assert!(summary.enable_log);
assert_eq!(summary.header_count, 0);
}
#[test]
fn test_config_is_complete() {
let mut config = Config::default();
assert!(!config.is_complete());
config.app_id = "test_app_id".to_string();
assert!(!config.is_complete());
config.app_secret = "test_app_secret".to_string();
assert!(config.is_complete());
}
}