use openlark_core::config::Config;
#[cfg(feature = "ccm-core")]
use openlark_core::error::{business_error, CoreError};
#[cfg(any(feature = "ccm-core", feature = "bitable"))]
use openlark_core::SDKResult;
use std::sync::Arc;
#[derive(Debug, Clone)]
pub struct DocsClient {
config: Arc<Config>,
#[cfg(feature = "ccm-core")]
pub ccm: CcmClient,
#[cfg(any(feature = "base", feature = "bitable"))]
pub base: BaseClient,
#[cfg(any(feature = "baike", feature = "lingo"))]
pub baike: BaikeClient,
#[cfg(feature = "minutes")]
pub minutes: MinutesClient,
}
impl DocsClient {
pub fn new(config: Config) -> Self {
let config = Arc::new(config);
Self {
config: config.clone(),
#[cfg(feature = "ccm-core")]
ccm: CcmClient::new(config.clone()),
#[cfg(any(feature = "base", feature = "bitable"))]
base: BaseClient::new(config.clone()),
#[cfg(any(feature = "baike", feature = "lingo"))]
baike: BaikeClient::new(config.clone()),
#[cfg(feature = "minutes")]
minutes: MinutesClient::new(config),
}
}
pub fn config(&self) -> &Config {
&self.config
}
#[cfg(feature = "ccm-core")]
pub async fn list_folder_children_all(
&self,
folder_token: &str,
doc_type: Option<&str>,
) -> SDKResult<Vec<crate::ccm::explorer::v2::models::FileItem>> {
use crate::ccm::explorer::v2::{get_folder_children, GetFolderChildrenParams};
let mut items = Vec::new();
let mut page_token = None;
loop {
let response = get_folder_children(
self.config(),
folder_token,
Some(GetFolderChildrenParams {
page_size: Some(crate::common::constants::MAX_PAGE_SIZE),
page_token: page_token.clone(),
doc_type: doc_type.map(str::to_owned),
}),
)
.await?;
let Some(data) = response.data else {
break;
};
items.extend(data.items);
if !data.has_more {
break;
}
page_token = data.page_token;
}
Ok(items)
}
#[cfg(feature = "bitable")]
pub async fn search_bitable_records_all(
&self,
app_token: &str,
table_id: &str,
) -> SDKResult<Vec<crate::base::bitable::v1::app::table::record::models::Record>> {
use crate::base::bitable::v1::app::table::record::search::SearchRecordRequest;
SearchRecordRequest::new(self.config().clone())
.app_token(app_token.to_string())
.table_id(table_id.to_string())
.automatic_fields(true)
.fetch_all()
.await
}
#[cfg(feature = "ccm-core")]
pub async fn read_multiple_ranges(
&self,
spreadsheet_token: &str,
ranges: Vec<String>,
) -> SDKResult<crate::ccm::sheets_v2::v2::data_io::models::MultipleRangeData> {
use crate::ccm::sheets_v2::v2::data_io::{
read_multiple_ranges as read_multiple_ranges_api, ReadMultipleRangesParams,
};
let response = read_multiple_ranges_api(
self.config(),
spreadsheet_token,
ReadMultipleRangesParams {
ranges,
value_render_option: None,
date_render_option: None,
},
)
.await?;
response
.data
.ok_or_else(|| CoreError::api_data_error("读取多个范围"))
}
#[cfg(feature = "ccm-core")]
pub async fn write_multiple_ranges(
&self,
spreadsheet_token: &str,
data: Vec<crate::ccm::sheets_v2::v2::data_io::models::BatchWriteData>,
) -> SDKResult<crate::ccm::sheets_v2::v2::data_io::models::BatchUpdateResult> {
use crate::ccm::sheets_v2::v2::data_io::{batch_write_ranges, BatchWriteRangesParams};
let response = batch_write_ranges(
self.config(),
spreadsheet_token,
BatchWriteRangesParams {
data,
include_style: None,
},
)
.await?;
response
.data
.ok_or_else(|| CoreError::api_data_error("批量写入多个范围"))
}
#[cfg(feature = "ccm-core")]
pub async fn find_sheet_by_title(
&self,
spreadsheet_token: &str,
title: &str,
) -> SDKResult<crate::ccm::sheets_v2::v2::spreadsheet::models::SpreadsheetSheetInfo> {
let sheets = self.list_sheet_infos(spreadsheet_token).await?;
find_sheet_info(&sheets, title)
.ok_or_else(|| business_error(format!("未找到工作表: {title}")))
}
#[cfg(feature = "ccm-core")]
pub async fn list_sheet_infos(
&self,
spreadsheet_token: &str,
) -> SDKResult<Vec<crate::ccm::sheets_v2::v2::spreadsheet::models::SpreadsheetSheetInfo>> {
use crate::ccm::sheets::v3::spreadsheet::sheet::query::query_sheets;
use crate::ccm::sheets_v2::v2::spreadsheet::models::SpreadsheetSheetInfo;
log::info!(
"[OPENLARK DEBUG] list_sheet_infos called with token: {}",
spreadsheet_token
);
let response = query_sheets(self.config(), spreadsheet_token).await?;
log::info!(
"[OPENLARK DEBUG] query_sheets response count: {}",
response.sheets.len()
);
let sheets: Vec<SpreadsheetSheetInfo> =
response.sheets.into_iter().map(map_v3_sheet_info).collect();
if sheets.is_empty() {
return Err(CoreError::api_data_error("获取工作表列表"));
}
Ok(sheets)
}
}
#[cfg(feature = "ccm-core")]
fn map_v3_sheet_info(
sheet: crate::ccm::sheets::v3::spreadsheet::Sheet,
) -> crate::ccm::sheets_v2::v2::spreadsheet::models::SpreadsheetSheetInfo {
crate::ccm::sheets_v2::v2::spreadsheet::models::SpreadsheetSheetInfo {
sheet_id: sheet.sheet_id,
title: sheet.title,
sheet_type: sheet.resource_type,
row_count: sheet.grid_properties.row_count,
column_count: sheet.grid_properties.column_count,
}
}
#[cfg(feature = "ccm-core")]
fn find_sheet_info(
sheets: &[crate::ccm::sheets_v2::v2::spreadsheet::models::SpreadsheetSheetInfo],
title: &str,
) -> Option<crate::ccm::sheets_v2::v2::spreadsheet::models::SpreadsheetSheetInfo> {
sheets.iter().find(|sheet| sheet.title == title).cloned()
}
#[cfg(feature = "ccm-core")]
#[derive(Debug, Clone)]
pub struct CcmClient {
config: Arc<Config>,
}
#[cfg(feature = "ccm-core")]
impl CcmClient {
fn new(config: Arc<Config>) -> Self {
Self { config }
}
pub fn config(&self) -> &Config {
&self.config
}
}
#[cfg(any(feature = "base", feature = "bitable"))]
#[derive(Debug, Clone)]
pub struct BaseClient {
config: Arc<Config>,
}
#[cfg(any(feature = "base", feature = "bitable"))]
impl BaseClient {
fn new(config: Arc<Config>) -> Self {
Self { config }
}
pub fn config(&self) -> &Config {
&self.config
}
#[cfg(feature = "bitable")]
pub fn bitable(&self) -> BitableClient {
BitableClient::new(self.config.clone())
}
}
#[cfg(feature = "bitable")]
#[derive(Debug, Clone)]
pub struct BitableClient {
config: Arc<Config>,
}
#[cfg(feature = "bitable")]
impl BitableClient {
fn new(config: Arc<Config>) -> Self {
Self { config }
}
pub fn config(&self) -> &Config {
&self.config
}
}
#[cfg(any(feature = "baike", feature = "lingo"))]
#[derive(Debug, Clone)]
pub struct BaikeClient {
config: Arc<Config>,
}
#[cfg(any(feature = "baike", feature = "lingo"))]
impl BaikeClient {
fn new(config: Arc<Config>) -> Self {
Self { config }
}
pub fn config(&self) -> &Config {
&self.config
}
}
#[cfg(feature = "minutes")]
#[derive(Debug, Clone)]
pub struct MinutesClient {
config: Arc<Config>,
}
#[cfg(feature = "minutes")]
impl MinutesClient {
fn new(config: Arc<Config>) -> Self {
Self { config }
}
pub fn config(&self) -> &Config {
&self.config
}
}
#[cfg(test)]
mod tests {
use serde_json;
#[test]
fn test_serialization_roundtrip() {
let json = r#"{"test": "value"}"#;
assert!(serde_json::from_str::<serde_json::Value>(json).is_ok());
}
#[test]
fn test_deserialization_from_json() {
let json = r#"{"field": "data"}"#;
let value: serde_json::Value = serde_json::from_str(json).unwrap();
assert_eq!(value["field"], "data");
}
}