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;
267pub use lingo::LingoApiV1 as BaikeApiV1;
268
269pub mod minutes;
270pub use minutes::MinutesApiV1;
271
272pub const LINGO_API_V1: &str = "/open-apis/lingo/v1";
274
275#[cfg(test)]
276mod tests {
277 use super::*;
278 use crate::common::api_endpoints::test_support::catalog_semantics_snapshot;
279 use openlark_core::api::{ApiRequest, HttpMethod};
280 use openlark_core::constants::AccessTokenType;
281
282 #[test]
283 fn base_catalog_semantics_snapshot() {
284 insta::assert_snapshot!(catalog_semantics_snapshot::<BaseApiV2>());
285 }
286
287 #[test]
288 fn ccm_doc_old_catalog_semantics_snapshot() {
289 insta::assert_snapshot!(catalog_semantics_snapshot::<CcmDocApiOld>());
290 }
291
292 #[test]
293 fn ccm_docs_old_catalog_semantics_snapshot() {
294 insta::assert_snapshot!(catalog_semantics_snapshot::<CcmDocsApiOld>());
295 }
296
297 #[test]
299 fn test_base_api_v2_role_create() {
300 let endpoint = BaseApiV2::RoleCreate("app_token_123".to_string());
301 assert_eq!(
302 endpoint.to_url(),
303 "/open-apis/base/v2/apps/app_token_123/roles"
304 );
305 assert_eq!(endpoint.method(), HttpMethod::Post);
306 let req: ApiRequest<()> = endpoint.to_request();
307 assert_eq!(req.method(), &HttpMethod::Post);
308 assert_eq!(
310 endpoint.supported_access_token_types(),
311 Some(vec![AccessTokenType::User, AccessTokenType::Tenant])
312 );
313 assert_eq!(
314 req.supported_access_token_types(),
315 vec![AccessTokenType::User, AccessTokenType::Tenant]
316 );
317 }
318
319 #[test]
320 fn test_base_api_v2_role_update() {
321 let endpoint =
322 BaseApiV2::RoleUpdate("app_token_123".to_string(), "role_id_456".to_string());
323 assert_eq!(
324 endpoint.to_url(),
325 "/open-apis/base/v2/apps/app_token_123/roles/role_id_456"
326 );
327 assert_eq!(endpoint.method(), HttpMethod::Put);
328 let req: ApiRequest<()> = endpoint.to_request();
329 assert_eq!(req.method(), &HttpMethod::Put);
330 assert_eq!(
331 endpoint.supported_access_token_types(),
332 Some(vec![AccessTokenType::User, AccessTokenType::Tenant])
333 );
334 assert_eq!(
335 req.supported_access_token_types(),
336 vec![AccessTokenType::User, AccessTokenType::Tenant]
337 );
338 }
339
340 #[test]
341 fn test_base_api_v2_role_list() {
342 let endpoint = BaseApiV2::RoleList("app_token_123".to_string());
343 assert_eq!(
344 endpoint.to_url(),
345 "/open-apis/base/v2/apps/app_token_123/roles"
346 );
347 assert_eq!(endpoint.method(), HttpMethod::Get);
348 let req: ApiRequest<()> = endpoint.to_request();
349 assert_eq!(req.method(), &HttpMethod::Get);
350 assert_eq!(
351 endpoint.supported_access_token_types(),
352 Some(vec![AccessTokenType::User, AccessTokenType::Tenant])
353 );
354 assert_eq!(
355 req.supported_access_token_types(),
356 vec![AccessTokenType::User, AccessTokenType::Tenant]
357 );
358 }
359
360 #[test]
361 fn test_base_api_v2_with_special_chars() {
362 let endpoint = BaseApiV2::RoleCreate("app-token_123".to_string());
363 assert!(endpoint.to_url().contains("app-token_123"));
364 }
365
366 #[test]
368 fn test_minutes_api_v1_get() {
369 let endpoint = MinutesApiV1::Get("minute_token_123".to_string());
370 assert_eq!(
371 endpoint.to_url(),
372 "/open-apis/minutes/v1/minutes/minute_token_123"
373 );
374 }
375
376 #[test]
377 fn test_minutes_api_v1_media_get() {
378 let endpoint = MinutesApiV1::MediaGet("minute_token_123".to_string());
379 assert_eq!(
380 endpoint.to_url(),
381 "/open-apis/minutes/v1/minutes/minute_token_123/media"
382 );
383 }
384
385 #[test]
386 fn test_minutes_api_v1_transcript_get() {
387 let endpoint = MinutesApiV1::TranscriptGet("minute_token_123".to_string());
388 assert_eq!(
389 endpoint.to_url(),
390 "/open-apis/minutes/v1/minutes/minute_token_123/transcript"
391 );
392 }
393
394 #[test]
395 fn test_minutes_api_v1_statistics_get() {
396 let endpoint = MinutesApiV1::StatisticsGet("minute_token_123".to_string());
397 assert_eq!(
398 endpoint.to_url(),
399 "/open-apis/minutes/v1/minutes/minute_token_123/statistics"
400 );
401 }
402
403 #[test]
404 fn minute_subscription_issue_194_endpoints() {
405 assert_eq!(
406 MinutesApiV1::Subscription.to_url(),
407 "/open-apis/minutes/v1/minutes/subscription"
408 );
409 assert_eq!(
410 MinutesApiV1::Unsubscription.to_url(),
411 "/open-apis/minutes/v1/minutes/unsubscription"
412 );
413 }
414
415 #[test]
417 fn test_wiki_api_v1_node_search() {
418 let endpoint = WikiApiV1::NodeSearch;
419 assert_eq!(endpoint.to_url(), "/open-apis/wiki/v1/nodes/search");
420 }
421
422 #[test]
424 fn test_docs_api_v1_content_get() {
425 let endpoint = DocsApiV1::ContentGet;
426 assert_eq!(endpoint.to_url(), "/open-apis/docs/v1/content");
427 }
428
429 #[test]
431 fn test_docx_api_v1_document_create() {
432 let endpoint = DocxApiV1::DocumentCreate;
433 assert_eq!(endpoint.to_url(), "/open-apis/docx/v1/documents");
434 }
435
436 #[test]
437 fn test_docx_api_v1_document_get() {
438 let endpoint = DocxApiV1::DocumentGet("doc_id_123".to_string());
439 assert_eq!(endpoint.to_url(), "/open-apis/docx/v1/documents/doc_id_123");
440 }
441
442 #[test]
443 fn test_docx_api_v1_document_block_list() {
444 let endpoint = DocxApiV1::DocumentBlockList("doc_id_123".to_string());
445 assert_eq!(
446 endpoint.to_url(),
447 "/open-apis/docx/v1/documents/doc_id_123/blocks"
448 );
449 }
450
451 #[test]
452 fn test_docx_api_v1_chat_announcement_get() {
453 let endpoint = DocxApiV1::ChatAnnouncementGet("chat_id_123".to_string());
454 assert_eq!(
455 endpoint.to_url(),
456 "/open-apis/docx/v1/chats/chat_id_123/announcement"
457 );
458 }
459
460 #[test]
461 fn test_docx_api_v1_document_convert() {
462 let endpoint = DocxApiV1::DocumentConvert;
463 assert_eq!(
464 endpoint.to_url(),
465 "/open-apis/docx/documents/blocks/convert"
466 );
467 }
468
469 #[test]
470 fn test_docx_api_v1_document_block_children_create() {
471 let endpoint = DocxApiV1::DocumentBlockChildrenCreate(
472 "doc_id_123".to_string(),
473 "block_id_456".to_string(),
474 );
475 assert_eq!(
476 endpoint.to_url(),
477 "/open-apis/docx/v1/documents/doc_id_123/blocks/block_id_456/children"
478 );
479 }
480
481 #[test]
483 fn test_wiki_api_v2_space_list() {
484 let endpoint = WikiApiV2::SpaceList;
485 assert_eq!(endpoint.to_url(), "/open-apis/wiki/v2/spaces");
486 }
487
488 #[test]
489 fn test_wiki_api_v2_space_get() {
490 let endpoint = WikiApiV2::SpaceGet("space_id_123".to_string());
491 assert_eq!(endpoint.to_url(), "/open-apis/wiki/v2/spaces/space_id_123");
492 }
493
494 #[test]
495 fn test_wiki_api_v2_space_create() {
496 let endpoint = WikiApiV2::SpaceCreate;
497 assert_eq!(endpoint.to_url(), "/open-apis/wiki/v2/spaces");
498 }
499
500 #[test]
501 fn test_wiki_api_v2_space_node_list() {
502 let endpoint = WikiApiV2::SpaceNodeList("space_id_123".to_string());
503 assert_eq!(
504 endpoint.to_url(),
505 "/open-apis/wiki/v2/spaces/space_id_123/nodes"
506 );
507 }
508
509 #[test]
510 fn test_wiki_api_v2_space_member_delete() {
511 let endpoint =
512 WikiApiV2::SpaceMemberDelete("space_id_123".to_string(), "member_id_456".to_string());
513 assert_eq!(
514 endpoint.to_url(),
515 "/open-apis/wiki/v2/spaces/space_id_123/members/member_id_456"
516 );
517 }
518
519 #[test]
520 fn test_wiki_api_v2_task_get() {
521 let endpoint = WikiApiV2::TaskGet("task_id_123".to_string());
522 assert_eq!(endpoint.to_url(), "/open-apis/wiki/v2/tasks/task_id_123");
523 }
524
525 #[test]
527 fn test_ccm_doc_api_old_create() {
528 let endpoint = CcmDocApiOld::Create;
529 assert_eq!(endpoint.to_url(), "/open-apis/doc/v2/create");
530 }
531
532 #[test]
533 fn test_ccm_doc_api_old_meta() {
534 let endpoint = CcmDocApiOld::Meta("doc_token_123".to_string());
535 assert_eq!(endpoint.to_url(), "/open-apis/doc/v2/meta/doc_token_123");
536 }
537
538 #[test]
539 fn test_ccm_doc_api_old_raw_content() {
540 let endpoint = CcmDocApiOld::RawContent("doc_token_123".to_string());
541 assert_eq!(
542 endpoint.to_url(),
543 "/open-apis/doc/v2/doc_token_123/raw_content"
544 );
545 }
546
547 #[test]
548 fn test_ccm_doc_api_old_batch_update() {
549 let endpoint = CcmDocApiOld::BatchUpdate("doc_token_123".to_string());
550 assert_eq!(
551 endpoint.to_url(),
552 "/open-apis/doc/v2/doc_token_123/batch_update"
553 );
554 }
555
556 #[test]
558 fn test_ccm_docs_api_old_search_object() {
559 let endpoint = CcmDocsApiOld::SearchObject;
560 assert_eq!(endpoint.to_url(), "/open-apis/suite/docs-api/search/object");
561 }
562
563 #[test]
564 fn test_ccm_docs_api_old_meta() {
565 let endpoint = CcmDocsApiOld::Meta;
566 assert_eq!(endpoint.to_url(), "/open-apis/suite/docs-api/meta");
567 }
568
569 #[test]
571 fn test_ccm_drive_explorer_api_old_root_folder_meta() {
572 let endpoint = CcmDriveExplorerApiOld::RootFolderMeta;
573 assert_eq!(
574 endpoint.to_url(),
575 "/open-apis/drive/explorer/v2/root_folder/meta"
576 );
577 }
578
579 #[test]
580 fn test_ccm_drive_explorer_api_old_folder_meta() {
581 let endpoint = CcmDriveExplorerApiOld::FolderMeta("folder_token_123".to_string());
582 assert_eq!(
583 endpoint.to_url(),
584 "/open-apis/drive/explorer/v2/folder/folder_token_123/meta"
585 );
586 }
587
588 #[test]
589 fn test_ccm_drive_explorer_api_old_file_copy() {
590 let endpoint = CcmDriveExplorerApiOld::FileCopy("file_token_123".to_string());
591 assert_eq!(
592 endpoint.to_url(),
593 "/open-apis/drive/explorer/v2/file/copy/files/file_token_123"
594 );
595 }
596
597 #[test]
599 fn test_ccm_drive_explorer_api_root_folder_meta() {
600 let endpoint = CcmDriveExplorerApi::RootFolderMeta;
601 assert_eq!(
602 endpoint.to_url(),
603 "/open-apis/drive/v1/explorer/root_folder/meta"
604 );
605 }
606
607 #[test]
608 fn test_ccm_drive_explorer_api_folder_meta() {
609 let endpoint = CcmDriveExplorerApi::FolderMeta("folder_token_123".to_string());
610 assert_eq!(
611 endpoint.to_url(),
612 "/open-apis/drive/v1/explorer/folder/folder_token_123/meta"
613 );
614 }
615
616 #[test]
617 fn test_ccm_drive_explorer_api_folder() {
618 let endpoint = CcmDriveExplorerApi::Folder;
619 assert_eq!(endpoint.to_url(), "/open-apis/drive/v1/explorer/folder");
620 }
621
622 #[test]
623 fn test_ccm_drive_explorer_api_to_url_with_params() {
624 let endpoint = CcmDriveExplorerApi::RootFolderMeta;
625 let params = vec![("key", "value".to_string())];
626 let url = endpoint.to_url_with_params(¶ms);
627 assert!(url.contains("?"));
628 assert!(url.contains("key=value"));
629 }
630
631 #[test]
632 fn test_ccm_drive_explorer_api_to_url_with_empty_params() {
633 let endpoint = CcmDriveExplorerApi::RootFolderMeta;
634 let params: Vec<(&str, String)> = vec![];
635 let url = endpoint.to_url_with_params(¶ms);
636 assert!(!url.contains("?"));
637 }
638
639 #[test]
640 fn test_ccm_drive_explorer_api_to_url_with_special_chars() {
641 let endpoint = CcmDriveExplorerApi::RootFolderMeta;
642 let params = vec![("query", "hello world".to_string())];
643 let url = endpoint.to_url_with_params(¶ms);
644 assert!(url.contains("%20"));
645 }
646
647 #[test]
649 fn test_permission_api_member_permitted() {
650 let endpoint = PermissionApi::MemberPermitted;
651 assert_eq!(
652 endpoint.to_url(),
653 "/open-apis/drive/v1/permission/member/permitted"
654 );
655 }
656
657 #[test]
658 fn test_permission_api_member_transfer() {
659 let endpoint = PermissionApi::MemberTransfer;
660 assert_eq!(
661 endpoint.to_url(),
662 "/open-apis/drive/v1/permission/member/transfer"
663 );
664 }
665
666 #[test]
667 fn test_permission_api_public() {
668 let endpoint = PermissionApi::Public;
669 assert_eq!(
670 endpoint.to_url(),
671 "/open-apis/drive/v1/permission/v2/public/"
672 );
673 }
674
675 #[test]
677 fn test_permission_api_old_member_permitted() {
678 let endpoint = PermissionApiOld::MemberPermitted;
679 assert_eq!(
680 endpoint.to_url(),
681 "/open-apis/drive/v1/permission/member/permitted"
682 );
683 }
684
685 #[test]
686 fn test_permission_api_old_public() {
687 let endpoint = PermissionApiOld::Public;
688 assert_eq!(
689 endpoint.to_url(),
690 "/open-apis/drive/v1/permission/v2/public/"
691 );
692 }
693}