use crate::errors::WOWSQLError;
use crate::models::TableSchema;
use crate::table::Table;
use reqwest::Client;
use serde_json::Value;
use std::time::Duration;
pub struct WOWSQLClient {
pub(crate) base_url: String,
api_key: String,
client: Client,
}
impl WOWSQLClient {
pub fn new(project_url: &str, api_key: &str) -> Result<Self, WOWSQLError> {
let mut url = project_url.trim().to_string();
if url.ends_with('/') {
url.pop();
}
let client = Client::builder()
.timeout(Duration::from_secs(30))
.build()
.map_err(|e| WOWSQLError::Network(format!("Failed to create HTTP client: {}", e)))?;
Ok(Self {
base_url: url,
api_key: api_key.to_string(),
client,
})
}
pub fn table(&self, table_name: &str) -> Table<'_> {
Table::new(self, table_name)
}
pub async fn list_tables(&self) -> Result<Vec<String>, WOWSQLError> {
let url = format!("{}/api/v2/tables", self.base_url);
let response: Value = self.execute_request(&url, "GET", None).await?;
if let Some(tables) = response.get("tables") {
if let Some(tables_array) = tables.as_array() {
return Ok(tables_array
.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect());
}
}
Ok(vec![])
}
pub async fn get_table_schema(&self, table_name: &str) -> Result<TableSchema, WOWSQLError> {
let url = format!("{}/api/v2/tables/{}/schema", self.base_url, table_name);
self.execute_request(&url, "GET", None).await
}
pub async fn query<T: serde::de::DeserializeOwned>(
&self,
sql: &str,
) -> Result<Vec<T>, WOWSQLError> {
let url = format!("{}/api/v2/query", self.base_url);
let body = serde_json::json!({ "sql": sql });
let response: Value = self.execute_request(&url, "POST", Some(body)).await?;
if let Some(data) = response.get("data") {
if let Some(data_array) = data.as_array() {
let mut results = Vec::new();
for item in data_array {
if let Ok(parsed) = serde_json::from_value(item.clone()) {
results.push(parsed);
}
}
return Ok(results);
}
}
Ok(vec![])
}
pub async fn health(&self) -> Result<Value, WOWSQLError> {
let url = format!("{}/api/v2/health", self.base_url);
self.execute_request(&url, "GET", None).await
}
pub(crate) async fn execute_request<T: serde::de::DeserializeOwned>(
&self,
url: &str,
method: &str,
body: Option<Value>,
) -> Result<T, WOWSQLError> {
let mut request = self
.client
.request(
method
.parse()
.map_err(|_| WOWSQLError::General("Invalid method".to_string()))?,
url,
)
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.header("Authorization", format!("Bearer {}", self.api_key));
if let Some(body) = body {
request = request.json(&body);
}
let response = request
.send()
.await
.map_err(|e| WOWSQLError::Network(format!("Request failed: {}", e)))?;
let status = response.status();
let text = response
.text()
.await
.map_err(|e| WOWSQLError::Network(format!("Failed to read response: {}", e)))?;
if !status.is_success() {
return Err(self.handle_error(status.as_u16(), &text));
}
serde_json::from_str(&text)
.map_err(|e| WOWSQLError::General(format!("Failed to parse response: {}", e)))
}
fn handle_error(&self, status_code: u16, body: &str) -> WOWSQLError {
let error_response: Value = serde_json::from_str(body).unwrap_or(Value::Null);
let message = error_response
.get("error")
.and_then(|v| v.as_str())
.or_else(|| error_response.get("message").and_then(|v| v.as_str()))
.or_else(|| error_response.get("detail").and_then(|v| v.as_str()))
.unwrap_or(&format!("Request failed with status {}", status_code))
.to_string();
match status_code {
401 | 403 => WOWSQLError::Authentication(message),
404 => WOWSQLError::NotFound(message),
429 => WOWSQLError::RateLimit(message),
_ => WOWSQLError::General(message),
}
}
}