wowsql 1.1.0

Official Rust SDK for WOWSQL - MySQL Backend-as-a-Service with S3 Storage
Documentation
use crate::errors::WOWSQLError;
use crate::models::TableSchema;
use crate::table::Table;
use reqwest::Client;
use serde_json::Value;
use std::time::Duration;

/// Main client for interacting with WOWSQL API
pub struct WOWSQLClient {
    pub(crate) base_url: String,
    api_key: String,
    client: Client,
}

impl WOWSQLClient {
    /// Create a new WOWSQL client
    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,
        })
    }

    /// Get a table interface for operations
    pub fn table(&self, table_name: &str) -> Table<'_> {
        Table::new(self, table_name)
    }

    /// List all tables in the database
    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![])
    }

    /// Get table schema information
    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
    }

    /// Execute a raw SQL query (read-only)
    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![])
    }

    /// Check API health
    pub async fn health(&self) -> Result<Value, WOWSQLError> {
        let url = format!("{}/api/v2/health", self.base_url);
        self.execute_request(&url, "GET", None).await
    }

    /// Execute a request and handle response
    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)))
    }

    /// Handle HTTP errors
    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),
        }
    }
}