use crate::{
error::{with_context, with_operation_context},
traits::LarkClient,
Config, DefaultServiceRegistry, Result,
};
use openlark_core::error::ErrorTrait;
use std::sync::Arc;
#[cfg(feature = "auth")]
#[derive(Debug, Clone)]
pub struct AuthClient {
pub app: openlark_auth::AuthService,
pub user: openlark_auth::AuthenService,
pub oauth: openlark_auth::OAuthService,
}
#[cfg(feature = "auth")]
impl AuthClient {
fn new(config: openlark_core::config::Config) -> Self {
Self {
app: openlark_auth::AuthService::new(config.clone()),
user: openlark_auth::AuthenService::new(config.clone()),
oauth: openlark_auth::OAuthService::new(config),
}
}
}
#[derive(Debug, Clone)]
pub struct Client {
config: Arc<Config>,
registry: Arc<DefaultServiceRegistry>,
core_config: openlark_core::config::Config,
#[cfg(feature = "cardkit")]
pub cardkit: openlark_cardkit::CardkitClient,
#[cfg(feature = "auth")]
pub auth: AuthClient,
#[cfg(feature = "docs")]
pub docs: openlark_docs::DocsClient,
#[cfg(feature = "communication")]
pub communication: openlark_communication::CommunicationClient,
#[cfg(feature = "hr")]
pub hr: openlark_hr::HrClient,
#[cfg(feature = "meeting")]
pub meeting: openlark_meeting::MeetingClient,
}
impl Client {
pub fn from_env() -> Result<Self> {
Self::builder().from_env().build()
}
pub fn builder() -> ClientBuilder {
ClientBuilder::new()
}
pub fn config(&self) -> &Config {
&self.config
}
pub fn registry(&self) -> &DefaultServiceRegistry {
&self.registry
}
pub fn core_config(&self) -> &openlark_core::config::Config {
&self.core_config
}
pub fn api_config(&self) -> &openlark_core::config::Config {
&self.core_config
}
pub fn is_configured(&self) -> bool {
!self.config.app_id.is_empty() && !self.config.app_secret.is_empty()
}
pub fn with_config(config: Config) -> Result<Self> {
let validation_result = config.validate();
if let Err(err) = validation_result {
return with_context(Err(err), "operation", "Client::with_config");
}
let config = Arc::new(config);
let mut registry = DefaultServiceRegistry::new();
if let Err(err) = crate::registry::bootstrap::register_compiled_services(&mut registry) {
return with_operation_context(Err(err), "Client::with_config", "service_loading");
}
let registry = Arc::new(registry);
#[cfg(feature = "auth")]
let base_core_config = config.as_ref().build_core_config();
#[cfg(feature = "auth")]
let core_config = config
.as_ref()
.get_or_build_core_config_with_token_provider();
#[cfg(not(feature = "auth"))]
let core_config = config.as_ref().get_or_build_core_config();
#[cfg(feature = "cardkit")]
let cardkit = openlark_cardkit::CardkitClient::new(core_config.clone());
#[cfg(feature = "auth")]
let auth = AuthClient::new(base_core_config.clone());
#[cfg(feature = "docs")]
let docs = openlark_docs::DocsClient::new(core_config.clone());
#[cfg(feature = "communication")]
let communication = openlark_communication::CommunicationClient::new(core_config.clone());
#[cfg(feature = "hr")]
let hr = openlark_hr::HrClient::new(core_config.clone());
#[cfg(feature = "meeting")]
let meeting = openlark_meeting::MeetingClient::new(core_config.clone());
Ok(Client {
config,
registry,
core_config: core_config.clone(),
#[cfg(feature = "cardkit")]
cardkit,
#[cfg(feature = "auth")]
auth,
#[cfg(feature = "docs")]
docs,
#[cfg(feature = "communication")]
communication,
#[cfg(feature = "hr")]
hr,
#[cfg(feature = "meeting")]
meeting,
})
}
pub async fn execute_with_context<F, T>(&self, operation: &str, f: F) -> Result<T>
where
F: std::future::Future<Output = Result<T>>,
{
let result = f.await;
with_operation_context(result, operation, "Client")
}
}
impl LarkClient for Client {
fn config(&self) -> &Config {
&self.config
}
fn is_configured(&self) -> bool {
self.is_configured()
}
}
#[derive(Debug, Clone)]
pub struct ClientBuilder {
config: Config,
}
impl ClientBuilder {
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: openlark_core::constants::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: std::time::Duration) -> Self {
self.config.timeout = timeout;
self
}
pub fn retry_count(mut self, retry_count: u32) -> Self {
self.config.retry_count = retry_count;
self
}
pub fn enable_log(mut self, enable: bool) -> Self {
self.config.enable_log = enable;
self
}
pub fn from_env(mut self) -> Self {
self.config.load_from_env();
self
}
pub fn build(self) -> Result<Client> {
let result = Client::with_config(self.config);
if let Err(ref error) = result {
tracing::error!(
"客户端构建失败: {}",
error.user_message().unwrap_or("未知错误")
);
}
result
}
}
impl Default for ClientBuilder {
fn default() -> Self {
Self::new()
}
}
impl From<Config> for Result<Client> {
fn from(config: Config) -> Self {
Client::with_config(config)
}
}
pub trait ClientErrorHandling {
fn handle_error<T>(&self, result: Result<T>, operation: &str) -> Result<T>;
async fn handle_async_error<T, F>(&self, f: F, operation: &str) -> Result<T>
where
F: std::future::Future<Output = Result<T>>;
}
impl ClientErrorHandling for Client {
fn handle_error<T>(&self, result: Result<T>, operation: &str) -> Result<T> {
with_operation_context(result, operation, "Client")
}
async fn handle_async_error<T, F>(&self, f: F, operation: &str) -> Result<T>
where
F: std::future::Future<Output = Result<T>>,
{
let result = f.await;
with_operation_context(result, operation, "Client")
}
}
#[cfg(test)]
#[allow(unused_imports)]
mod tests {
use super::*;
use openlark_core::error::ErrorTrait;
use std::time::Duration;
#[test]
fn test_client_builder() {
let client = Client::builder()
.app_id("test_app_id")
.app_secret("test_app_secret")
.timeout(Duration::from_secs(30))
.build();
assert!(client.is_ok());
}
#[test]
fn test_client_config() {
let config = Config {
app_id: "test_app_id".to_string(),
app_secret: "test_app_secret".to_string(),
base_url: "https://open.feishu.cn".to_string(),
..Default::default()
};
let client = Client::with_config(config).unwrap();
assert_eq!(client.config().app_id, "test_app_id");
assert_eq!(client.config().app_secret, "test_app_secret");
assert!(client.is_configured());
}
#[test]
fn test_client_not_configured() {
let config = Config {
app_id: String::new(),
app_secret: String::new(),
..Default::default()
};
let client_result = Client::with_config(config);
assert!(client_result.is_err());
if let Err(error) = client_result {
assert!(error.is_config_error() || error.is_validation_error());
assert!(!error.user_message().unwrap_or("未知错误").is_empty());
}
}
#[test]
fn test_client_clone() {
let client = Client::builder()
.app_id("test_app_id")
.app_secret("test_app_secret")
.build()
.unwrap();
let cloned_client = client.clone();
assert_eq!(client.config().app_id, cloned_client.config().app_id);
}
#[cfg(feature = "cardkit")]
#[test]
fn test_cardkit_chain_exists() {
let client = Client::builder()
.app_id("test_app_id")
.app_secret("test_app_secret")
.build()
.unwrap();
let _ = &client.cardkit.v1.card;
}
#[cfg(feature = "docs")]
#[test]
fn test_docs_chain_exists() {
let client = Client::builder()
.app_id("test_app_id")
.app_secret("test_app_secret")
.build()
.unwrap();
let _ = client.docs.config();
}
#[cfg(feature = "communication")]
#[test]
fn test_communication_chain_exists() {
let client = Client::builder()
.app_id("test_app_id")
.app_secret("test_app_secret")
.build()
.unwrap();
let _ = client.communication.config();
}
#[cfg(feature = "meeting")]
#[test]
fn test_meeting_chain_exists() {
let client = Client::builder()
.app_id("test_app_id")
.app_secret("test_app_secret")
.build()
.unwrap();
let _ = client.meeting.config();
}
#[test]
fn test_client_error_handling() {
let client = Client::builder()
.app_id("test_app_id")
.app_secret("test_app_secret")
.build()
.unwrap();
let error_result: Result<i32> =
Err(crate::error::validation_error("field", "validation failed"));
let result = client.handle_error(error_result, "test_operation");
assert!(result.is_err());
if let Err(error) = result {
assert!(error.context().has_context("operation"));
assert_eq!(
error.context().get_context("operation"),
Some("test_operation")
);
assert_eq!(error.context().get_context("component"), Some("Client"));
}
}
#[tokio::test]
async fn test_async_error_handling() {
let client = Client::builder()
.app_id("test_app_id")
.app_secret("test_app_secret")
.build()
.unwrap();
let result = client
.handle_async_error(
async { Err::<i32, _>(crate::error::network_error("async error")) },
"async_test",
)
.await;
assert!(result.is_err());
if let Err(error) = result {
assert!(error.context().has_context("operation"));
assert_eq!(error.context().get_context("operation"), Some("async_test"));
assert_eq!(error.context().get_context("component"), Some("Client"));
}
}
#[test]
fn test_from_env_missing_vars() {
let builder = ClientBuilder::default();
let result = builder.build();
assert!(result.is_err()); }
#[test]
fn test_from_app_id_string() {
crate::test_utils::with_env_vars(
&[
("OPENLARK_APP_ID", Some("test_app_id")),
("OPENLARK_APP_SECRET", Some("test_secret")),
],
|| {
let result: Result<Client> = Client::from_env();
assert!(result.is_ok());
if let Ok(client) = result {
assert_eq!(client.config().app_id, "test_app_id");
assert_eq!(client.config().app_secret, "test_secret");
}
},
);
}
#[test]
fn test_builder_default() {
let builder = ClientBuilder::default();
assert!(builder.config.app_id.is_empty());
assert!(builder.config.app_secret.is_empty());
}
#[cfg(feature = "communication")]
#[test]
fn test_communication_service_access() {
let client = Client::builder()
.app_id("test_app_id")
.app_secret("test_app_secret")
.build()
.unwrap();
let _comm = &client.communication;
}
}