openlark-client 0.17.0

OpenLark 高级客户端 - 统一入口点和轻量级服务注册表
Documentation
#![allow(deprecated)]

use crate::{Config, Result};
use openlark_core::{config::Config as CoreConfig, constants::AppType};
use std::{collections::HashMap, time::Duration};

#[derive(Debug, Clone)]
pub(crate) struct ClientBuildConfig {
    pub(crate) app_id: String,
    pub(crate) app_secret: String,
    app_type: AppType,
    enable_token_cache: bool,
    base_url: String,
    allow_custom_base_url: bool,
    timeout: Duration,
    retry_count: u32,
    enable_log: bool,
    headers: HashMap<String, String>,
    max_response_size: u64,
}

impl Default for ClientBuildConfig {
    fn default() -> Self {
        Self {
            app_id: String::new(),
            app_secret: String::new(),
            app_type: AppType::SelfBuild,
            enable_token_cache: true,
            base_url: openlark_core::constants::FEISHU_BASE_URL.to_string(),
            allow_custom_base_url: false,
            timeout: Duration::from_secs(30),
            retry_count: 3,
            enable_log: true,
            headers: HashMap::new(),
            max_response_size: 100 * 1024 * 1024,
        }
    }
}

impl From<Config> for ClientBuildConfig {
    fn from(config: Config) -> Self {
        Self {
            app_id: config.app_id,
            app_secret: config.app_secret,
            app_type: config.app_type,
            enable_token_cache: config.enable_token_cache,
            base_url: config.base_url,
            allow_custom_base_url: config.allow_custom_base_url,
            timeout: config.timeout,
            retry_count: config.retry_count,
            enable_log: config.enable_log,
            headers: config.headers,
            max_response_size: config.max_response_size,
        }
    }
}

impl ClientBuildConfig {
    pub(crate) fn app_id(&mut self, app_id: impl Into<String>) {
        self.app_id = app_id.into();
    }

    pub(crate) fn app_secret(&mut self, app_secret: impl Into<String>) {
        self.app_secret = app_secret.into();
    }

    pub(crate) fn app_type(&mut self, app_type: AppType) {
        self.app_type = app_type;
    }

    pub(crate) fn enable_token_cache(&mut self, enable: bool) {
        self.enable_token_cache = enable;
    }

    pub(crate) fn base_url(&mut self, base_url: impl Into<String>) {
        self.base_url = base_url.into();
    }

    pub(crate) fn allow_custom_base_url(&mut self, allow: bool) {
        self.allow_custom_base_url = allow;
    }

    pub(crate) fn timeout(&mut self, timeout: Duration) {
        self.timeout = timeout;
    }

    pub(crate) fn retry_count(&mut self, retry_count: u32) {
        self.retry_count = retry_count;
    }

    pub(crate) fn enable_log(&mut self, enable: bool) {
        self.enable_log = enable;
    }

    pub(crate) fn max_response_size(&mut self, size: u64) {
        self.max_response_size = size;
    }

    pub(crate) fn add_header(&mut self, key: impl Into<String>, value: impl Into<String>) {
        self.headers.insert(key.into(), value.into());
    }

    pub(crate) fn load_from_env(&mut self) {
        for (key, value) in std::env::vars() {
            self.apply_env_var(&key, &value);
        }
    }

    pub(crate) 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不能为空",
            ));
        }

        validate_base_url(&self.base_url)?;

        if !self.allow_custom_base_url && !crate::config::is_known_base_url(&self.base_url) {
            tracing::warn!(
                "base_url '{}' is not a known Feishu/Lark domain. If this is intentional, set allow_custom_base_url(true).",
                self.base_url
            );
            return Err(crate::error::validation_error(
                "base_url",
                "base_url 域名不在白名单中,已知域名: *.feishu.cn, *.larksuite.com, *.larkoffice.com。如需使用自定义域名,请设置 allow_custom_base_url(true)",
            ));
        }

        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(crate) 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)
            .max_response_size(self.max_response_size)
            .retry_count(self.retry_count)
            .enable_log(self.enable_log)
            .header(self.headers.clone())
            .build()
    }

    fn apply_env_var(&mut self, key: &str, value: &str) {
        match key {
            "OPENLARK_APP_ID" if !value.is_empty() => self.app_id(value),
            "OPENLARK_APP_SECRET" if !value.is_empty() => self.app_secret(value),
            "OPENLARK_APP_TYPE" => self.apply_app_type(value),
            "OPENLARK_BASE_URL" if !value.is_empty() => self.base_url(value),
            "OPENLARK_ENABLE_TOKEN_CACHE" => {
                let normalized = value.trim().to_lowercase();
                if !normalized.is_empty() {
                    self.enable_token_cache(!(normalized.starts_with('f') || normalized == "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_MAX_RESPONSE_SIZE" => {
                if let Ok(size) = value.parse::<u64>() {
                    self.max_response_size(size);
                }
            }
            "OPENLARK_ENABLE_LOG" => self.enable_log(!value.to_lowercase().starts_with('f')),
            _ => {}
        }
    }

    fn apply_app_type(&mut self, value: &str) {
        match value.trim().to_lowercase().as_str() {
            "self_build" | "selfbuild" | "self" => self.app_type(AppType::SelfBuild),
            "marketplace" | "store" => self.app_type(AppType::Marketplace),
            _ => {}
        }
    }
}

pub(crate) fn validate_core_config(config: &CoreConfig) -> Result<()> {
    if config.app_id().is_empty() {
        return Err(crate::error::validation_error("app_id", "app_id不能为空"));
    }

    if config.app_secret().is_empty() {
        return Err(crate::error::validation_error(
            "app_secret",
            "app_secret不能为空",
        ));
    }

    validate_base_url(config.base_url())?;

    if config
        .req_timeout()
        .is_some_and(|timeout| timeout.is_zero())
    {
        return Err(crate::error::validation_error(
            "timeout",
            "timeout必须大于0",
        ));
    }

    if config.retry_count() > 10 {
        return Err(crate::error::validation_error(
            "retry_count",
            "retry_count不能超过10",
        ));
    }

    Ok(())
}

fn validate_base_url(base_url: &str) -> Result<()> {
    if base_url.is_empty() {
        return Err(crate::error::validation_error(
            "base_url",
            "base_url不能为空",
        ));
    }

    if !base_url.starts_with("http://") && !base_url.starts_with("https://") {
        return Err(crate::error::validation_error(
            "base_url",
            "base_url必须以http://或https://开头",
        ));
    }

    Ok(())
}