1use openlark_core::api::{ApiRequest, HttpMethod};
34use openlark_core::constants::AccessTokenType;
35
36pub trait CatalogEndpoint {
39 fn to_url(&self) -> String;
41
42 fn method(&self) -> HttpMethod;
44
45 fn supported_access_token_types(&self) -> Option<Vec<AccessTokenType>> {
48 Some(vec![AccessTokenType::User, AccessTokenType::Tenant])
49 }
50
51 fn to_request<R>(&self) -> ApiRequest<R> {
53 self.to_request_with_url(self.to_url())
54 }
55
56 fn to_request_with_url<R>(&self, url: impl Into<String>) -> ApiRequest<R> {
58 let url = url.into();
59 let mut req = match self.method() {
60 HttpMethod::Get => ApiRequest::get(url),
61 HttpMethod::Post => ApiRequest::post(url),
62 HttpMethod::Put => ApiRequest::put(url),
63 HttpMethod::Delete => ApiRequest::delete(url),
64 HttpMethod::Patch => ApiRequest::patch(url),
65 _ => unreachable!(
66 "CatalogEndpoint encountered unknown HttpMethod variant — this should never happen"
67 ),
68 };
69 if let Some(tokens) = self.supported_access_token_types() {
70 req = req.with_supported_access_token_types(tokens);
71 }
72 req
73 }
74}
75
76#[cfg(test)]
77pub(crate) mod test_support {
78 use super::CatalogEndpoint;
79 use openlark_core::{
80 api::{ApiRequest, HttpMethod},
81 constants::AccessTokenType,
82 };
83 use std::fmt::Debug;
84 use strum::IntoEnumIterator;
85
86 pub(crate) fn assert_endpoint_semantics<E>(
88 endpoint: E,
89 expected_method: HttpMethod,
90 expected_path: &str,
91 ) where
92 E: CatalogEndpoint,
93 {
94 assert_eq!(endpoint.to_url(), expected_path);
95 assert_eq!(endpoint.method(), expected_method);
96 assert_eq!(
97 endpoint.supported_access_token_types(),
98 Some(vec![AccessTokenType::User, AccessTokenType::Tenant])
99 );
100
101 let request: ApiRequest<()> = endpoint.to_request();
102 assert_eq!(request.method(), &expected_method);
103 assert_eq!(request.api_path(), expected_path);
104 assert_eq!(
105 request.supported_access_token_types(),
106 vec![AccessTokenType::User, AccessTokenType::Tenant]
107 );
108 }
109
110 pub(crate) fn catalog_semantics_snapshot<E>() -> String
112 where
113 E: CatalogEndpoint + IntoEnumIterator + Debug,
114 {
115 E::iter()
116 .map(|endpoint| {
117 let path = endpoint.to_url();
118 let method = endpoint.method();
119 let auth = endpoint.supported_access_token_types();
120 let request: ApiRequest<()> = endpoint.to_request();
121
122 assert_eq!(request.method(), &method);
123 assert_eq!(request.api_path(), path);
124 assert_eq!(
125 request.supported_access_token_types(),
126 auth.clone().unwrap_or_default()
127 );
128
129 format!("{endpoint:?} | {method:?} | {path} | {auth:?}")
130 })
131 .collect::<Vec<_>>()
132 .join("\n")
133 }
134}
135
136pub mod base;
137pub use base::BaseApiV2;
138
139pub mod bitable;
140pub use bitable::BitableApiV1;
141
142pub mod docs;
143pub use docs::DocsApiV1;
144
145pub mod docx;
146pub use docx::DocxApiV1;
147
148#[derive(Debug, Clone, PartialEq)]
151#[cfg_attr(test, derive(strum_macros::EnumIter))]
152pub enum CcmDocApiOld {
153 Create,
155 Meta(String), SheetMeta(String), RawContent(String), Content(String), BatchUpdate(String), }
166
167impl CcmDocApiOld {
168 pub fn to_url(&self) -> String {
170 match self {
171 CcmDocApiOld::Create => "/open-apis/doc/v2/create".to_string(),
172 CcmDocApiOld::Meta(doc_token) => {
173 format!("/open-apis/doc/v2/meta/{doc_token}")
174 }
175 CcmDocApiOld::SheetMeta(doc_token) => {
176 format!("/open-apis/doc/v2/{doc_token}/sheet_meta")
177 }
178 CcmDocApiOld::RawContent(doc_token) => {
179 format!("/open-apis/doc/v2/{doc_token}/raw_content")
180 }
181 CcmDocApiOld::Content(doc_token) => {
182 format!("/open-apis/doc/v2/{doc_token}/content")
183 }
184 CcmDocApiOld::BatchUpdate(doc_token) => {
185 format!("/open-apis/doc/v2/{doc_token}/batch_update")
186 }
187 }
188 }
189
190 pub fn to_request<R>(&self) -> ApiRequest<R> {
192 <Self as CatalogEndpoint>::to_request(self)
193 }
194}
195
196impl CatalogEndpoint for CcmDocApiOld {
197 fn to_url(&self) -> String {
198 CcmDocApiOld::to_url(self)
199 }
200
201 fn method(&self) -> HttpMethod {
202 match self {
203 Self::Create | Self::BatchUpdate(_) => HttpMethod::Post,
204 Self::Meta(_) | Self::SheetMeta(_) | Self::RawContent(_) | Self::Content(_) => {
205 HttpMethod::Get
206 }
207 }
208 }
209
210 }
212
213#[derive(Debug, Clone, PartialEq)]
216#[cfg_attr(test, derive(strum_macros::EnumIter))]
217pub enum CcmDocsApiOld {
218 SearchObject,
220 Meta,
222}
223
224impl CcmDocsApiOld {
225 pub fn to_url(&self) -> String {
227 match self {
228 CcmDocsApiOld::SearchObject => "/open-apis/suite/docs-api/search/object".to_string(),
229 CcmDocsApiOld::Meta => "/open-apis/suite/docs-api/meta".to_string(),
230 }
231 }
232
233 pub fn to_request<R>(&self) -> ApiRequest<R> {
235 <Self as CatalogEndpoint>::to_request(self)
236 }
237}
238
239impl CatalogEndpoint for CcmDocsApiOld {
240 fn to_url(&self) -> String {
241 CcmDocsApiOld::to_url(self)
242 }
243
244 fn method(&self) -> HttpMethod {
245 match self {
246 Self::SearchObject => HttpMethod::Post,
247 Self::Meta => HttpMethod::Get,
248 }
249 }
250
251 }
253
254pub mod drive;
255pub use drive::{
256 CcmDriveExplorerApi, CcmDriveExplorerApiOld, DriveApi, PermissionApi, PermissionApiOld,
257};
258
259pub mod sheets;
260pub use sheets::{CcmSheetApiOld, SheetsApiV3};
261
262pub mod wiki;
263pub use wiki::{WikiApi, WikiApiV1, WikiApiV2};
264
265pub mod lingo;
266pub use lingo::LingoApiV1;
267
268pub mod baike;
269pub use baike::BaikeApiV1;
270
271pub mod minutes;
272pub use minutes::MinutesApiV1;
273
274pub const LINGO_API_V1: &str = "/open-apis/lingo/v1";
276
277#[cfg(test)]
278mod tests {
279 use super::*;
280 use crate::common::api_endpoints::test_support::catalog_semantics_snapshot;
281 use openlark_core::api::{ApiRequest, HttpMethod};
282 use openlark_core::constants::AccessTokenType;
283
284 #[test]
285 fn base_catalog_semantics_snapshot() {
286 insta::assert_snapshot!(catalog_semantics_snapshot::<BaseApiV2>());
287 }
288
289 #[test]
290 fn ccm_doc_old_catalog_semantics_snapshot() {
291 insta::assert_snapshot!(catalog_semantics_snapshot::<CcmDocApiOld>());
292 }
293
294 #[test]
295 fn ccm_docs_old_catalog_semantics_snapshot() {
296 insta::assert_snapshot!(catalog_semantics_snapshot::<CcmDocsApiOld>());
297 }
298
299 #[test]
301 fn test_base_api_v2_role_create() {
302 let endpoint = BaseApiV2::RoleCreate("app_token_123".to_string());
303 assert_eq!(
304 endpoint.to_url(),
305 "/open-apis/base/v2/apps/app_token_123/roles"
306 );
307 assert_eq!(endpoint.method(), HttpMethod::Post);
308 let req: ApiRequest<()> = endpoint.to_request();
309 assert_eq!(req.method(), &HttpMethod::Post);
310 assert_eq!(
312 endpoint.supported_access_token_types(),
313 Some(vec![AccessTokenType::User, AccessTokenType::Tenant])
314 );
315 assert_eq!(
316 req.supported_access_token_types(),
317 vec![AccessTokenType::User, AccessTokenType::Tenant]
318 );
319 }
320
321 #[test]
322 fn test_base_api_v2_role_update() {
323 let endpoint =
324 BaseApiV2::RoleUpdate("app_token_123".to_string(), "role_id_456".to_string());
325 assert_eq!(
326 endpoint.to_url(),
327 "/open-apis/base/v2/apps/app_token_123/roles/role_id_456"
328 );
329 assert_eq!(endpoint.method(), HttpMethod::Put);
330 let req: ApiRequest<()> = endpoint.to_request();
331 assert_eq!(req.method(), &HttpMethod::Put);
332 assert_eq!(
333 endpoint.supported_access_token_types(),
334 Some(vec![AccessTokenType::User, AccessTokenType::Tenant])
335 );
336 assert_eq!(
337 req.supported_access_token_types(),
338 vec![AccessTokenType::User, AccessTokenType::Tenant]
339 );
340 }
341
342 #[test]
343 fn test_base_api_v2_role_list() {
344 let endpoint = BaseApiV2::RoleList("app_token_123".to_string());
345 assert_eq!(
346 endpoint.to_url(),
347 "/open-apis/base/v2/apps/app_token_123/roles"
348 );
349 assert_eq!(endpoint.method(), HttpMethod::Get);
350 let req: ApiRequest<()> = endpoint.to_request();
351 assert_eq!(req.method(), &HttpMethod::Get);
352 assert_eq!(
353 endpoint.supported_access_token_types(),
354 Some(vec![AccessTokenType::User, AccessTokenType::Tenant])
355 );
356 assert_eq!(
357 req.supported_access_token_types(),
358 vec![AccessTokenType::User, AccessTokenType::Tenant]
359 );
360 }
361
362 #[test]
363 fn test_base_api_v2_with_special_chars() {
364 let endpoint = BaseApiV2::RoleCreate("app-token_123".to_string());
365 assert!(endpoint.to_url().contains("app-token_123"));
366 }
367
368 #[test]
370 fn test_minutes_api_v1_get() {
371 let endpoint = MinutesApiV1::Get("minute_token_123".to_string());
372 assert_eq!(
373 endpoint.to_url(),
374 "/open-apis/minutes/v1/minutes/minute_token_123"
375 );
376 }
377
378 #[test]
379 fn test_minutes_api_v1_media_get() {
380 let endpoint = MinutesApiV1::MediaGet("minute_token_123".to_string());
381 assert_eq!(
382 endpoint.to_url(),
383 "/open-apis/minutes/v1/minutes/minute_token_123/media"
384 );
385 }
386
387 #[test]
388 fn test_minutes_api_v1_transcript_get() {
389 let endpoint = MinutesApiV1::TranscriptGet("minute_token_123".to_string());
390 assert_eq!(
391 endpoint.to_url(),
392 "/open-apis/minutes/v1/minutes/minute_token_123/transcript"
393 );
394 }
395
396 #[test]
397 fn test_minutes_api_v1_statistics_get() {
398 let endpoint = MinutesApiV1::StatisticsGet("minute_token_123".to_string());
399 assert_eq!(
400 endpoint.to_url(),
401 "/open-apis/minutes/v1/minutes/minute_token_123/statistics"
402 );
403 }
404
405 #[test]
406 fn minute_subscription_issue_194_endpoints() {
407 assert_eq!(
408 MinutesApiV1::Subscription.to_url(),
409 "/open-apis/minutes/v1/minutes/subscription"
410 );
411 assert_eq!(
412 MinutesApiV1::Unsubscription.to_url(),
413 "/open-apis/minutes/v1/minutes/unsubscription"
414 );
415 }
416
417 #[test]
419 fn test_wiki_api_v1_node_search() {
420 let endpoint = WikiApiV1::NodeSearch;
421 assert_eq!(endpoint.to_url(), "/open-apis/wiki/v1/nodes/search");
422 }
423
424 #[test]
426 fn test_docs_api_v1_content_get() {
427 let endpoint = DocsApiV1::ContentGet;
428 assert_eq!(endpoint.to_url(), "/open-apis/docs/v1/content");
429 }
430
431 #[test]
433 fn test_docx_api_v1_document_create() {
434 let endpoint = DocxApiV1::DocumentCreate;
435 assert_eq!(endpoint.to_url(), "/open-apis/docx/v1/documents");
436 }
437
438 #[test]
439 fn test_docx_api_v1_document_get() {
440 let endpoint = DocxApiV1::DocumentGet("doc_id_123".to_string());
441 assert_eq!(endpoint.to_url(), "/open-apis/docx/v1/documents/doc_id_123");
442 }
443
444 #[test]
445 fn test_docx_api_v1_document_block_list() {
446 let endpoint = DocxApiV1::DocumentBlockList("doc_id_123".to_string());
447 assert_eq!(
448 endpoint.to_url(),
449 "/open-apis/docx/v1/documents/doc_id_123/blocks"
450 );
451 }
452
453 #[test]
454 fn test_docx_api_v1_chat_announcement_get() {
455 let endpoint = DocxApiV1::ChatAnnouncementGet("chat_id_123".to_string());
456 assert_eq!(
457 endpoint.to_url(),
458 "/open-apis/docx/v1/chats/chat_id_123/announcement"
459 );
460 }
461
462 #[test]
463 fn test_docx_api_v1_document_convert() {
464 let endpoint = DocxApiV1::DocumentConvert;
465 assert_eq!(
466 endpoint.to_url(),
467 "/open-apis/docx/documents/blocks/convert"
468 );
469 }
470
471 #[test]
472 fn test_docx_api_v1_document_block_children_create() {
473 let endpoint = DocxApiV1::DocumentBlockChildrenCreate(
474 "doc_id_123".to_string(),
475 "block_id_456".to_string(),
476 );
477 assert_eq!(
478 endpoint.to_url(),
479 "/open-apis/docx/v1/documents/doc_id_123/blocks/block_id_456/children"
480 );
481 }
482
483 #[test]
485 fn test_wiki_api_v2_space_list() {
486 let endpoint = WikiApiV2::SpaceList;
487 assert_eq!(endpoint.to_url(), "/open-apis/wiki/v2/spaces");
488 }
489
490 #[test]
491 fn test_wiki_api_v2_space_get() {
492 let endpoint = WikiApiV2::SpaceGet("space_id_123".to_string());
493 assert_eq!(endpoint.to_url(), "/open-apis/wiki/v2/spaces/space_id_123");
494 }
495
496 #[test]
497 fn test_wiki_api_v2_space_create() {
498 let endpoint = WikiApiV2::SpaceCreate;
499 assert_eq!(endpoint.to_url(), "/open-apis/wiki/v2/spaces");
500 }
501
502 #[test]
503 fn test_wiki_api_v2_space_node_list() {
504 let endpoint = WikiApiV2::SpaceNodeList("space_id_123".to_string());
505 assert_eq!(
506 endpoint.to_url(),
507 "/open-apis/wiki/v2/spaces/space_id_123/nodes"
508 );
509 }
510
511 #[test]
512 fn test_wiki_api_v2_space_member_delete() {
513 let endpoint =
514 WikiApiV2::SpaceMemberDelete("space_id_123".to_string(), "member_id_456".to_string());
515 assert_eq!(
516 endpoint.to_url(),
517 "/open-apis/wiki/v2/spaces/space_id_123/members/member_id_456"
518 );
519 }
520
521 #[test]
522 fn test_wiki_api_v2_task_get() {
523 let endpoint = WikiApiV2::TaskGet("task_id_123".to_string());
524 assert_eq!(endpoint.to_url(), "/open-apis/wiki/v2/tasks/task_id_123");
525 }
526
527 #[test]
529 fn test_ccm_doc_api_old_create() {
530 let endpoint = CcmDocApiOld::Create;
531 assert_eq!(endpoint.to_url(), "/open-apis/doc/v2/create");
532 }
533
534 #[test]
535 fn test_ccm_doc_api_old_meta() {
536 let endpoint = CcmDocApiOld::Meta("doc_token_123".to_string());
537 assert_eq!(endpoint.to_url(), "/open-apis/doc/v2/meta/doc_token_123");
538 }
539
540 #[test]
541 fn test_ccm_doc_api_old_raw_content() {
542 let endpoint = CcmDocApiOld::RawContent("doc_token_123".to_string());
543 assert_eq!(
544 endpoint.to_url(),
545 "/open-apis/doc/v2/doc_token_123/raw_content"
546 );
547 }
548
549 #[test]
550 fn test_ccm_doc_api_old_batch_update() {
551 let endpoint = CcmDocApiOld::BatchUpdate("doc_token_123".to_string());
552 assert_eq!(
553 endpoint.to_url(),
554 "/open-apis/doc/v2/doc_token_123/batch_update"
555 );
556 }
557
558 #[test]
560 fn test_ccm_docs_api_old_search_object() {
561 let endpoint = CcmDocsApiOld::SearchObject;
562 assert_eq!(endpoint.to_url(), "/open-apis/suite/docs-api/search/object");
563 }
564
565 #[test]
566 fn test_ccm_docs_api_old_meta() {
567 let endpoint = CcmDocsApiOld::Meta;
568 assert_eq!(endpoint.to_url(), "/open-apis/suite/docs-api/meta");
569 }
570
571 #[test]
573 fn test_ccm_drive_explorer_api_old_root_folder_meta() {
574 let endpoint = CcmDriveExplorerApiOld::RootFolderMeta;
575 assert_eq!(
576 endpoint.to_url(),
577 "/open-apis/drive/explorer/v2/root_folder/meta"
578 );
579 }
580
581 #[test]
582 fn test_ccm_drive_explorer_api_old_folder_meta() {
583 let endpoint = CcmDriveExplorerApiOld::FolderMeta("folder_token_123".to_string());
584 assert_eq!(
585 endpoint.to_url(),
586 "/open-apis/drive/explorer/v2/folder/folder_token_123/meta"
587 );
588 }
589
590 #[test]
591 fn test_ccm_drive_explorer_api_old_file_copy() {
592 let endpoint = CcmDriveExplorerApiOld::FileCopy("file_token_123".to_string());
593 assert_eq!(
594 endpoint.to_url(),
595 "/open-apis/drive/explorer/v2/file/copy/files/file_token_123"
596 );
597 }
598
599 #[test]
601 fn test_ccm_drive_explorer_api_root_folder_meta() {
602 let endpoint = CcmDriveExplorerApi::RootFolderMeta;
603 assert_eq!(
604 endpoint.to_url(),
605 "/open-apis/drive/v1/explorer/root_folder/meta"
606 );
607 }
608
609 #[test]
610 fn test_ccm_drive_explorer_api_folder_meta() {
611 let endpoint = CcmDriveExplorerApi::FolderMeta("folder_token_123".to_string());
612 assert_eq!(
613 endpoint.to_url(),
614 "/open-apis/drive/v1/explorer/folder/folder_token_123/meta"
615 );
616 }
617
618 #[test]
619 fn test_ccm_drive_explorer_api_folder() {
620 let endpoint = CcmDriveExplorerApi::Folder;
621 assert_eq!(endpoint.to_url(), "/open-apis/drive/v1/explorer/folder");
622 }
623
624 #[test]
625 fn test_ccm_drive_explorer_api_to_url_with_params() {
626 let endpoint = CcmDriveExplorerApi::RootFolderMeta;
627 let params = vec![("key", "value".to_string())];
628 let url = endpoint.to_url_with_params(¶ms);
629 assert!(url.contains("?"));
630 assert!(url.contains("key=value"));
631 }
632
633 #[test]
634 fn test_ccm_drive_explorer_api_to_url_with_empty_params() {
635 let endpoint = CcmDriveExplorerApi::RootFolderMeta;
636 let params: Vec<(&str, String)> = vec![];
637 let url = endpoint.to_url_with_params(¶ms);
638 assert!(!url.contains("?"));
639 }
640
641 #[test]
642 fn test_ccm_drive_explorer_api_to_url_with_special_chars() {
643 let endpoint = CcmDriveExplorerApi::RootFolderMeta;
644 let params = vec![("query", "hello world".to_string())];
645 let url = endpoint.to_url_with_params(¶ms);
646 assert!(url.contains("%20"));
647 }
648
649 #[test]
651 fn test_permission_api_member_permitted() {
652 let endpoint = PermissionApi::MemberPermitted;
653 assert_eq!(
654 endpoint.to_url(),
655 "/open-apis/drive/v1/permission/member/permitted"
656 );
657 }
658
659 #[test]
660 fn test_permission_api_member_transfer() {
661 let endpoint = PermissionApi::MemberTransfer;
662 assert_eq!(
663 endpoint.to_url(),
664 "/open-apis/drive/v1/permission/member/transfer"
665 );
666 }
667
668 #[test]
669 fn test_permission_api_public() {
670 let endpoint = PermissionApi::Public;
671 assert_eq!(
672 endpoint.to_url(),
673 "/open-apis/drive/v1/permission/v2/public/"
674 );
675 }
676
677 #[test]
679 fn test_permission_api_old_member_permitted() {
680 let endpoint = PermissionApiOld::MemberPermitted;
681 assert_eq!(
682 endpoint.to_url(),
683 "/open-apis/drive/v1/permission/member/permitted"
684 );
685 }
686
687 #[test]
688 fn test_permission_api_old_public() {
689 let endpoint = PermissionApiOld::Public;
690 assert_eq!(
691 endpoint.to_url(),
692 "/open-apis/drive/v1/permission/v2/public/"
693 );
694 }
695}