Skip to main content

openlark_docs/common/
chain.rs

1//! # openlark-docs 链式调用入口(简化为仅配置获取)
2//!
3//! ## 设计理念
4//!
5//! openlark-docs 涵盖多个 bizTag/Project(ccm/base/bitable/baike/minutes 等),
6//! 提供简洁的配置获取入口,Request 构建仍使用各 `*RequestBuilder/*Request` 的 `new(config)` / `execute(...)`。
7//!
8//! ## 推荐入口
9//!
10//! **公开入口** (推荐用户使用):
11//! - `DocsClient` - 文档服务的唯一公开入口
12//! - 示例: `DocsClient::new(config).ccm.config().clone()` 用于获取配置
13//!
14//! ## 推荐调用方式
15//!
16//! ```rust,ignore
17//! use openlark_core::config::Config;
18//! use openlark_docs::DocsClient;
19//!
20//! // 创建客户端
21//! let config = Config::builder()
22//!     .app_id("app_id")
23//!     .app_secret("app_secret")
24//!     .build();
25//! let docs = DocsClient::new(config);
26//!
27//! // ✅ 推荐:获取配置后构建 Request
28//! // 访问云盘服务
29//! let config = docs.ccm.config().clone();
30//! // let file = UploadAllRequest::new(config, ...).execute().await?;
31//!
32//! // 访问多维表格
33//! let config = docs.base.bitable.config().clone();
34//! // let table = CreateTableRequest::new(config, ...).execute().await?;
35//!
36//! // 访问知识库
37//! let config = docs.ccm.wiki.config().clone();
38//! // let node = CreateNodeRequest::new(config, ...).execute().await?;
39//! ```
40
41use openlark_core::config::Config;
42#[cfg(feature = "ccm-core")]
43use openlark_core::error::{business_error, CoreError};
44#[cfg(any(feature = "ccm-core", feature = "bitable"))]
45use openlark_core::SDKResult;
46use std::sync::Arc;
47
48/// Docs 链式入口:`docs.ccm.config()` / `docs.base.bitable.config()`(按 feature 裁剪)
49#[derive(Debug, Clone)]
50pub struct DocsClient {
51    config: Arc<Config>,
52
53    #[cfg(feature = "ccm-core")]
54    pub ccm: CcmClient,
55
56    #[cfg(any(feature = "base", feature = "bitable"))]
57    pub base: BaseClient,
58
59    #[cfg(any(feature = "baike", feature = "lingo"))]
60    pub baike: BaikeClient,
61
62    #[cfg(feature = "minutes")]
63    pub minutes: MinutesClient,
64}
65
66impl DocsClient {
67    pub fn new(config: Config) -> Self {
68        let config = Arc::new(config);
69        Self {
70            config: config.clone(),
71            #[cfg(feature = "ccm-core")]
72            ccm: CcmClient::new(config.clone()),
73            #[cfg(any(feature = "base", feature = "bitable"))]
74            base: BaseClient::new(config.clone()),
75            #[cfg(any(feature = "baike", feature = "lingo"))]
76            baike: BaikeClient::new(config.clone()),
77            #[cfg(feature = "minutes")]
78            minutes: MinutesClient::new(config),
79        }
80    }
81
82    pub fn config(&self) -> &Config {
83        &self.config
84    }
85
86    /// 获取文件夹下的全部子项,自动处理分页。
87    #[cfg(feature = "ccm-core")]
88    pub async fn list_folder_children_all(
89        &self,
90        folder_token: &str,
91        doc_type: Option<&str>,
92    ) -> SDKResult<Vec<crate::ccm::explorer::v2::models::FileItem>> {
93        use crate::ccm::explorer::v2::{get_folder_children, GetFolderChildrenParams};
94
95        let mut items = Vec::new();
96        let mut page_token = None;
97
98        loop {
99            let response = get_folder_children(
100                self.config(),
101                folder_token,
102                Some(GetFolderChildrenParams {
103                    page_size: Some(crate::common::constants::MAX_PAGE_SIZE),
104                    page_token: page_token.clone(),
105                    doc_type: doc_type.map(str::to_owned),
106                }),
107            )
108            .await?;
109
110            let Some(data) = response.data else {
111                break;
112            };
113
114            items.extend(data.items);
115
116            if !data.has_more {
117                break;
118            }
119
120            page_token = data.page_token;
121        }
122
123        Ok(items)
124    }
125
126    /// 读取多维表格全部记录,自动处理分页。
127    #[cfg(feature = "bitable")]
128    pub async fn search_bitable_records_all(
129        &self,
130        app_token: &str,
131        table_id: &str,
132    ) -> SDKResult<Vec<crate::base::bitable::v1::app::table::record::models::Record>> {
133        use crate::base::bitable::v1::app::table::record::search::SearchRecordRequest;
134
135        SearchRecordRequest::new(self.config().clone())
136            .app_token(app_token.to_string())
137            .table_id(table_id.to_string())
138            .automatic_fields(true)
139            .fetch_all()
140            .await
141    }
142
143    /// 读取多个单元格范围,返回聚合后的范围数据。
144    #[cfg(feature = "ccm-core")]
145    pub async fn read_multiple_ranges(
146        &self,
147        spreadsheet_token: &str,
148        ranges: Vec<String>,
149    ) -> SDKResult<crate::ccm::sheets_v2::v2::data_io::models::MultipleRangeData> {
150        use crate::ccm::sheets_v2::v2::data_io::{
151            read_multiple_ranges as read_multiple_ranges_api, ReadMultipleRangesParams,
152        };
153
154        let response = read_multiple_ranges_api(
155            self.config(),
156            spreadsheet_token,
157            ReadMultipleRangesParams {
158                ranges,
159                value_render_option: None,
160                date_render_option: None,
161            },
162        )
163        .await?;
164
165        response
166            .data
167            .ok_or_else(|| CoreError::api_data_error("读取多个范围"))
168    }
169
170    /// 批量写入多个单元格范围。
171    #[cfg(feature = "ccm-core")]
172    pub async fn write_multiple_ranges(
173        &self,
174        spreadsheet_token: &str,
175        data: Vec<crate::ccm::sheets_v2::v2::data_io::models::BatchWriteData>,
176    ) -> SDKResult<crate::ccm::sheets_v2::v2::data_io::models::BatchUpdateResult> {
177        use crate::ccm::sheets_v2::v2::data_io::{batch_write_ranges, BatchWriteRangesParams};
178
179        let response = batch_write_ranges(
180            self.config(),
181            spreadsheet_token,
182            BatchWriteRangesParams {
183                data,
184                include_style: None,
185            },
186        )
187        .await?;
188
189        response
190            .data
191            .ok_or_else(|| CoreError::api_data_error("批量写入多个范围"))
192    }
193
194    /// 根据工作表标题查找工作表。
195    #[cfg(feature = "ccm-core")]
196    pub async fn find_sheet_by_title(
197        &self,
198        spreadsheet_token: &str,
199        title: &str,
200    ) -> SDKResult<crate::ccm::sheets_v2::v2::spreadsheet::models::SpreadsheetSheetInfo> {
201        let sheets = self.list_sheet_infos(spreadsheet_token).await?;
202
203        find_sheet_info(&sheets, title)
204            .ok_or_else(|| business_error(format!("未找到工作表: {title}")))
205    }
206
207    #[cfg(feature = "ccm-core")]
208    pub async fn list_sheet_infos(
209        &self,
210        spreadsheet_token: &str,
211    ) -> SDKResult<Vec<crate::ccm::sheets_v2::v2::spreadsheet::models::SpreadsheetSheetInfo>> {
212        use crate::ccm::sheets::v3::spreadsheet::sheet::query::query_sheets;
213        use crate::ccm::sheets_v2::v2::spreadsheet::models::SpreadsheetSheetInfo;
214
215        log::info!(
216            "[OPENLARK DEBUG] list_sheet_infos called with token: {}",
217            spreadsheet_token
218        );
219
220        let response = query_sheets(self.config(), spreadsheet_token).await?;
221
222        log::info!(
223            "[OPENLARK DEBUG] query_sheets response count: {}",
224            response.sheets.len()
225        );
226
227        let sheets: Vec<SpreadsheetSheetInfo> =
228            response.sheets.into_iter().map(map_v3_sheet_info).collect();
229
230        if sheets.is_empty() {
231            return Err(CoreError::api_data_error("获取工作表列表"));
232        }
233
234        Ok(sheets)
235    }
236}
237
238#[cfg(feature = "ccm-core")]
239fn map_v3_sheet_info(
240    sheet: crate::ccm::sheets::v3::spreadsheet::Sheet,
241) -> crate::ccm::sheets_v2::v2::spreadsheet::models::SpreadsheetSheetInfo {
242    crate::ccm::sheets_v2::v2::spreadsheet::models::SpreadsheetSheetInfo {
243        sheet_id: sheet.sheet_id,
244        title: sheet.title,
245        sheet_type: sheet.resource_type,
246        row_count: sheet.grid_properties.row_count,
247        column_count: sheet.grid_properties.column_count,
248    }
249}
250
251#[cfg(feature = "ccm-core")]
252fn find_sheet_info(
253    sheets: &[crate::ccm::sheets_v2::v2::spreadsheet::models::SpreadsheetSheetInfo],
254    title: &str,
255) -> Option<crate::ccm::sheets_v2::v2::spreadsheet::models::SpreadsheetSheetInfo> {
256    sheets.iter().find(|sheet| sheet.title == title).cloned()
257}
258
259/// ccm:`docs.ccm`(云文档协同)
260#[cfg(feature = "ccm-core")]
261#[derive(Debug, Clone)]
262pub struct CcmClient {
263    config: Arc<Config>,
264}
265
266#[cfg(feature = "ccm-core")]
267impl CcmClient {
268    fn new(config: Arc<Config>) -> Self {
269        Self { config }
270    }
271
272    pub fn config(&self) -> &Config {
273        &self.config
274    }
275}
276
277/// base:`docs.base`(base/bitable 都归口在 base 模块下)
278#[cfg(any(feature = "base", feature = "bitable"))]
279#[derive(Debug, Clone)]
280pub struct BaseClient {
281    config: Arc<Config>,
282}
283
284#[cfg(any(feature = "base", feature = "bitable"))]
285impl BaseClient {
286    fn new(config: Arc<Config>) -> Self {
287        Self { config }
288    }
289
290    pub fn config(&self) -> &Config {
291        &self.config
292    }
293
294    #[cfg(feature = "bitable")]
295    pub fn bitable(&self) -> BitableClient {
296        BitableClient::new(self.config.clone())
297    }
298}
299
300/// bitable:`docs.base.bitable`(多维表格)
301#[cfg(feature = "bitable")]
302#[derive(Debug, Clone)]
303pub struct BitableClient {
304    config: Arc<Config>,
305}
306
307#[cfg(feature = "bitable")]
308impl BitableClient {
309    fn new(config: Arc<Config>) -> Self {
310        Self { config }
311    }
312
313    pub fn config(&self) -> &Config {
314        &self.config
315    }
316}
317
318/// baike:`docs.baike`(baike/lingo 相关)
319#[cfg(any(feature = "baike", feature = "lingo"))]
320#[derive(Debug, Clone)]
321pub struct BaikeClient {
322    config: Arc<Config>,
323}
324
325#[cfg(any(feature = "baike", feature = "lingo"))]
326impl BaikeClient {
327    fn new(config: Arc<Config>) -> Self {
328        Self { config }
329    }
330
331    pub fn config(&self) -> &Config {
332        &self.config
333    }
334}
335
336/// minutes:`docs.minutes`(会议纪要)
337#[cfg(feature = "minutes")]
338#[derive(Debug, Clone)]
339pub struct MinutesClient {
340    config: Arc<Config>,
341}
342
343#[cfg(feature = "minutes")]
344impl MinutesClient {
345    fn new(config: Arc<Config>) -> Self {
346        Self { config }
347    }
348
349    pub fn config(&self) -> &Config {
350        &self.config
351    }
352}
353
354#[cfg(test)]
355mod tests {
356    use serde_json;
357
358    #[test]
359    fn test_serialization_roundtrip() {
360        // 基础序列化测试
361        let json = r#"{"test": "value"}"#;
362        assert!(serde_json::from_str::<serde_json::Value>(json).is_ok());
363    }
364
365    #[test]
366    fn test_deserialization_from_json() {
367        // 基础反序列化测试
368        let json = r#"{"field": "data"}"#;
369        let value: serde_json::Value = serde_json::from_str(json).unwrap();
370        assert_eq!(value["field"], "data");
371    }
372}