Skip to main content

vantage_api_client/
api.rs

1use indexmap::IndexMap;
2use vantage_core::error;
3use vantage_dataset::traits::Result;
4use vantage_types::Record;
5
6/// REST API backend for Vantage — reads data from HTTP JSON endpoints.
7///
8/// Each table maps to an API endpoint: `{base_url}/{table_name}`.
9/// The API returns paginated JSON: `{"data": [...], "pagination": {...}}`.
10/// Uses `serde_json::Value` as the native value type — no custom type system needed.
11///
12/// Currently read-only — write operations return errors.
13#[derive(Clone, Debug)]
14pub struct RestApi {
15    base_url: String,
16    client: reqwest::Client,
17    pub(crate) auth_header: Option<String>,
18}
19
20impl RestApi {
21    /// Create a new REST API data source pointing at `base_url`.
22    pub fn new(base_url: impl Into<String>) -> Self {
23        Self {
24            base_url: base_url.into(),
25            client: reqwest::Client::new(),
26            auth_header: None,
27        }
28    }
29
30    /// Set the Authorization header value (e.g. "Bearer `<token>`").
31    pub fn with_auth(mut self, auth: impl Into<String>) -> Self {
32        self.auth_header = Some(auth.into());
33        self
34    }
35
36    /// Build the endpoint URL for a given table name.
37    fn endpoint_url(&self, table_name: &str) -> String {
38        format!("{}/{}", self.base_url, table_name)
39    }
40
41    /// Fetch data from the API endpoint and return parsed records.
42    ///
43    /// The `id_field` parameter determines which JSON field to use as the record ID.
44    /// If None, row indices are used.
45    pub(crate) async fn fetch_records(
46        &self,
47        table_name: &str,
48        id_field: Option<&str>,
49    ) -> Result<IndexMap<String, Record<serde_json::Value>>> {
50        let url = self.endpoint_url(table_name);
51
52        let mut request = self.client.get(&url);
53        if let Some(ref auth) = self.auth_header {
54            request = request.header("Authorization", auth);
55        }
56
57        let response = request
58            .send()
59            .await
60            .map_err(|e| error!("API request failed", url = url, detail = e))?;
61
62        if !response.status().is_success() {
63            return Err(error!(
64                "API returned error status",
65                url = url,
66                status = response.status().as_u16()
67            ));
68        }
69
70        let body: serde_json::Value = response
71            .json()
72            .await
73            .map_err(|e| error!("Failed to parse API response as JSON", detail = e))?;
74
75        let data = body["data"]
76            .as_array()
77            .ok_or_else(|| error!("API response missing 'data' array", url = url))?;
78
79        let mut records = IndexMap::new();
80
81        for (row_idx, item) in data.iter().enumerate() {
82            let obj = item
83                .as_object()
84                .ok_or_else(|| error!("API data item is not an object", index = row_idx))?;
85
86            // Extract ID from the configured id_field, or use row index
87            let id = id_field
88                .and_then(|field| obj.get(field))
89                .and_then(|v| match v {
90                    serde_json::Value::String(s) => Some(s.clone()),
91                    serde_json::Value::Number(n) => Some(n.to_string()),
92                    _ => None,
93                })
94                .unwrap_or_else(|| row_idx.to_string());
95
96            let record: Record<serde_json::Value> =
97                obj.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
98
99            records.insert(id, record);
100        }
101
102        Ok(records)
103    }
104}