1use 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#[cfg(feature = "auth")]
15#[derive(Debug, Clone)]
16pub struct AuthClient {
17 pub app: openlark_auth::AuthService,
19 pub user: openlark_auth::AuthenService,
21 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#[derive(Clone)]
63pub struct Client {
64 config: Arc<Config>,
66 registry: Arc<DefaultServiceRegistry>,
68 core_config: openlark_core::config::Config,
70
71 #[cfg(feature = "cardkit")]
73 pub cardkit: openlark_cardkit::CardkitClient,
74
75 #[cfg(feature = "auth")]
77 pub auth: AuthClient,
78
79 #[cfg(feature = "docs")]
81 pub docs: openlark_docs::DocsClient,
82
83 #[cfg(feature = "communication")]
85 pub communication: openlark_communication::CommunicationClient,
86
87 #[cfg(feature = "hr")]
89 pub hr: openlark_hr::HrClient,
90
91 #[cfg(feature = "meeting")]
93 pub meeting: openlark_meeting::MeetingClient,
94
95 #[cfg(feature = "ai")]
97 pub ai: openlark_ai::AiClient,
98
99 #[cfg(feature = "workflow")]
101 pub workflow: crate::WorkflowClient,
102
103 #[cfg(feature = "platform")]
105 pub platform: crate::PlatformClient,
106
107 #[cfg(feature = "application")]
109 pub application: crate::ApplicationClient,
110
111 #[cfg(feature = "helpdesk")]
113 pub helpdesk: crate::HelpdeskClient,
114
115 #[cfg(feature = "mail")]
117 pub mail: crate::MailClient,
118
119 #[cfg(feature = "analytics")]
121 pub analytics: crate::AnalyticsClient,
122
123 #[cfg(feature = "user")]
125 pub user: crate::UserClient,
126
127 #[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 pub fn from_env() -> Result<Self> {
178 Self::builder().from_env().build()
179 }
180
181 pub fn builder() -> ClientBuilder {
183 ClientBuilder::new()
184 }
185
186 pub fn config(&self) -> &Config {
188 &self.config
189 }
190
191 pub fn registry(&self) -> &DefaultServiceRegistry {
193 &self.registry
194 }
195
196 pub fn core_config(&self) -> &openlark_core::config::Config {
198 &self.core_config
199 }
200
201 pub fn api_config(&self) -> &openlark_core::config::Config {
207 &self.core_config
208 }
209
210 pub fn is_configured(&self) -> bool {
212 !self.config.app_id.is_empty() && !self.config.app_secret.is_empty()
213 }
214
215 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 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 #[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 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
341impl 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#[derive(Debug, Clone)]
373pub struct ClientBuilder {
374 config: Config,
375}
376
377impl ClientBuilder {
378 pub fn new() -> Self {
380 Self {
381 config: Config::default(),
382 }
383 }
384
385 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 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 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 pub fn enable_token_cache(mut self, enable: bool) -> Self {
405 self.config.enable_token_cache = enable;
406 self
407 }
408
409 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 pub fn timeout(mut self, timeout: std::time::Duration) -> Self {
417 self.config.timeout = timeout;
418 self
419 }
420
421 pub fn retry_count(mut self, retry_count: u32) -> Self {
423 self.config.retry_count = retry_count;
424 self
425 }
426
427 pub fn enable_log(mut self, enable: bool) -> Self {
429 self.config.enable_log = enable;
430 self
431 }
432
433 pub fn from_env(mut self) -> Self {
435 self.config.load_from_env();
436 self
437 }
438
439 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
464impl From<Config> for Result<Client> {
466 fn from(config: Config) -> Self {
467 Client::with_config(config)
468 }
469}
470
471pub trait ClientErrorHandling {
473 fn handle_error<T>(&self, result: Result<T>, operation: &str) -> Result<T>;
475 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 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 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 let builder = ClientBuilder::default();
657 let result = builder.build();
658 assert!(result.is_err()); }
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 let _comm = &client.communication;
698 }
699}