Skip to main content

wowsql/
client.rs

1use crate::errors::WOWSQLError;
2use crate::models::TableSchema;
3use crate::table::Table;
4use reqwest::Client;
5use serde_json::Value;
6use std::time::Duration;
7
8/// Builder for configuring a WOWSQLClient
9pub struct WOWSQLClientBuilder {
10    project_url: String,
11    api_key: String,
12    base_domain: String,
13    secure: bool,
14    timeout: u64,
15    verify_ssl: bool,
16}
17
18impl WOWSQLClientBuilder {
19    fn new(project_url: &str, api_key: &str) -> Self {
20        Self {
21            project_url: project_url.to_string(),
22            api_key: api_key.to_string(),
23            base_domain: "wowsql.com".to_string(),
24            secure: true,
25            timeout: 30,
26            verify_ssl: true,
27        }
28    }
29
30    pub fn base_domain(mut self, domain: &str) -> Self {
31        self.base_domain = domain.to_string();
32        self
33    }
34
35    pub fn secure(mut self, secure: bool) -> Self {
36        self.secure = secure;
37        self
38    }
39
40    pub fn timeout(mut self, seconds: u64) -> Self {
41        self.timeout = seconds;
42        self
43    }
44
45    pub fn verify_ssl(mut self, verify: bool) -> Self {
46        self.verify_ssl = verify;
47        self
48    }
49
50    pub fn build(self) -> Result<WOWSQLClient, WOWSQLError> {
51        let base_url =
52            WOWSQLClient::build_base_url(&self.project_url, &self.base_domain, self.secure);
53
54        let client = Client::builder()
55            .timeout(Duration::from_secs(self.timeout))
56            .danger_accept_invalid_certs(!self.verify_ssl)
57            .build()
58            .map_err(|e| {
59                WOWSQLError::new(&format!("Failed to create HTTP client: {}", e), None, None)
60            })?;
61
62        Ok(WOWSQLClient {
63            base_url,
64            api_key: self.api_key,
65            base_domain: self.base_domain,
66            secure: self.secure,
67            client,
68        })
69    }
70}
71
72/// Main client for interacting with WOWSQL API
73pub struct WOWSQLClient {
74    pub(crate) base_url: String,
75    pub(crate) api_key: String,
76    #[allow(dead_code)]
77    pub(crate) base_domain: String,
78    #[allow(dead_code)]
79    pub(crate) secure: bool,
80    pub(crate) client: Client,
81}
82
83impl WOWSQLClient {
84    /// Create a new WOWSQL client with default settings
85    pub fn new(project_url: &str, api_key: &str) -> Result<Self, WOWSQLError> {
86        WOWSQLClientBuilder::new(project_url, api_key).build()
87    }
88
89    /// Create a builder for advanced configuration
90    pub fn builder(project_url: &str, api_key: &str) -> WOWSQLClientBuilder {
91        WOWSQLClientBuilder::new(project_url, api_key)
92    }
93
94    /// Build the base URL from project_url, handling full URLs, slugs, and domains
95    pub(crate) fn build_base_url(project_url: &str, base_domain: &str, secure: bool) -> String {
96        let mut url = project_url.trim().to_string();
97
98        if url.ends_with('/') {
99            url.pop();
100        }
101
102        if url.starts_with("http://") || url.starts_with("https://") {
103            url = url.trim_end_matches('/').to_string();
104            if url.ends_with("/api") {
105                url = url.trim_end_matches("/api").to_string();
106            }
107            return url;
108        }
109
110        let protocol = if secure { "https" } else { "http" };
111
112        if url.contains(&format!(".{}", base_domain)) || url.ends_with(base_domain) {
113            url = format!("{}://{}", protocol, url);
114        } else {
115            url = format!("{}://{}.{}", protocol, url, base_domain);
116        }
117
118        url = url.trim_end_matches('/').to_string();
119        if url.ends_with("/api") {
120            url = url.trim_end_matches("/api").to_string();
121        }
122
123        url
124    }
125
126    /// Get a table interface for operations
127    pub fn table(&self, table_name: &str) -> Table<'_> {
128        Table::new(self, table_name)
129    }
130
131    /// List all tables in the database
132    pub async fn list_tables(&self) -> Result<Vec<String>, WOWSQLError> {
133        let url = format!("{}/api/v2/tables", self.base_url);
134        let response: Value = self.execute_request(&url, "GET", None).await?;
135
136        if let Some(tables) = response.get("tables") {
137            if let Some(tables_array) = tables.as_array() {
138                return Ok(tables_array
139                    .iter()
140                    .filter_map(|v| v.as_str().map(|s| s.to_string()))
141                    .collect());
142            }
143        }
144
145        Ok(vec![])
146    }
147
148    /// Get table schema information
149    pub async fn get_table_schema(&self, table_name: &str) -> Result<TableSchema, WOWSQLError> {
150        let url = format!("{}/api/v2/tables/{}/schema", self.base_url, table_name);
151        self.execute_request(&url, "GET", None).await
152    }
153
154    /// Execute a raw SQL query (read-only)
155    pub async fn query<T: serde::de::DeserializeOwned>(
156        &self,
157        sql: &str,
158    ) -> Result<Vec<T>, WOWSQLError> {
159        let url = format!("{}/api/v2/query", self.base_url);
160        let body = serde_json::json!({ "sql": sql });
161        let response: Value = self.execute_request(&url, "POST", Some(body)).await?;
162
163        if let Some(data) = response.get("data") {
164            if let Some(data_array) = data.as_array() {
165                let mut results = Vec::new();
166                for item in data_array {
167                    if let Ok(parsed) = serde_json::from_value(item.clone()) {
168                        results.push(parsed);
169                    }
170                }
171                return Ok(results);
172            }
173        }
174
175        Ok(vec![])
176    }
177
178    /// Check API health
179    pub async fn health(&self) -> Result<Value, WOWSQLError> {
180        let url = format!("{}/api/v2/health", self.base_url);
181        self.execute_request(&url, "GET", None).await
182    }
183
184    /// Close the client (drops internal resources)
185    pub fn close(self) {
186        drop(self);
187    }
188
189    /// Execute a request and handle response
190    pub(crate) async fn execute_request<T: serde::de::DeserializeOwned>(
191        &self,
192        url: &str,
193        method: &str,
194        body: Option<Value>,
195    ) -> Result<T, WOWSQLError> {
196        let mut request = self
197            .client
198            .request(
199                method
200                    .parse()
201                    .map_err(|_| WOWSQLError::new("Invalid method", None, None))?,
202                url,
203            )
204            .header("Content-Type", "application/json")
205            .header("Accept", "application/json")
206            .header("Authorization", format!("Bearer {}", self.api_key));
207
208        if let Some(body) = body {
209            request = request.json(&body);
210        }
211
212        let response = request
213            .send()
214            .await
215            .map_err(|e| WOWSQLError::new(&format!("Request failed: {}", e), None, None))?;
216
217        let status = response.status();
218        let text = response.text().await.map_err(|e| {
219            WOWSQLError::new(&format!("Failed to read response: {}", e), None, None)
220        })?;
221
222        if !status.is_success() {
223            return Err(WOWSQLError::from_response(status.as_u16(), &text));
224        }
225
226        serde_json::from_str(&text)
227            .map_err(|e| WOWSQLError::new(&format!("Failed to parse response: {}", e), None, None))
228    }
229}