larkrs_client/bitable/
table.rs1#![allow(dead_code)]
2
3use crate::LarkApiResponse;
4use crate::auth::FeishuTokenManager;
5use crate::bitable::{FieldsListResponse, SearchRecordsResponse};
6use anyhow::{Result, anyhow};
7use reqwest::Client;
8use serde_json::Value;
9use thiserror::Error;
10
11use super::BatchCreateRecordsRequest;
12
13#[derive(Error, Debug)]
14pub enum BitableApiError {
15 #[error("Network error: {0}")]
16 NetworkError(#[from] reqwest::Error),
17
18 #[error("JSON serialization error: {0}")]
19 SerdeError(#[from] serde_json::Error),
20
21 #[error("API error: {message} (code: {code})")]
22 ApiError { code: i32, message: String },
23}
24
25pub struct BitableTableClient {
26 token_manager: FeishuTokenManager,
27}
28
29impl BitableTableClient {
30 pub fn new() -> Self {
31 Self {
32 token_manager: FeishuTokenManager::new(),
33 }
34 }
35
36 pub async fn get_records_list(
40 &self,
41 app_token: &str,
42 table_id: &str,
43 request: super::SearchRecordsCond,
44 ) -> Result<SearchRecordsResponse> {
45 let url = format!(
46 "https://open.feishu.cn/open-apis/bitable/v1/apps/{}/tables/{}/records/search",
47 app_token, table_id
48 );
49
50 let token = self.token_manager.get_token().await?;
51 let response = Client::new()
52 .post(&url)
53 .header("Authorization", format!("Bearer {}", token))
54 .header("Content-Type", "application/json; charset=utf-8")
55 .json(&request)
56 .send()
57 .await
58 .map_err(|e| anyhow!(e).context("Failed to send request for searching records"))?
59 .json::<LarkApiResponse<SearchRecordsResponse>>()
60 .await
61 .map_err(|e| anyhow!(e).context("Failed to parse search records response"))?;
62
63 if !response.is_success() {
64 let err = BitableApiError::ApiError {
65 code: response.code,
66 message: response.msg.clone(),
67 };
68 return Err(anyhow!(err).context(format!("API returned error code: {}", response.code)));
69 }
70
71 Ok(response.data)
72 }
73
74 pub async fn batch_create_records(
78 &self,
79 app_token: &str,
80 table_id: &str,
81 request: BatchCreateRecordsRequest,
82 ) -> Result<()> {
83 if app_token.is_empty() || table_id.is_empty() {
84 return Err(anyhow!("app_token and table_id cannot be empty"));
85 }
86 if request.records.is_empty() {
87 return Err(anyhow!("No records provided for batch creation"));
88 }
89
90 let url = format!(
91 "https://open.feishu.cn/open-apis/bitable/v1/apps/{}/tables/{}/records/batch_create",
92 app_token, table_id
93 );
94 let token = self
95 .token_manager
96 .get_token()
97 .await
98 .map_err(|e| anyhow!(e).context("Failed to obtain authentication token"))?;
99
100 let resp = Client::new()
101 .post(&url)
102 .header("Authorization", format!("Bearer {}", token))
103 .header("Content-Type", "application/json; charset=utf-8")
104 .json(&request)
105 .send()
106 .await
107 .map_err(|e| anyhow!(e).context("Failed to send request for batch creating records"))?
108 .json::<LarkApiResponse<Value>>()
109 .await
110 .map_err(|e| anyhow!(e).context("Failed to parse batch create records response"))?;
111
112 match resp.is_success() {
113 true => Ok(()),
114 false => Err(anyhow!(BitableApiError::ApiError {
115 code: resp.code,
116 message: resp.msg.clone(),
117 })
118 .context(format!(
119 "API returned error code: {} - {}",
120 resp.code, resp.msg
121 ))),
122 }
123 }
124
125 pub async fn batch_create_records_json(
126 &self,
127 app_token: &str,
128 table_id: &str,
129 records_json: &str,
130 ) -> Result<()> {
131 if app_token.is_empty() || table_id.is_empty() {
132 return Err(anyhow!("app_token and table_id cannot be empty"));
133 }
134
135 let value: Value = serde_json::from_str(records_json)
137 .map_err(|e| anyhow!("Failed to parse JSON string: {}", e))?;
138
139 let request = BatchCreateRecordsRequest::from(value);
141
142 if request.records.is_empty() {
143 return Err(anyhow!("No valid records found in the provided JSON"));
144 }
145
146 self.batch_create_records(app_token, table_id, request)
147 .await
148 }
149
150 pub async fn get_fields_list(
151 &self,
152 app_token: &str,
153 table_id: &str,
154 ) -> Result<FieldsListResponse> {
155 if app_token.is_empty() || table_id.is_empty() {
156 return Err(anyhow!("app_token and table_id cannot be empty"));
157 }
158
159 let token = self.token_manager.get_token().await?;
160
161 let url = format!(
162 "https://open.feishu.cn/open-apis/bitable/v1/apps/{}/tables/{}/fields",
163 app_token, table_id
164 );
165
166 let response = Client::new()
167 .get(&url)
168 .header("Authorization", format!("Bearer {}", token))
169 .header("Content-Type", "application/json; charset=utf-8")
170 .send()
171 .await
172 .map_err(|e| anyhow!(e).context("Failed to send request for getting fields list"))?
173 .json::<LarkApiResponse<FieldsListResponse>>()
174 .await
175 .map_err(|e| anyhow!(e).context("Failed to parse fields list response"))?;
176
177 match response.is_success() {
178 true => Ok(response.data),
179 false => Err(anyhow!(BitableApiError::ApiError {
180 code: response.code,
181 message: response.msg.clone(),
182 })
183 .context(format!(
184 "API returned error code: {} - {}",
185 response.code, response.msg
186 ))),
187 }
188 }
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194 use crate::bitable::{
195 Filter, FilterCondition, FilterConjunction, FilterOperator, SearchRecordsCond,
196 };
197
198 #[tokio::test]
199 async fn test_get_records_list() {
200 dotenvy::dotenv().ok();
201
202 let client = BitableTableClient::new();
203
204 let app_token = "xxx";
205 let table_id = "xxx";
206 let view_id = "xxx";
207 let result = client
208 .get_records_list(
209 &app_token,
210 &table_id,
211 SearchRecordsCond {
212 view_id: view_id.to_string(),
213 filter: Some(Filter {
214 conditions: vec![FilterCondition {
215 field_name: "战法".to_string(),
216 operator: FilterOperator::Is,
217 value: vec!["战法A".to_string()],
218 }],
219 conjunction: FilterConjunction::And,
220 }),
221 ..Default::default()
222 },
223 )
224 .await;
225 assert!(result.is_ok());
226
227 println!("Result: {:#?}", result.unwrap());
228 }
229
230 #[tokio::test]
231 async fn test_batch_create_records() {
232 dotenvy::dotenv().ok();
233
234 let client = BitableTableClient::new();
235
236 let app_token = "xxxx";
237 let table_id = "xxxx";
238
239 let records_json = r#"[
240 {
241 "股票名称": "xxxx",
242 "题材概念": "xxxx",
243 "日期": 1743129600000,
244 "梯队": ["xxxx"]
245 }
246 ]"#;
247 let result = client
248 .batch_create_records_json(app_token, table_id, records_json)
249 .await;
250 assert!(result.is_ok());
251
252 println!("Result: {:#?}", result.unwrap());
253 }
254
255 #[tokio::test]
256 async fn test_get_fields_list() {
257 dotenvy::dotenv().ok();
258
259 let client = BitableTableClient::new();
260
261 let app_token = "xxxx";
262 let table_id = "xxxx";
263
264 let result = client.get_fields_list(app_token, table_id).await;
265 assert!(result.is_ok());
266
267 let fields: Vec<crate::bitable::FieldInfo> = result.unwrap().into();
268 println!("Simplified Field Infos: {:#?}", fields);
269 }
270}