use reqwest::Client;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::time::Duration;
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum VerificationType {
#[default]
NaturalLanguage,
Math,
Logic,
Stats,
Fact,
Code,
Sql,
Image,
Reasoning,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "UPPERCASE")]
pub enum VerificationStatus {
Verified,
Failed,
Corrected,
Blocked,
Error,
Timeout,
Unsupported,
}
#[derive(Debug, Clone, Serialize)]
pub struct VerificationRequest {
pub query: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub r#type: Option<VerificationType>,
#[serde(skip_serializing_if = "Option::is_none")]
pub params: Option<HashMap<String, serde_json::Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub options: Option<RequestOptions>,
}
#[derive(Debug, Clone, Serialize, Default)]
pub struct RequestOptions {
#[serde(skip_serializing_if = "Option::is_none")]
pub timeout_ms: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub include_proof: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub include_attestation: Option<bool>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct VerificationResponse {
pub status: VerificationStatus,
pub verified: bool,
#[serde(default)]
pub engine: Option<String>,
#[serde(default)]
pub result: Option<HashMap<String, serde_json::Value>>,
#[serde(default)]
pub attestation: Option<String>,
#[serde(default)]
pub error: Option<ErrorInfo>,
#[serde(default)]
pub metadata: Option<ResponseMetadata>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ErrorInfo {
pub code: String,
pub message: String,
#[serde(default)]
pub details: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ResponseMetadata {
pub request_id: Option<String>,
pub latency_ms: Option<f64>,
pub protocol_version: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
pub struct BatchItem {
pub query: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub r#type: Option<VerificationType>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct BatchResponse {
pub job_id: String,
pub status: String,
#[serde(default)]
pub summary: Option<BatchSummary>,
#[serde(default)]
pub items: Vec<BatchResult>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct BatchSummary {
pub total: u32,
pub verified: u32,
pub failed: u32,
pub success_rate: f64,
}
#[derive(Debug, Clone, Deserialize)]
pub struct BatchResult {
pub id: String,
pub status: VerificationStatus,
pub verified: bool,
#[serde(default)]
pub result: Option<HashMap<String, serde_json::Value>>,
#[serde(default)]
pub error: Option<ErrorInfo>,
}
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("HTTP error: {0}")]
Http(#[from] reqwest::Error),
#[error("API error [{code}]: {message}")]
Api { code: String, message: String },
#[error("Authentication failed")]
Auth,
#[error("Rate limit exceeded")]
RateLimit,
#[error("Serialization error: {0}")]
Serialization(#[from] serde_json::Error),
}
pub struct QWEDClient {
api_key: String,
base_url: String,
client: Client,
}
impl QWEDClient {
pub fn new(api_key: impl Into<String>) -> Self {
Self::with_options(api_key, "http://localhost:8000", Duration::from_secs(30))
}
pub fn with_options(
api_key: impl Into<String>,
base_url: impl Into<String>,
timeout: Duration,
) -> Self {
Self {
api_key: api_key.into(),
base_url: base_url.into().trim_end_matches('/').to_string(),
client: Client::builder()
.timeout(timeout)
.build()
.expect("Failed to create HTTP client"),
}
}
pub async fn health(&self) -> Result<HashMap<String, serde_json::Value>, Error> {
self.get("/health").await
}
pub async fn verify(&self, query: &str) -> Result<VerificationResponse, Error> {
let req = VerificationRequest {
query: query.to_string(),
r#type: Some(VerificationType::NaturalLanguage),
params: None,
options: None,
};
self.post("/verify/natural_language", &req).await
}
pub async fn verify_math(&self, expression: &str) -> Result<VerificationResponse, Error> {
let mut body = HashMap::new();
body.insert("expression", expression);
self.post("/verify/math", &body).await
}
pub async fn verify_logic(&self, query: &str) -> Result<VerificationResponse, Error> {
let mut body = HashMap::new();
body.insert("query", query);
self.post("/verify/logic", &body).await
}
pub async fn verify_code(&self, code: &str, language: &str) -> Result<VerificationResponse, Error> {
let mut body = HashMap::new();
body.insert("code", code);
body.insert("language", language);
self.post("/verify/code", &body).await
}
pub async fn verify_fact(&self, claim: &str, context: &str) -> Result<VerificationResponse, Error> {
let mut body = HashMap::new();
body.insert("claim", claim);
body.insert("context", context);
self.post("/verify/fact", &body).await
}
pub async fn verify_sql(&self, query: &str, schema: &str, dialect: &str) -> Result<VerificationResponse, Error> {
let mut body = HashMap::new();
body.insert("query", query);
body.insert("schema_ddl", schema);
body.insert("dialect", dialect);
self.post("/verify/sql", &body).await
}
pub async fn verify_batch(&self, items: Vec<BatchItem>) -> Result<BatchResponse, Error> {
let mut body = HashMap::new();
body.insert("items", items);
self.post("/verify/batch", &body).await
}
async fn get<T: for<'de> Deserialize<'de>>(&self, path: &str) -> Result<T, Error> {
let response = self.client
.get(format!("{}{}", self.base_url, path))
.header("X-API-Key", &self.api_key)
.send()
.await?;
self.handle_response(response).await
}
async fn post<T: for<'de> Deserialize<'de>, B: Serialize>(&self, path: &str, body: &B) -> Result<T, Error> {
let response = self.client
.post(format!("{}{}", self.base_url, path))
.header("X-API-Key", &self.api_key)
.header("Content-Type", "application/json")
.json(body)
.send()
.await?;
self.handle_response(response).await
}
async fn handle_response<T: for<'de> Deserialize<'de>>(&self, response: reqwest::Response) -> Result<T, Error> {
let status = response.status();
if status == 401 {
return Err(Error::Auth);
}
if status == 429 {
return Err(Error::RateLimit);
}
if !status.is_success() {
let text = response.text().await.unwrap_or_default();
return Err(Error::Api {
code: format!("HTTP-{}", status.as_u16()),
message: text,
});
}
Ok(response.json().await?)
}
}
pub fn is_verified(response: &VerificationResponse) -> bool {
response.verified
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_verification_type_serialization() {
let t = VerificationType::Math;
let json = serde_json::to_string(&t).unwrap();
assert_eq!(json, "\"math\"");
}
#[test]
fn test_verification_status_deserialization() {
let json = "\"VERIFIED\"";
let status: VerificationStatus = serde_json::from_str(json).unwrap();
assert_eq!(status, VerificationStatus::Verified);
}
}