Skip to main content

openlark_client/
client.rs

1//! OpenLark Client - 全新简化架构
2//!
3//! 极简设计:仅保留 meta 链式字段访问(单入口,KISS)
4
5use crate::{
6    error::{with_context, with_operation_context},
7    traits::LarkClient,
8    Config, DefaultServiceRegistry, Result,
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(Debug, 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
96impl Client {
97    /// 🔥 从环境变量创建客户端
98    ///
99    /// # 环境变量
100    /// ```bash
101    /// export OPENLARK_APP_ID=your_app_id
102    /// export OPENLARK_APP_SECRET=your_app_secret
103    /// export OPENLARK_BASE_URL=https://open.feishu.cn  # 可选
104    /// ```
105    ///
106    /// # 返回值
107    /// 返回配置好的客户端实例或错误
108    ///
109    /// # 示例
110    /// ```rust,no_run
111    /// use openlark_client::Client;
112    ///
113    /// let _client = Client::from_env();
114    /// ```
115    pub fn from_env() -> Result<Self> {
116        Self::builder().from_env().build()
117    }
118
119    /// 🏗️ 创建构建器
120    pub fn builder() -> ClientBuilder {
121        ClientBuilder::new()
122    }
123
124    /// 🔧 获取客户端配置
125    pub fn config(&self) -> &Config {
126        &self.config
127    }
128
129    /// 📋 获取服务注册表
130    pub fn registry(&self) -> &DefaultServiceRegistry {
131        &self.registry
132    }
133
134    /// 🔧 获取底层 core 配置(高级用法/调试用)
135    pub fn core_config(&self) -> &openlark_core::config::Config {
136        &self.core_config
137    }
138
139    /// 🔧 获取可直接传给函数式 API 的认证后配置
140    ///
141    /// 与 [`Self::core_config`] 返回同一份配置,保留这个别名是为了让
142    /// 业务侧更容易理解它的用途:可直接传给 `openlark_docs::*`、
143    /// `openlark_auth::*` 等函数式 API。
144    pub fn api_config(&self) -> &openlark_core::config::Config {
145        &self.core_config
146    }
147
148    /// ✅ 检查客户端是否已正确配置
149    pub fn is_configured(&self) -> bool {
150        !self.config.app_id.is_empty() && !self.config.app_secret.is_empty()
151    }
152
153    /// 🆕 创建带有自定义配置的客户端
154    pub fn with_config(config: Config) -> Result<Self> {
155        let validation_result = config.validate();
156        if let Err(err) = validation_result {
157            return with_context(Err(err), "operation", "Client::with_config");
158        }
159
160        let config = Arc::new(config);
161        let mut registry = DefaultServiceRegistry::new();
162
163        // 加载启用的服务
164        if let Err(err) = crate::registry::bootstrap::register_compiled_services(&mut registry) {
165            return with_operation_context(Err(err), "Client::with_config", "service_loading");
166        }
167
168        let registry = Arc::new(registry);
169
170        // 从 client Config 获取 core Config
171        #[cfg(feature = "auth")]
172        let base_core_config = config.as_ref().build_core_config();
173        #[cfg(feature = "auth")]
174        let core_config = config
175            .as_ref()
176            .get_or_build_core_config_with_token_provider();
177        #[cfg(not(feature = "auth"))]
178        let core_config = config.as_ref().get_or_build_core_config();
179
180        #[cfg(feature = "cardkit")]
181        let cardkit = openlark_cardkit::CardkitClient::new(core_config.clone());
182
183        #[cfg(feature = "auth")]
184        let auth = AuthClient::new(base_core_config.clone());
185
186        #[cfg(feature = "docs")]
187        let docs = openlark_docs::DocsClient::new(core_config.clone());
188
189        #[cfg(feature = "communication")]
190        let communication = openlark_communication::CommunicationClient::new(core_config.clone());
191
192        #[cfg(feature = "hr")]
193        let hr = openlark_hr::HrClient::new(core_config.clone());
194
195        #[cfg(feature = "meeting")]
196        let meeting = openlark_meeting::MeetingClient::new(core_config.clone());
197
198        Ok(Client {
199            config,
200            registry,
201            core_config: core_config.clone(),
202            #[cfg(feature = "cardkit")]
203            cardkit,
204            #[cfg(feature = "auth")]
205            auth,
206            #[cfg(feature = "docs")]
207            docs,
208            #[cfg(feature = "communication")]
209            communication,
210            #[cfg(feature = "hr")]
211            hr,
212            #[cfg(feature = "meeting")]
213            meeting,
214        })
215    }
216
217    /// 🔧 执行带有错误上下文的操作
218    pub async fn execute_with_context<F, T>(&self, operation: &str, f: F) -> Result<T>
219    where
220        F: std::future::Future<Output = Result<T>>,
221    {
222        let result = f.await;
223        with_operation_context(result, operation, "Client")
224    }
225}
226
227// 实现LarkClient trait
228impl LarkClient for Client {
229    fn config(&self) -> &Config {
230        &self.config
231    }
232
233    fn is_configured(&self) -> bool {
234        self.is_configured()
235    }
236}
237
238/// 🏗️ 客户端构建器 - 流畅API
239///
240/// 提供链式调用的客户端构建方式
241///
242/// # 示例
243/// ```rust,no_run
244/// use openlark_client::Client;
245/// use openlark_client::Result;
246/// use std::time::Duration;
247///
248/// fn main() -> Result<()> {
249///     let _client = Client::builder()
250///         .app_id("your_app_id")
251///         .app_secret("your_app_secret")
252///         .base_url("https://open.feishu.cn")
253///         .timeout(Duration::from_secs(30))
254///         .build()?;
255///     Ok(())
256/// }
257/// ```
258#[derive(Debug, Clone)]
259pub struct ClientBuilder {
260    config: Config,
261}
262
263impl ClientBuilder {
264    /// 🆕 创建新的构建器实例
265    pub fn new() -> Self {
266        Self {
267            config: Config::default(),
268        }
269    }
270
271    /// 🆔 设置应用ID
272    pub fn app_id<S: Into<String>>(mut self, app_id: S) -> Self {
273        self.config.app_id = app_id.into();
274        self
275    }
276
277    /// 🔑 设置应用密钥
278    pub fn app_secret<S: Into<String>>(mut self, app_secret: S) -> Self {
279        self.config.app_secret = app_secret.into();
280        self
281    }
282
283    /// 🏷️ 设置应用类型(自建 / 商店)
284    pub fn app_type(mut self, app_type: openlark_core::constants::AppType) -> Self {
285        self.config.app_type = app_type;
286        self
287    }
288
289    /// 🔐 设置是否允许自动获取 token(默认 true)
290    pub fn enable_token_cache(mut self, enable: bool) -> Self {
291        self.config.enable_token_cache = enable;
292        self
293    }
294
295    /// 🌐 设置基础URL
296    pub fn base_url<S: Into<String>>(mut self, base_url: S) -> Self {
297        self.config.base_url = base_url.into();
298        self
299    }
300
301    /// ⏱️ 设置请求超时时间
302    pub fn timeout(mut self, timeout: std::time::Duration) -> Self {
303        self.config.timeout = timeout;
304        self
305    }
306
307    /// 🔄 设置重试次数
308    pub fn retry_count(mut self, retry_count: u32) -> Self {
309        self.config.retry_count = retry_count;
310        self
311    }
312
313    /// 📝 启用或禁用日志
314    pub fn enable_log(mut self, enable: bool) -> Self {
315        self.config.enable_log = enable;
316        self
317    }
318
319    /// 🌍 从环境变量加载配置
320    pub fn from_env(mut self) -> Self {
321        self.config.load_from_env();
322        self
323    }
324
325    /// 🔨 构建客户端实例
326    ///
327    /// # 返回值
328    /// 返回配置好的客户端实例或验证错误
329    ///
330    /// # 错误
331    /// 如果配置验证失败,会返回相应的错误信息,包含用户友好的恢复建议
332    pub fn build(self) -> Result<Client> {
333        let result = Client::with_config(self.config);
334        if let Err(ref error) = result {
335            tracing::error!(
336                "客户端构建失败: {}",
337                error.user_message().unwrap_or("未知错误")
338            );
339        }
340        result
341    }
342}
343
344impl Default for ClientBuilder {
345    fn default() -> Self {
346        Self::new()
347    }
348}
349
350/// Client的便利构造函数
351impl From<Config> for Result<Client> {
352    fn from(config: Config) -> Self {
353        Client::with_config(config)
354    }
355}
356
357/// 客户端错误处理扩展特征
358pub trait ClientErrorHandling {
359    /// 处理错误并添加客户端上下文
360    fn handle_error<T>(&self, result: Result<T>, operation: &str) -> Result<T>;
361    /// 处理异步错误并添加客户端上下文
362    async fn handle_async_error<T, F>(&self, f: F, operation: &str) -> Result<T>
363    where
364        F: std::future::Future<Output = Result<T>>;
365}
366
367impl ClientErrorHandling for Client {
368    fn handle_error<T>(&self, result: Result<T>, operation: &str) -> Result<T> {
369        with_operation_context(result, operation, "Client")
370    }
371
372    async fn handle_async_error<T, F>(&self, f: F, operation: &str) -> Result<T>
373    where
374        F: std::future::Future<Output = Result<T>>,
375    {
376        let result = f.await;
377        with_operation_context(result, operation, "Client")
378    }
379}
380
381#[cfg(test)]
382#[allow(unused_imports)]
383mod tests {
384    use super::*;
385    use openlark_core::error::ErrorTrait;
386    use std::time::Duration;
387
388    #[test]
389    fn test_client_builder() {
390        let client = Client::builder()
391            .app_id("test_app_id")
392            .app_secret("test_app_secret")
393            .timeout(Duration::from_secs(30))
394            .build();
395
396        assert!(client.is_ok());
397    }
398
399    #[test]
400    fn test_client_config() {
401        let config = Config {
402            app_id: "test_app_id".to_string(),
403            app_secret: "test_app_secret".to_string(),
404            base_url: "https://open.feishu.cn".to_string(),
405            ..Default::default()
406        };
407
408        let client = Client::with_config(config).unwrap();
409        assert_eq!(client.config().app_id, "test_app_id");
410        assert_eq!(client.config().app_secret, "test_app_secret");
411        assert!(client.is_configured());
412    }
413
414    #[test]
415    fn test_client_not_configured() {
416        let config = Config {
417            app_id: String::new(),
418            app_secret: String::new(),
419            ..Default::default()
420        };
421
422        let client_result = Client::with_config(config);
423        assert!(client_result.is_err());
424
425        if let Err(error) = client_result {
426            assert!(error.is_config_error() || error.is_validation_error());
427            assert!(!error.user_message().unwrap_or("未知错误").is_empty());
428        }
429    }
430
431    #[test]
432    fn test_client_clone() {
433        let client = Client::builder()
434            .app_id("test_app_id")
435            .app_secret("test_app_secret")
436            .build()
437            .unwrap();
438
439        let cloned_client = client.clone();
440        assert_eq!(client.config().app_id, cloned_client.config().app_id);
441    }
442
443    #[cfg(feature = "cardkit")]
444    #[test]
445    fn test_cardkit_chain_exists() {
446        let client = Client::builder()
447            .app_id("test_app_id")
448            .app_secret("test_app_secret")
449            .build()
450            .unwrap();
451
452        let _ = &client.cardkit.v1.card;
453    }
454
455    #[cfg(feature = "docs")]
456    #[test]
457    fn test_docs_chain_exists() {
458        let client = Client::builder()
459            .app_id("test_app_id")
460            .app_secret("test_app_secret")
461            .build()
462            .unwrap();
463
464        let _ = client.docs.config();
465    }
466
467    #[cfg(feature = "communication")]
468    #[test]
469    fn test_communication_chain_exists() {
470        let client = Client::builder()
471            .app_id("test_app_id")
472            .app_secret("test_app_secret")
473            .build()
474            .unwrap();
475
476        let _ = client.communication.config();
477    }
478
479    #[cfg(feature = "meeting")]
480    #[test]
481    fn test_meeting_chain_exists() {
482        let client = Client::builder()
483            .app_id("test_app_id")
484            .app_secret("test_app_secret")
485            .build()
486            .unwrap();
487
488        let _ = client.meeting.config();
489    }
490
491    #[test]
492    fn test_client_error_handling() {
493        let client = Client::builder()
494            .app_id("test_app_id")
495            .app_secret("test_app_secret")
496            .build()
497            .unwrap();
498
499        // 测试错误上下文处理
500        let error_result: Result<i32> =
501            Err(crate::error::validation_error("field", "validation failed"));
502        let result = client.handle_error(error_result, "test_operation");
503
504        assert!(result.is_err());
505        if let Err(error) = result {
506            assert!(error.context().has_context("operation"));
507            assert_eq!(
508                error.context().get_context("operation"),
509                Some("test_operation")
510            );
511            assert_eq!(error.context().get_context("component"), Some("Client"));
512        }
513    }
514
515    #[tokio::test]
516    async fn test_async_error_handling() {
517        let client = Client::builder()
518            .app_id("test_app_id")
519            .app_secret("test_app_secret")
520            .build()
521            .unwrap();
522
523        // 测试异步错误上下文处理
524        let result = client
525            .handle_async_error(
526                async { Err::<i32, _>(crate::error::network_error("async error")) },
527                "async_test",
528            )
529            .await;
530
531        assert!(result.is_err());
532        if let Err(error) = result {
533            assert!(error.context().has_context("operation"));
534            assert_eq!(error.context().get_context("operation"), Some("async_test"));
535            assert_eq!(error.context().get_context("component"), Some("Client"));
536        }
537    }
538
539    #[test]
540    fn test_from_env_missing_vars() {
541        // 验证默认构建器未配置 app_id/app_secret 时会失败(不依赖环境变量)。
542        let builder = ClientBuilder::default();
543        let result = builder.build();
544        assert!(result.is_err()); // 没有app_id和app_secret应该失败
545    }
546
547    #[test]
548    fn test_from_app_id_string() {
549        crate::test_utils::with_env_vars(
550            &[
551                ("OPENLARK_APP_ID", Some("test_app_id")),
552                ("OPENLARK_APP_SECRET", Some("test_secret")),
553            ],
554            || {
555                let result: Result<Client> = Client::from_env();
556                assert!(result.is_ok());
557
558                if let Ok(client) = result {
559                    assert_eq!(client.config().app_id, "test_app_id");
560                    assert_eq!(client.config().app_secret, "test_secret");
561                }
562            },
563        );
564    }
565
566    #[test]
567    fn test_builder_default() {
568        let builder = ClientBuilder::default();
569        assert!(builder.config.app_id.is_empty());
570        assert!(builder.config.app_secret.is_empty());
571    }
572
573    #[cfg(feature = "communication")]
574    #[test]
575    fn test_communication_service_access() {
576        let client = Client::builder()
577            .app_id("test_app_id")
578            .app_secret("test_app_secret")
579            .build()
580            .unwrap();
581
582        // 单入口:meta 链式字段访问(这里只验证字段可用)
583        let _comm = &client.communication;
584    }
585}