synaptic_lark/loaders/
bitable.rs1use crate::{api::bitable::BitableApi, LarkConfig};
2use async_trait::async_trait;
3use serde_json::Value;
4use std::collections::HashMap;
5use synaptic_core::{Document, Loader, SynapticError};
6
7pub struct LarkBitableLoader {
35 api: BitableApi,
36 app_token: Option<String>,
37 table_id: Option<String>,
38 view_id: Option<String>,
39 content_field: Option<String>,
42}
43
44impl LarkBitableLoader {
45 pub fn new(config: LarkConfig) -> Self {
47 Self {
48 api: BitableApi::new(config),
49 app_token: None,
50 table_id: None,
51 view_id: None,
52 content_field: None,
53 }
54 }
55
56 pub fn with_app(mut self, app_token: impl Into<String>) -> Self {
58 self.app_token = Some(app_token.into());
59 self
60 }
61
62 pub fn with_table(mut self, table_id: impl Into<String>) -> Self {
64 self.table_id = Some(table_id.into());
65 self
66 }
67
68 pub fn with_view(mut self, view_id: impl Into<String>) -> Self {
70 self.view_id = Some(view_id.into());
71 self
72 }
73
74 pub fn with_content_field(mut self, field: impl Into<String>) -> Self {
78 self.content_field = Some(field.into());
79 self
80 }
81
82 pub fn app_token(&self) -> &str {
86 self.app_token.as_deref().unwrap_or("")
87 }
88
89 pub fn table_id(&self) -> &str {
91 self.table_id.as_deref().unwrap_or("")
92 }
93
94 pub fn view_id(&self) -> Option<&str> {
96 self.view_id.as_deref()
97 }
98
99 pub fn content_field(&self) -> Option<&str> {
101 self.content_field.as_deref()
102 }
103
104 fn record_to_document(&self, record: &Value) -> Document {
108 let record_id = record["record_id"].as_str().unwrap_or("").to_string();
109 let fields = record["fields"].as_object();
110
111 let mut metadata: HashMap<String, Value> = HashMap::new();
112 metadata.insert("record_id".to_string(), Value::String(record_id.clone()));
113 metadata.insert(
114 "source".to_string(),
115 Value::String("lark_bitable".to_string()),
116 );
117
118 let mut content = String::new();
119
120 if let Some(fields_map) = fields {
121 for (k, v) in fields_map {
122 if let Some(ref cf) = self.content_field {
123 if k == cf {
124 content = value_to_text(v);
125 } else {
126 metadata.insert(k.clone(), v.clone());
127 }
128 } else {
129 if content.is_empty() {
131 if let Some(s) = v.as_str() {
132 content = s.to_string();
133 } else if v.is_array() || v.is_object() {
134 metadata.insert(k.clone(), v.clone());
135 } else {
136 content = v.to_string();
137 }
138 } else {
139 metadata.insert(k.clone(), v.clone());
140 }
141 }
142 }
143 }
144
145 Document {
146 id: record_id,
147 content,
148 metadata,
149 }
150 }
151}
152
153fn value_to_text(v: &Value) -> String {
158 match v {
159 Value::String(s) => s.clone(),
160 Value::Array(arr) => arr
161 .iter()
162 .filter_map(|item| item["text"].as_str())
163 .collect::<Vec<_>>()
164 .join(""),
165 _ => v.to_string(),
166 }
167}
168
169#[async_trait]
170impl Loader for LarkBitableLoader {
171 async fn load(&self) -> Result<Vec<Document>, SynapticError> {
172 let app_token = self.app_token.as_deref().ok_or_else(|| {
173 SynapticError::Config("LarkBitableLoader: app_token not set".to_string())
174 })?;
175 let table_id = self.table_id.as_deref().ok_or_else(|| {
176 SynapticError::Config("LarkBitableLoader: table_id not set".to_string())
177 })?;
178
179 let mut docs = Vec::new();
180 let mut page_token: Option<String> = None;
181
182 loop {
183 let (items, next) = self
184 .api
185 .list_records_page(
186 app_token,
187 table_id,
188 self.view_id.as_deref(),
189 page_token.as_deref(),
190 )
191 .await?;
192 for record in &items {
193 docs.push(self.record_to_document(record));
194 }
195 match next {
196 Some(pt) => page_token = Some(pt),
197 None => break,
198 }
199 }
200
201 Ok(docs)
202 }
203}