use indexmap::IndexMap;
use serde_json::Value;
use vantage_core::error;
use vantage_dataset::traits::Result;
use vantage_expressions::Expression;
use vantage_table::pagination::Pagination;
use vantage_types::Record;
#[derive(Clone, Debug)]
pub enum ResponseShape {
BareArray,
Wrapped { array_key: String },
WrappedByTableName,
}
impl Default for ResponseShape {
fn default() -> Self {
ResponseShape::Wrapped {
array_key: "data".to_string(),
}
}
}
#[derive(Clone, Debug)]
pub struct PaginationParams {
pub page: String,
pub limit: String,
pub skip_based: bool,
}
impl PaginationParams {
pub fn page_limit(page: impl Into<String>, limit: impl Into<String>) -> Self {
Self {
page: page.into(),
limit: limit.into(),
skip_based: false,
}
}
pub fn skip_limit(skip: impl Into<String>, limit: impl Into<String>) -> Self {
Self {
page: skip.into(),
limit: limit.into(),
skip_based: true,
}
}
}
impl Default for PaginationParams {
fn default() -> Self {
Self::page_limit("_page", "_limit")
}
}
#[derive(Clone, Debug)]
pub struct RestApi {
base_url: String,
client: reqwest::Client,
pub(crate) auth_header: Option<String>,
response_shape: ResponseShape,
pagination: PaginationParams,
no_pagination: bool,
}
impl RestApi {
pub fn new(base_url: impl Into<String>) -> Self {
RestApi::builder(base_url).build()
}
pub fn builder(base_url: impl Into<String>) -> RestApiBuilder {
RestApiBuilder::new(base_url.into())
}
pub fn with_auth(mut self, auth: impl Into<String>) -> Self {
self.auth_header = Some(auth.into());
self
}
fn endpoint_url(&self, table_name: &str) -> String {
format!("{}/{}", self.base_url, table_name)
}
fn build_query_string<'a>(
&self,
pagination: Option<&Pagination>,
conditions: impl IntoIterator<Item = &'a Expression<Value>>,
) -> String {
let mut params: Vec<(String, String)> = Vec::new();
if !self.no_pagination {
if let Some(p) = pagination {
let page_value = if self.pagination.skip_based {
p.skip().to_string()
} else {
p.get_page().to_string()
};
params.push((self.pagination.page.clone(), page_value));
params.push((self.pagination.limit.clone(), p.limit().to_string()));
}
}
for cond in conditions {
if let Some((field, value)) = crate::condition_to_query_param(cond) {
params.push((field, value));
}
}
if params.is_empty() {
return String::new();
}
let mut s = String::from("?");
for (i, (k, v)) in params.iter().enumerate() {
if i > 0 {
s.push('&');
}
s.push_str(&urlencode(k));
s.push('=');
s.push_str(&urlencode(v));
}
s
}
pub(crate) async fn fetch_records<'a>(
&self,
table_name: &str,
id_field: Option<&str>,
pagination: Option<&Pagination>,
conditions: impl IntoIterator<Item = &'a Expression<Value>>,
) -> Result<IndexMap<String, Record<serde_json::Value>>> {
if self.no_pagination {
if let Some(p) = pagination {
if p.get_page() > 1 {
return Ok(IndexMap::new());
}
}
}
let url = format!(
"{}{}",
self.endpoint_url(table_name),
self.build_query_string(pagination, conditions)
);
let mut request = self.client.get(&url);
if let Some(ref auth) = self.auth_header {
request = request.header("Authorization", auth);
}
let response = request
.send()
.await
.map_err(|e| error!("API request failed", url = url, detail = e))?;
if !response.status().is_success() {
return Err(error!(
"API returned error status",
url = url,
status = response.status().as_u16()
));
}
let body: serde_json::Value = response
.json()
.await
.map_err(|e| error!("Failed to parse API response as JSON", detail = e))?;
let data = self.extract_array(&body, table_name)?;
let mut records = IndexMap::new();
for (row_idx, item) in data.iter().enumerate() {
let obj = item
.as_object()
.ok_or_else(|| error!("API data item is not an object", index = row_idx))?;
let id = id_field
.and_then(|field| obj.get(field))
.and_then(|v| match v {
serde_json::Value::String(s) => Some(s.clone()),
serde_json::Value::Number(n) => Some(n.to_string()),
_ => None,
})
.unwrap_or_else(|| row_idx.to_string());
let record: Record<serde_json::Value> =
obj.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
records.insert(id, record);
}
Ok(records)
}
}
fn urlencode(s: &str) -> String {
urlencoding::encode(s).into_owned()
}
impl RestApi {
fn extract_array<'a>(
&self,
body: &'a serde_json::Value,
table_name: &str,
) -> Result<&'a Vec<serde_json::Value>> {
match &self.response_shape {
ResponseShape::BareArray => body.as_array().ok_or_else(|| {
error!("Expected response body to be a JSON array (BareArray shape)")
}),
ResponseShape::Wrapped { array_key } => body[array_key].as_array().ok_or_else(|| {
error!(
"Response missing array under wrapper key",
array_key = array_key
)
}),
ResponseShape::WrappedByTableName => body[table_name].as_array().ok_or_else(|| {
error!(
"Response missing array under table-name key",
table_name = table_name
)
}),
}
}
}
#[derive(Clone, Debug)]
pub struct RestApiBuilder {
base_url: String,
auth_header: Option<String>,
response_shape: ResponseShape,
pagination: PaginationParams,
no_pagination: bool,
}
impl RestApiBuilder {
fn new(base_url: String) -> Self {
Self {
base_url,
auth_header: None,
response_shape: ResponseShape::default(),
pagination: PaginationParams::default(),
no_pagination: false,
}
}
pub fn auth(mut self, auth: impl Into<String>) -> Self {
self.auth_header = Some(auth.into());
self
}
pub fn response_shape(mut self, shape: ResponseShape) -> Self {
self.response_shape = shape;
self
}
pub fn pagination_params(mut self, pagination: PaginationParams) -> Self {
self.pagination = pagination;
self
}
pub fn no_pagination(mut self) -> Self {
self.no_pagination = true;
self
}
pub fn build(self) -> RestApi {
RestApi {
base_url: self.base_url,
client: reqwest::Client::new(),
auth_header: self.auth_header,
response_shape: self.response_shape,
pagination: self.pagination,
no_pagination: self.no_pagination,
}
}
}