Skip to main content

openlark_client/
client.rs

1//! OpenLark Client - 全新简化架构
2//!
3//! 极简设计:仅保留 meta 链式字段访问(单入口,KISS)
4
5use crate::{
6    Config, DefaultServiceRegistry, Result,
7    error::{with_context, with_operation_context},
8    traits::LarkClient,
9};
10use openlark_core::error::ErrorTrait;
11use std::sync::Arc;
12
13/// 🔐 认证 meta 入口:`client.auth.app / client.auth.user / client.auth.oauth`
14#[cfg(feature = "auth")]
15#[derive(Debug, Clone)]
16pub struct AuthClient {
17    /// 应用认证服务
18    pub app: openlark_auth::AuthService,
19    /// 用户身份认证服务
20    pub user: openlark_auth::AuthenService,
21    /// OAuth 授权服务
22    pub oauth: openlark_auth::OAuthService,
23}
24
25#[cfg(feature = "auth")]
26impl AuthClient {
27    fn new(config: openlark_core::config::Config) -> Self {
28        Self {
29            app: openlark_auth::AuthService::new(config.clone()),
30            user: openlark_auth::AuthenService::new(config.clone()),
31            oauth: openlark_auth::OAuthService::new(config),
32        }
33    }
34}
35
36/// 🚀 OpenLark客户端 - 极简设计
37///
38/// # 特性
39/// - 零配置启动:`Client::from_env()`
40/// - 单入口:meta 链式字段访问(`client.docs/...`)
41/// - 编译时feature优化
42/// - 高性能异步
43/// - 现代化错误处理
44///
45/// # 示例
46/// ```rust,no_run
47/// use openlark_client::prelude::*;
48///
49/// #[tokio::main]
50/// async fn main() -> Result<()> {
51///     // 从环境变量创建客户端
52///     let client = Client::from_env()?;
53///
54///     // meta 链式入口(需要对应 feature)
55///     // - 通讯:client.communication.im...
56///     // - 文档:client.docs.ccm...
57///     // - 认证:client.auth.app / client.auth.user / client.auth.oauth
58///
59///     Ok(())
60/// }
61/// ```
62#[derive(Clone)]
63pub struct Client {
64    /// 客户端配置
65    config: Arc<Config>,
66    /// 服务注册表
67    registry: Arc<DefaultServiceRegistry>,
68    /// 底层 core 配置(供各 meta client 复用)
69    core_config: openlark_core::config::Config,
70
71    /// CardKit meta 调用链:client.cardkit.v1.card.create(...)
72    #[cfg(feature = "cardkit")]
73    pub cardkit: openlark_cardkit::CardkitClient,
74
75    /// Auth meta 调用链入口:client.auth.app / client.auth.user / client.auth.oauth
76    #[cfg(feature = "auth")]
77    pub auth: AuthClient,
78
79    /// Docs meta 调用链入口:client.docs.ccm / client.docs.base ...
80    #[cfg(feature = "docs")]
81    pub docs: openlark_docs::DocsClient,
82
83    /// Communication meta 调用链入口:client.communication.im / client.communication.contact ...
84    #[cfg(feature = "communication")]
85    pub communication: openlark_communication::CommunicationClient,
86
87    /// HR meta 调用链入口:client.hr.attendance / client.hr.corehr / client.hr.hire ...
88    #[cfg(feature = "hr")]
89    pub hr: openlark_hr::HrClient,
90
91    /// Meeting meta 调用链入口:client.meeting.vc.v1.room.create() ...
92    #[cfg(feature = "meeting")]
93    pub meeting: openlark_meeting::MeetingClient,
94
95    /// AI meta 调用链入口:client.ai.chat.create() ...
96    #[cfg(feature = "ai")]
97    pub ai: openlark_ai::AiClient,
98
99    /// Workflow meta 调用链入口:client.workflow.task.create() ...
100    #[cfg(feature = "workflow")]
101    pub workflow: crate::WorkflowClient,
102
103    /// Platform meta 调用链入口:client.platform.app_engine... ...
104    #[cfg(feature = "platform")]
105    pub platform: crate::PlatformClient,
106
107    /// Application meta 调用链入口:client.application.applet... ...
108    #[cfg(feature = "application")]
109    pub application: crate::ApplicationClient,
110
111    /// Helpdesk meta 调用链入口:client.helpdesk.ticket... ...
112    #[cfg(feature = "helpdesk")]
113    pub helpdesk: crate::HelpdeskClient,
114
115    /// Mail meta 调用链入口:client.mail.group... ...
116    #[cfg(feature = "mail")]
117    pub mail: crate::MailClient,
118
119    /// Analytics meta 调用链入口:client.analytics.report... ...
120    #[cfg(feature = "analytics")]
121    pub analytics: crate::AnalyticsClient,
122
123    /// User meta 调用链入口:client.user.setting... ...
124    #[cfg(feature = "user")]
125    pub user: crate::UserClient,
126
127    /// Security meta 调用链入口:client.security.acs... ...
128    #[cfg(feature = "security")]
129    pub security: crate::SecurityClient,
130}
131
132impl std::fmt::Debug for Client {
133    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
134        f.debug_struct("Client")
135            .field("config", &"<Config>")
136            .field("registry", &"<Registry>")
137            .field("core_config", &"<CoreConfig>")
138            .field("cardkit", &cfg!(feature = "cardkit"))
139            .field("auth", &cfg!(feature = "auth"))
140            .field("docs", &cfg!(feature = "docs"))
141            .field("communication", &cfg!(feature = "communication"))
142            .field("hr", &cfg!(feature = "hr"))
143            .field("meeting", &cfg!(feature = "meeting"))
144            .field("ai", &cfg!(feature = "ai"))
145            .field("workflow", &cfg!(feature = "workflow"))
146            .field("platform", &cfg!(feature = "platform"))
147            .field("application", &cfg!(feature = "application"))
148            .field("helpdesk", &cfg!(feature = "helpdesk"))
149            .field("mail", &cfg!(feature = "mail"))
150            .field("analytics", &cfg!(feature = "analytics"))
151            .field("user", &cfg!(feature = "user"))
152            .field("security", &cfg!(feature = "security"))
153            .finish()
154    }
155}
156
157impl Client {
158    /// 🔥 从环境变量创建客户端
159    ///
160    /// # 环境变量
161    /// ```bash
162    /// export OPENLARK_APP_ID=your_app_id
163    /// export OPENLARK_APP_SECRET=your_app_secret
164    /// export OPENLARK_BASE_URL=https://open.feishu.cn  # 可选
165    /// ```
166    ///
167    ///
168    /// # 返回值
169    /// 返回配置好的客户端实例或错误
170    ///
171    /// # 示例
172    /// ```rust,no_run
173    /// use openlark_client::Client;
174    ///
175    /// let _client = Client::from_env();
176    /// ```
177    pub fn from_env() -> Result<Self> {
178        Self::builder().from_env().build()
179    }
180
181    /// 🏗️ 创建构建器
182    pub fn builder() -> ClientBuilder {
183        ClientBuilder::new()
184    }
185
186    /// 🔧 获取客户端配置
187    pub fn config(&self) -> &Config {
188        &self.config
189    }
190
191    /// 📋 获取服务注册表
192    pub fn registry(&self) -> &DefaultServiceRegistry {
193        &self.registry
194    }
195
196    /// 🔧 获取底层 core 配置(高级用法/调试用)
197    pub fn core_config(&self) -> &openlark_core::config::Config {
198        &self.core_config
199    }
200
201    /// 🔧 获取可直接传给函数式 API 的认证后配置
202    ///
203    /// 与 [`Self::core_config`] 返回同一份配置,保留这个别名是为了让
204    /// 业务侧更容易理解它的用途:可直接传给 `openlark_docs::*`、
205    /// `openlark_auth::*` 等函数式 API。
206    pub fn api_config(&self) -> &openlark_core::config::Config {
207        &self.core_config
208    }
209
210    /// ✅ 检查客户端是否已正确配置
211    pub fn is_configured(&self) -> bool {
212        !self.config.app_id.is_empty() && !self.config.app_secret.is_empty()
213    }
214
215    /// 🆕 创建带有自定义配置的客户端
216    pub fn with_config(config: Config) -> Result<Self> {
217        let validation_result = config.validate();
218        if let Err(err) = validation_result {
219            return with_context(Err(err), "operation", "Client::with_config");
220        }
221
222        let config = Arc::new(config);
223        let mut registry = DefaultServiceRegistry::new();
224
225        // 加载启用的服务
226        if let Err(err) = crate::registry::bootstrap::register_compiled_services(&mut registry) {
227            return with_operation_context(Err(err), "Client::with_config", "service_loading");
228        }
229
230        let registry = Arc::new(registry);
231
232        // 从 client Config 获取 core Config
233        #[cfg(feature = "auth")]
234        let base_core_config = config.as_ref().build_core_config();
235        #[cfg(feature = "auth")]
236        let core_config = config
237            .as_ref()
238            .get_or_build_core_config_with_token_provider();
239        #[cfg(not(feature = "auth"))]
240        let core_config = config.as_ref().get_or_build_core_config();
241
242        #[cfg(feature = "cardkit")]
243        let cardkit = openlark_cardkit::CardkitClient::new(core_config.clone());
244
245        #[cfg(feature = "auth")]
246        let auth = AuthClient::new(base_core_config.clone());
247
248        #[cfg(feature = "docs")]
249        let docs = openlark_docs::DocsClient::new(core_config.clone());
250
251        #[cfg(feature = "communication")]
252        let communication = openlark_communication::CommunicationClient::new(core_config.clone());
253
254        #[cfg(feature = "hr")]
255        let hr = openlark_hr::HrClient::new(core_config.clone());
256
257        #[cfg(feature = "meeting")]
258        let meeting = openlark_meeting::MeetingClient::new(core_config.clone());
259
260        #[cfg(feature = "ai")]
261        let ai = openlark_ai::AiClient::new(core_config.clone());
262
263        #[cfg(feature = "workflow")]
264        let workflow = crate::WorkflowClient::new(core_config.clone());
265
266        #[cfg(feature = "platform")]
267        let platform = crate::PlatformClient::new(core_config.clone())?;
268
269        #[cfg(feature = "application")]
270        let application = crate::ApplicationClient::new(core_config.clone());
271
272        #[cfg(feature = "helpdesk")]
273        let helpdesk = crate::HelpdeskClient::new(core_config.clone());
274
275        #[cfg(feature = "mail")]
276        let mail = crate::MailClient::new(core_config.clone());
277
278        #[cfg(feature = "analytics")]
279        let analytics = crate::AnalyticsClient::new(core_config.clone())?;
280
281        #[cfg(feature = "user")]
282        let user = crate::UserClient::new(core_config.clone())?;
283
284        #[cfg(feature = "security")]
285        let security = {
286            let security_config = openlark_security::models::SecurityConfig::new(
287                config.app_id.clone(),
288                config.app_secret.clone(),
289            )
290            .with_base_url(&config.base_url);
291            std::sync::Arc::new(openlark_security::SecurityServices::new(security_config))
292        };
293
294        Ok(Client {
295            config,
296            registry,
297            core_config: core_config.clone(),
298            #[cfg(feature = "cardkit")]
299            cardkit,
300            #[cfg(feature = "auth")]
301            auth,
302            #[cfg(feature = "docs")]
303            docs,
304            #[cfg(feature = "communication")]
305            communication,
306            #[cfg(feature = "hr")]
307            hr,
308            #[cfg(feature = "meeting")]
309            meeting,
310            #[cfg(feature = "ai")]
311            ai,
312            #[cfg(feature = "workflow")]
313            workflow,
314            #[cfg(feature = "platform")]
315            platform,
316            #[cfg(feature = "application")]
317            application,
318            #[cfg(feature = "helpdesk")]
319            helpdesk,
320            #[cfg(feature = "mail")]
321            mail,
322            #[cfg(feature = "analytics")]
323            analytics,
324            #[cfg(feature = "user")]
325            user,
326            #[cfg(feature = "security")]
327            security,
328        })
329    }
330
331    /// 🔧 执行带有错误上下文的操作
332    pub async fn execute_with_context<F, T>(&self, operation: &str, f: F) -> Result<T>
333    where
334        F: std::future::Future<Output = Result<T>>,
335    {
336        let result = f.await;
337        with_operation_context(result, operation, "Client")
338    }
339}
340
341// 实现LarkClient trait
342impl LarkClient for Client {
343    fn config(&self) -> &Config {
344        &self.config
345    }
346
347    fn is_configured(&self) -> bool {
348        self.is_configured()
349    }
350}
351
352/// 🏗️ 客户端构建器 - 流畅API
353///
354/// 提供链式调用的客户端构建方式
355///
356/// # 示例
357/// ```rust,no_run
358/// use openlark_client::Client;
359/// use openlark_client::Result;
360/// use std::time::Duration;
361///
362/// fn main() -> Result<()> {
363///     let _client = Client::builder()
364///         .app_id("your_app_id")
365///         .app_secret("your_app_secret")
366///         .base_url("https://open.feishu.cn")
367///         .timeout(Duration::from_secs(30))
368///         .build()?;
369///     Ok(())
370/// }
371/// ```
372#[derive(Debug, Clone)]
373pub struct ClientBuilder {
374    config: Config,
375}
376
377impl ClientBuilder {
378    /// 🆕 创建新的构建器实例
379    pub fn new() -> Self {
380        Self {
381            config: Config::default(),
382        }
383    }
384
385    /// 🆔 设置应用ID
386    pub fn app_id<S: Into<String>>(mut self, app_id: S) -> Self {
387        self.config.app_id = app_id.into();
388        self
389    }
390
391    /// 🔑 设置应用密钥
392    pub fn app_secret<S: Into<String>>(mut self, app_secret: S) -> Self {
393        self.config.app_secret = app_secret.into();
394        self
395    }
396
397    /// 🏷️ 设置应用类型(自建 / 商店)
398    pub fn app_type(mut self, app_type: openlark_core::constants::AppType) -> Self {
399        self.config.app_type = app_type;
400        self
401    }
402
403    /// 🔐 设置是否允许自动获取 token(默认 true)
404    pub fn enable_token_cache(mut self, enable: bool) -> Self {
405        self.config.enable_token_cache = enable;
406        self
407    }
408
409    /// 🌐 设置基础URL
410    pub fn base_url<S: Into<String>>(mut self, base_url: S) -> Self {
411        self.config.base_url = base_url.into();
412        self
413    }
414
415    /// ⏱️ 设置请求超时时间
416    pub fn timeout(mut self, timeout: std::time::Duration) -> Self {
417        self.config.timeout = timeout;
418        self
419    }
420
421    /// 🔄 设置重试次数
422    pub fn retry_count(mut self, retry_count: u32) -> Self {
423        self.config.retry_count = retry_count;
424        self
425    }
426
427    /// 📝 启用或禁用日志
428    pub fn enable_log(mut self, enable: bool) -> Self {
429        self.config.enable_log = enable;
430        self
431    }
432
433    /// 🌍 从环境变量加载配置
434    pub fn from_env(mut self) -> Self {
435        self.config.load_from_env();
436        self
437    }
438
439    /// 🔨 构建客户端实例
440    ///
441    /// # 返回值
442    /// 返回配置好的客户端实例或验证错误
443    ///
444    /// # 错误
445    /// 如果配置验证失败,会返回相应的错误信息,包含用户友好的恢复建议
446    pub fn build(self) -> Result<Client> {
447        let result = Client::with_config(self.config);
448        if let Err(ref error) = result {
449            tracing::error!(
450                "客户端构建失败: {}",
451                error.user_message().unwrap_or("未知错误")
452            );
453        }
454        result
455    }
456}
457
458impl Default for ClientBuilder {
459    fn default() -> Self {
460        Self::new()
461    }
462}
463
464/// Client的便利构造函数
465impl From<Config> for Result<Client> {
466    fn from(config: Config) -> Self {
467        Client::with_config(config)
468    }
469}
470
471/// 客户端错误处理扩展特征
472pub trait ClientErrorHandling {
473    /// 处理错误并添加客户端上下文
474    fn handle_error<T>(&self, result: Result<T>, operation: &str) -> Result<T>;
475    /// 处理异步错误并添加客户端上下文
476    async fn handle_async_error<T, F>(&self, f: F, operation: &str) -> Result<T>
477    where
478        F: std::future::Future<Output = Result<T>>;
479}
480
481impl ClientErrorHandling for Client {
482    fn handle_error<T>(&self, result: Result<T>, operation: &str) -> Result<T> {
483        with_operation_context(result, operation, "Client")
484    }
485
486    async fn handle_async_error<T, F>(&self, f: F, operation: &str) -> Result<T>
487    where
488        F: std::future::Future<Output = Result<T>>,
489    {
490        let result = f.await;
491        with_operation_context(result, operation, "Client")
492    }
493}
494
495#[cfg(test)]
496#[allow(unused_imports)]
497mod tests {
498    use super::*;
499    use openlark_core::error::ErrorTrait;
500    use std::time::Duration;
501
502    #[test]
503    fn test_client_builder() {
504        let client = Client::builder()
505            .app_id("test_app_id")
506            .app_secret("test_app_secret")
507            .timeout(Duration::from_secs(30))
508            .build();
509
510        assert!(client.is_ok());
511    }
512
513    #[test]
514    fn test_client_config() {
515        let config = Config {
516            app_id: "test_app_id".to_string(),
517            app_secret: "test_app_secret".to_string(),
518            base_url: "https://open.feishu.cn".to_string(),
519            ..Default::default()
520        };
521
522        let client = Client::with_config(config).unwrap();
523        assert_eq!(client.config().app_id, "test_app_id");
524        assert_eq!(client.config().app_secret, "test_app_secret");
525        assert!(client.is_configured());
526    }
527
528    #[test]
529    fn test_client_not_configured() {
530        let config = Config {
531            app_id: String::new(),
532            app_secret: String::new(),
533            ..Default::default()
534        };
535
536        let client_result = Client::with_config(config);
537        assert!(client_result.is_err());
538
539        if let Err(error) = client_result {
540            assert!(error.is_config_error() || error.is_validation_error());
541            assert!(!error.user_message().unwrap_or("未知错误").is_empty());
542        }
543    }
544
545    #[test]
546    fn test_client_clone() {
547        let client = Client::builder()
548            .app_id("test_app_id")
549            .app_secret("test_app_secret")
550            .build()
551            .unwrap();
552
553        let cloned_client = client.clone();
554        assert_eq!(client.config().app_id, cloned_client.config().app_id);
555    }
556
557    #[cfg(feature = "cardkit")]
558    #[test]
559    fn test_cardkit_chain_exists() {
560        let client = Client::builder()
561            .app_id("test_app_id")
562            .app_secret("test_app_secret")
563            .build()
564            .unwrap();
565
566        let _ = &client.cardkit.v1.card;
567    }
568
569    #[cfg(feature = "docs")]
570    #[test]
571    fn test_docs_chain_exists() {
572        let client = Client::builder()
573            .app_id("test_app_id")
574            .app_secret("test_app_secret")
575            .build()
576            .unwrap();
577
578        let _ = client.docs.config();
579    }
580
581    #[cfg(feature = "communication")]
582    #[test]
583    fn test_communication_chain_exists() {
584        let client = Client::builder()
585            .app_id("test_app_id")
586            .app_secret("test_app_secret")
587            .build()
588            .unwrap();
589
590        let _ = client.communication.config();
591    }
592
593    #[cfg(feature = "meeting")]
594    #[test]
595    fn test_meeting_chain_exists() {
596        let client = Client::builder()
597            .app_id("test_app_id")
598            .app_secret("test_app_secret")
599            .build()
600            .unwrap();
601
602        let _ = client.meeting.config();
603    }
604
605    #[test]
606    fn test_client_error_handling() {
607        let client = Client::builder()
608            .app_id("test_app_id")
609            .app_secret("test_app_secret")
610            .build()
611            .unwrap();
612
613        // 测试错误上下文处理
614        let error_result: Result<i32> =
615            Err(crate::error::validation_error("field", "validation failed"));
616        let result = client.handle_error(error_result, "test_operation");
617
618        assert!(result.is_err());
619        if let Err(error) = result {
620            assert!(error.context().has_context("operation"));
621            assert_eq!(
622                error.context().get_context("operation"),
623                Some("test_operation")
624            );
625            assert_eq!(error.context().get_context("component"), Some("Client"));
626        }
627    }
628
629    #[tokio::test]
630    async fn test_async_error_handling() {
631        let client = Client::builder()
632            .app_id("test_app_id")
633            .app_secret("test_app_secret")
634            .build()
635            .unwrap();
636
637        // 测试异步错误上下文处理
638        let result = client
639            .handle_async_error(
640                async { Err::<i32, _>(crate::error::network_error("async error")) },
641                "async_test",
642            )
643            .await;
644
645        assert!(result.is_err());
646        if let Err(error) = result {
647            assert!(error.context().has_context("operation"));
648            assert_eq!(error.context().get_context("operation"), Some("async_test"));
649            assert_eq!(error.context().get_context("component"), Some("Client"));
650        }
651    }
652
653    #[test]
654    fn test_from_env_missing_vars() {
655        // 验证默认构建器未配置 app_id/app_secret 时会失败(不依赖环境变量)。
656        let builder = ClientBuilder::default();
657        let result = builder.build();
658        assert!(result.is_err()); // 没有app_id和app_secret应该失败
659    }
660
661    #[test]
662    fn test_from_app_id_string() {
663        crate::test_utils::with_env_vars(
664            &[
665                ("OPENLARK_APP_ID", Some("test_app_id")),
666                ("OPENLARK_APP_SECRET", Some("test_secret")),
667            ],
668            || {
669                let result: Result<Client> = Client::from_env();
670                assert!(result.is_ok());
671
672                if let Ok(client) = result {
673                    assert_eq!(client.config().app_id, "test_app_id");
674                    assert_eq!(client.config().app_secret, "test_secret");
675                }
676            },
677        );
678    }
679
680    #[test]
681    fn test_builder_default() {
682        let builder = ClientBuilder::default();
683        assert!(builder.config.app_id.is_empty());
684        assert!(builder.config.app_secret.is_empty());
685    }
686
687    #[cfg(feature = "communication")]
688    #[test]
689    fn test_communication_service_access() {
690        let client = Client::builder()
691            .app_id("test_app_id")
692            .app_secret("test_app_secret")
693            .build()
694            .unwrap();
695
696        // 单入口:meta 链式字段访问(这里只验证字段可用)
697        let _comm = &client.communication;
698    }
699}