Skip to main content

reddb_client/connector/
http.rs

1//! HTTP/HTTPS REST client.
2//!
3//! Talks to `red`'s HTTP listener (`POST /query` with a JSON body).
4//! Bearer auth via the `Authorization` header. HTTPS uses rustls
5//! through ureq's `rustls` feature; certificate validation is on by
6//! default, no implicit "skip verify".
7//!
8//! ureq is synchronous; the bin offloads each call onto
9//! `tokio::task::spawn_blocking` so the current-thread runtime
10//! stays responsive.
11
12use std::fmt;
13
14#[derive(Debug, Clone)]
15pub enum Auth {
16    Anonymous,
17    Bearer(String),
18}
19
20#[derive(Debug)]
21pub enum HttpError {
22    Network(String),
23    Http { status: u16, body: String },
24    Decode(String),
25}
26
27impl fmt::Display for HttpError {
28    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
29        match self {
30            Self::Network(m) => write!(f, "network: {m}"),
31            Self::Http { status, body } => write!(f, "HTTP {status}: {body}"),
32            Self::Decode(m) => write!(f, "decode: {m}"),
33        }
34    }
35}
36
37impl std::error::Error for HttpError {}
38
39type Result<T> = std::result::Result<T, HttpError>;
40
41/// Single-shot query: POST `<base_url>/query` with a JSON body and
42/// return the response body as a string. The caller decides what to
43/// do with the response shape (typically pretty-print it).
44///
45/// Synchronous: callers from an async context should wrap in
46/// `tokio::task::spawn_blocking`.
47pub fn query_one_shot(base_url: &str, sql: &str, auth: &Auth) -> Result<String> {
48    let url = format!("{}/query", base_url.trim_end_matches('/'));
49    let body = serde_json::json!({ "query": sql }).to_string();
50    let mut req = ureq::post(&url).header("content-type", "application/json");
51    if let Auth::Bearer(token) = auth {
52        req = req.header("authorization", &format!("Bearer {token}"));
53    }
54    let mut resp = req
55        .send(body.as_bytes())
56        .map_err(|e| HttpError::Network(e.to_string()))?;
57    let status = resp.status().as_u16();
58    let body_text = resp
59        .body_mut()
60        .read_to_string()
61        .map_err(|e| HttpError::Decode(e.to_string()))?;
62    if status >= 400 {
63        return Err(HttpError::Http {
64            status,
65            body: body_text,
66        });
67    }
68    Ok(body_text)
69}