use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::env;
pub struct PGAPIClient {
base_url: String,
api_key: String,
client: reqwest::Client,
}
#[derive(Debug, Deserialize)]
pub struct ApiResponse<T> {
pub success: bool,
pub data: Option<T>,
pub error: Option<ApiError>,
pub metadata: ResponseMetadata,
}
#[derive(Debug, Deserialize)]
pub struct ApiError {
pub code: String,
pub message: String,
}
#[derive(Debug, Deserialize)]
pub struct ResponseMetadata {
pub request_id: String,
pub execution_time_ms: u64,
pub rows_affected: Option<u64>,
pub instance_id: String,
pub timestamp: String,
}
#[derive(Debug, Deserialize)]
pub struct AccountInfo {
pub id: String,
pub name: String,
pub role: String,
pub rate_limit: u32,
pub max_connections: u32,
pub databases: Vec<String>,
}
#[derive(Debug, Deserialize)]
pub struct QueryResult {
pub rows: Vec<serde_json::Value>,
pub columns: Vec<String>,
pub row_count: u64,
}
#[derive(Debug, Deserialize)]
pub struct UsageStats {
pub requests_today: u64,
pub active_connections: u32,
pub rate_limit_remaining: u32,
}
impl PGAPIClient {
pub fn new(base_url: impl Into<String>, api_key: impl Into<String>) -> Result<Self> {
Ok(Self {
base_url: base_url.into(),
api_key: api_key.into(),
client: reqwest::Client::new(),
})
}
async fn get<T: for<'de> Deserialize<'de>>(&self, endpoint: &str) -> Result<ApiResponse<T>> {
let url = format!("{}{}", self.base_url, endpoint);
let response = self
.client
.get(&url)
.header("X-API-Key", &self.api_key)
.send()
.await
.context("Falha na requisição GET")?;
let status = response.status();
let body = response
.json::<ApiResponse<T>>()
.await
.context("Falha ao parsear resposta")?;
if !status.is_success() || !body.success {
anyhow::bail!(
"Erro na API: {:?}",
body.error.unwrap_or_else(|| ApiError {
code: "UNKNOWN".to_string(),
message: "Erro desconhecido".to_string(),
})
);
}
Ok(body)
}
async fn post<T: for<'de> Deserialize<'de>>(
&self,
endpoint: &str,
body: impl Serialize,
) -> Result<ApiResponse<T>> {
let url = format!("{}{}", self.base_url, endpoint);
let response = self
.client
.post(&url)
.header("X-API-Key", &self.api_key)
.header("Content-Type", "application/json")
.json(&body)
.send()
.await
.context("Falha na requisição POST")?;
let status = response.status();
let body = response
.json::<ApiResponse<T>>()
.await
.context("Falha ao parsear resposta")?;
if !status.is_success() || !body.success {
anyhow::bail!(
"Erro na API: {:?}",
body.error.unwrap_or_else(|| ApiError {
code: "UNKNOWN".to_string(),
message: "Erro desconhecido".to_string(),
})
);
}
Ok(body)
}
pub async fn health(&self) -> Result<serde_json::Value> {
let url = format!("{}/health", self.base_url);
let response = self
.client
.get(&url)
.send()
.await
.context("Falha no health check")?;
Ok(response.json().await?)
}
pub async fn get_account(&self) -> Result<AccountInfo> {
let response = self.get::<AccountInfo>("/v1/account").await?;
Ok(response.data.unwrap())
}
pub async fn get_usage(&self) -> Result<UsageStats> {
let response = self.get::<UsageStats>("/v1/account/usage").await?;
Ok(response.data.unwrap())
}
pub async fn list_databases(&self) -> Result<Vec<String>> {
let response = self
.get::<serde_json::Value>("/v1/databases")
.await?;
let data = response.data.unwrap();
Ok(serde_json::from_value(data["databases"].clone())?)
}
pub async fn query(
&self,
database: &str,
sql: &str,
params: Option<Vec<serde_json::Value>>,
) -> Result<QueryResult> {
let mut body = json!({
"database": database,
"query": sql
});
if let Some(p) = params {
body["params"] = json!(p);
}
let response = self.post::<QueryResult>("/v1/query", body).await?;
Ok(response.data.unwrap())
}
pub async fn batch(
&self,
queries: Vec<serde_json::Value>,
) -> Result<Vec<QueryResult>> {
let response = self.post::<Vec<QueryResult>>("/v1/batch", queries).await?;
Ok(response.data.unwrap())
}
pub async fn transaction(
&self,
database: &str,
queries: Vec<serde_json::Value>,
) -> Result<serde_json::Value> {
let body = json!({
"database": database,
"queries": queries
});
let response = self.post::<serde_json::Value>("/v1/transaction", body).await?;
Ok(response.data.unwrap())
}
}
fn print_section(title: &str) {
println!("\n{}", "=".repeat(50));
println!(" {}", title);
println!("{}", "=".repeat(50));
}
#[tokio::main]
async fn main() -> Result<()> {
let api_key = env::var("API_KEY").unwrap_or_else(|_| "sk_dev_123456789".to_string());
let base_url = env::var("BASE_URL").unwrap_or_else(|_| "http://localhost:8580".to_string());
println!("{}", "=".repeat(50));
println!(" PG-API Rust Client Example");
println!("{}", "=".repeat(50));
println!("\nConfiguração:");
println!(" Base URL: {}", base_url);
println!(" API Key: {}...", &api_key[..10.min(api_key.len())]);
let client = PGAPIClient::new(&base_url, &api_key)?;
print_section("1. Health Check");
let health = client.health().await?;
println!("Status: {}", health["status"]);
println!("Versão: {}", health["version"]);
print_section("2. Informações da Conta");
let account = client.get_account().await?;
println!("ID: {}", account.id);
println!("Nome: {}", account.name);
println!("Role: {}", account.role);
println!("Databases: {}", account.databases.join(", "));
print_section("3. Versão do PostgreSQL");
let result = client.query("postgres", "SELECT version()", None).await?;
println!("Resultado: {}", result.rows[0]["version"]);
print_section("4. Criar Tabela de Exemplo");
let result = client
.query(
"postgres",
"CREATE TABLE IF NOT EXISTS rust_demo (
id SERIAL PRIMARY KEY,
name VARCHAR(100),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)",
None,
)
.await?;
println!("Tabela criada: {} linha(s)", result.row_count);
print_section("5. Inserir Dados");
let result = client
.query(
"postgres",
"INSERT INTO rust_demo (name) VALUES ($1), ($2) RETURNING *",
Some(vec![json!("Alice"), json!("Bob")]),
)
.await?;
println!("Linhas inseridas: {}", result.row_count);
for row in &result.rows {
println!(" - ID: {}, Name: {}", row["id"], row["name"]);
}
print_section("6. Consultar Dados");
let result = client
.query("postgres", "SELECT * FROM rust_demo ORDER BY id", None)
.await?;
println!("Total de registros: {}", result.row_count);
for row in &result.rows {
println!(" ID: {}, Name: {}", row["id"], row["name"]);
}
print_section("7. Query com Parâmetros");
let result = client
.query(
"postgres",
"SELECT * FROM rust_demo WHERE name = $1",
Some(vec![json!("Alice")]),
)
.await?;
println!("Encontrado: {} registro(s)", result.row_count);
print_section("8. Batch de Queries");
let batch_queries = vec![
json!({
"database": "postgres",
"query": "SELECT $1::text as msg",
"params": ["Batch Query 1"]
}),
json!({
"database": "postgres",
"query": "SELECT $1::text as msg",
"params": ["Batch Query 2"]
}),
];
let results = client.batch(batch_queries).await?;
println!("Queries executadas: {}", results.len());
print_section("9. Transação");
let tx_queries = vec![
json!({
"query": "INSERT INTO rust_demo (name) VALUES ($1) RETURNING id",
"params": ["Transaction User"]
}),
];
let result = client.transaction("postgres", tx_queries).await?;
println!("Transação commitada: {}", result["committed"]);
print_section("10. Listar Databases");
let databases = client.list_databases().await?;
println!("Databases: {}", databases.join(", "));
print_section("11. Limpar Dados de Exemplo");
let result = client
.query(
"postgres",
"DELETE FROM rust_demo WHERE name LIKE $1",
Some(vec![json!("%User%")]),
)
.await?;
println!("Registros removidos");
print_section("12. Dropar Tabela");
let _ = client
.query("postgres", "DROP TABLE IF EXISTS rust_demo", None)
.await?;
println!("Tabela removida");
print_section("13. Estatísticas de Uso");
let usage = client.get_usage().await?;
println!("Requisições hoje: {}", usage.requests_today);
println!("Conexões ativas: {}", usage.active_connections);
print_section("Exemplos Concluídos! ✅");
println!("Todos os exemplos foram executados com sucesso!");
Ok(())
}