1use indexmap::IndexMap;
2use serde_json::Value;
3use vantage_core::error;
4use vantage_dataset::traits::Result;
5use vantage_expressions::Expression;
6use vantage_table::pagination::Pagination;
7use vantage_types::Record;
8
9#[derive(Clone, Debug)]
14pub enum ResponseShape {
15 BareArray,
18
19 Wrapped { array_key: String },
22
23 WrappedByTableName,
27}
28
29impl Default for ResponseShape {
30 fn default() -> Self {
32 ResponseShape::Wrapped {
33 array_key: "data".to_string(),
34 }
35 }
36}
37
38#[derive(Clone, Debug)]
44pub struct PaginationParams {
45 pub page: String,
46 pub limit: String,
47 pub skip_based: bool,
50}
51
52impl PaginationParams {
53 pub fn page_limit(page: impl Into<String>, limit: impl Into<String>) -> Self {
54 Self {
55 page: page.into(),
56 limit: limit.into(),
57 skip_based: false,
58 }
59 }
60
61 pub fn skip_limit(skip: impl Into<String>, limit: impl Into<String>) -> Self {
62 Self {
63 page: skip.into(),
64 limit: limit.into(),
65 skip_based: true,
66 }
67 }
68}
69
70impl Default for PaginationParams {
71 fn default() -> Self {
72 Self::page_limit("_page", "_limit")
73 }
74}
75
76#[derive(Clone, Debug)]
84pub struct RestApi {
85 base_url: String,
86 client: reqwest::Client,
87 pub(crate) auth_header: Option<String>,
88 response_shape: ResponseShape,
89 pagination: PaginationParams,
90}
91
92impl RestApi {
93 pub fn new(base_url: impl Into<String>) -> Self {
97 RestApi::builder(base_url).build()
98 }
99
100 pub fn builder(base_url: impl Into<String>) -> RestApiBuilder {
102 RestApiBuilder::new(base_url.into())
103 }
104
105 pub fn with_auth(mut self, auth: impl Into<String>) -> Self {
109 self.auth_header = Some(auth.into());
110 self
111 }
112
113 fn endpoint_url(&self, table_name: &str) -> String {
115 format!("{}/{}", self.base_url, table_name)
116 }
117
118 fn build_query_string<'a>(
124 &self,
125 pagination: Option<&Pagination>,
126 conditions: impl IntoIterator<Item = &'a Expression<Value>>,
127 ) -> String {
128 let mut params: Vec<(String, String)> = Vec::new();
129
130 if let Some(p) = pagination {
132 let page_value = if self.pagination.skip_based {
133 p.skip().to_string()
134 } else {
135 p.get_page().to_string()
136 };
137 params.push((self.pagination.page.clone(), page_value));
138 params.push((self.pagination.limit.clone(), p.limit().to_string()));
139 }
140
141 for cond in conditions {
144 if let Some((field, value)) = crate::condition_to_query_param(cond) {
145 params.push((field, value));
146 }
147 }
148
149 if params.is_empty() {
150 return String::new();
151 }
152 let mut s = String::from("?");
153 for (i, (k, v)) in params.iter().enumerate() {
154 if i > 0 {
155 s.push('&');
156 }
157 s.push_str(&urlencode(k));
161 s.push('=');
162 s.push_str(&urlencode(v));
163 }
164 s
165 }
166
167 pub(crate) async fn fetch_records<'a>(
176 &self,
177 table_name: &str,
178 id_field: Option<&str>,
179 pagination: Option<&Pagination>,
180 conditions: impl IntoIterator<Item = &'a Expression<Value>>,
181 ) -> Result<IndexMap<String, Record<serde_json::Value>>> {
182 let url = format!(
183 "{}{}",
184 self.endpoint_url(table_name),
185 self.build_query_string(pagination, conditions)
186 );
187
188 let mut request = self.client.get(&url);
189 if let Some(ref auth) = self.auth_header {
190 request = request.header("Authorization", auth);
191 }
192
193 let response = request
194 .send()
195 .await
196 .map_err(|e| error!("API request failed", url = url, detail = e))?;
197
198 if !response.status().is_success() {
199 return Err(error!(
200 "API returned error status",
201 url = url,
202 status = response.status().as_u16()
203 ));
204 }
205
206 let body: serde_json::Value = response
207 .json()
208 .await
209 .map_err(|e| error!("Failed to parse API response as JSON", detail = e))?;
210
211 let data = self.extract_array(&body, table_name)?;
212
213 let mut records = IndexMap::new();
214 for (row_idx, item) in data.iter().enumerate() {
215 let obj = item
216 .as_object()
217 .ok_or_else(|| error!("API data item is not an object", index = row_idx))?;
218
219 let id = id_field
221 .and_then(|field| obj.get(field))
222 .and_then(|v| match v {
223 serde_json::Value::String(s) => Some(s.clone()),
224 serde_json::Value::Number(n) => Some(n.to_string()),
225 _ => None,
226 })
227 .unwrap_or_else(|| row_idx.to_string());
228
229 let record: Record<serde_json::Value> =
230 obj.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
231
232 records.insert(id, record);
233 }
234
235 Ok(records)
236 }
237}
238
239fn urlencode(s: &str) -> String {
240 urlencoding::encode(s).into_owned()
241}
242
243impl RestApi {
244 fn extract_array<'a>(
247 &self,
248 body: &'a serde_json::Value,
249 table_name: &str,
250 ) -> Result<&'a Vec<serde_json::Value>> {
251 match &self.response_shape {
252 ResponseShape::BareArray => body.as_array().ok_or_else(|| {
253 error!("Expected response body to be a JSON array (BareArray shape)")
254 }),
255 ResponseShape::Wrapped { array_key } => body[array_key].as_array().ok_or_else(|| {
256 error!(
257 "Response missing array under wrapper key",
258 array_key = array_key
259 )
260 }),
261 ResponseShape::WrappedByTableName => body[table_name].as_array().ok_or_else(|| {
262 error!(
263 "Response missing array under table-name key",
264 table_name = table_name
265 )
266 }),
267 }
268 }
269}
270
271#[derive(Clone, Debug)]
289pub struct RestApiBuilder {
290 base_url: String,
291 auth_header: Option<String>,
292 response_shape: ResponseShape,
293 pagination: PaginationParams,
294}
295
296impl RestApiBuilder {
297 fn new(base_url: String) -> Self {
298 Self {
299 base_url,
300 auth_header: None,
301 response_shape: ResponseShape::default(),
302 pagination: PaginationParams::default(),
303 }
304 }
305
306 pub fn auth(mut self, auth: impl Into<String>) -> Self {
308 self.auth_header = Some(auth.into());
309 self
310 }
311
312 pub fn response_shape(mut self, shape: ResponseShape) -> Self {
315 self.response_shape = shape;
316 self
317 }
318
319 pub fn pagination_params(mut self, pagination: PaginationParams) -> Self {
322 self.pagination = pagination;
323 self
324 }
325
326 pub fn build(self) -> RestApi {
327 RestApi {
328 base_url: self.base_url,
329 client: reqwest::Client::new(),
330 auth_header: self.auth_header,
331 response_shape: self.response_shape,
332 pagination: self.pagination,
333 }
334 }
335}