1use 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#[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(Debug, 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
96impl Client {
97 pub fn from_env() -> Result<Self> {
116 Self::builder().from_env().build()
117 }
118
119 pub fn builder() -> ClientBuilder {
121 ClientBuilder::new()
122 }
123
124 pub fn config(&self) -> &Config {
126 &self.config
127 }
128
129 pub fn registry(&self) -> &DefaultServiceRegistry {
131 &self.registry
132 }
133
134 pub fn core_config(&self) -> &openlark_core::config::Config {
136 &self.core_config
137 }
138
139 pub fn api_config(&self) -> &openlark_core::config::Config {
145 &self.core_config
146 }
147
148 pub fn is_configured(&self) -> bool {
150 !self.config.app_id.is_empty() && !self.config.app_secret.is_empty()
151 }
152
153 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 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 #[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 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
227impl 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#[derive(Debug, Clone)]
259pub struct ClientBuilder {
260 config: Config,
261}
262
263impl ClientBuilder {
264 pub fn new() -> Self {
266 Self {
267 config: Config::default(),
268 }
269 }
270
271 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 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 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 pub fn enable_token_cache(mut self, enable: bool) -> Self {
291 self.config.enable_token_cache = enable;
292 self
293 }
294
295 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 pub fn timeout(mut self, timeout: std::time::Duration) -> Self {
303 self.config.timeout = timeout;
304 self
305 }
306
307 pub fn retry_count(mut self, retry_count: u32) -> Self {
309 self.config.retry_count = retry_count;
310 self
311 }
312
313 pub fn enable_log(mut self, enable: bool) -> Self {
315 self.config.enable_log = enable;
316 self
317 }
318
319 pub fn from_env(mut self) -> Self {
321 self.config.load_from_env();
322 self
323 }
324
325 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
350impl From<Config> for Result<Client> {
352 fn from(config: Config) -> Self {
353 Client::with_config(config)
354 }
355}
356
357pub trait ClientErrorHandling {
359 fn handle_error<T>(&self, result: Result<T>, operation: &str) -> Result<T>;
361 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 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 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 let builder = ClientBuilder::default();
543 let result = builder.build();
544 assert!(result.is_err()); }
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 let _comm = &client.communication;
584 }
585}