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
14use reddb_wire::auth::{
15    apikey_authorization_value, bearer_authorization_value, AUTHORIZATION_HEADER,
16};
17
18#[derive(Clone)]
19pub enum Auth {
20    Anonymous,
21    Bearer(String),
22    Basic { user: String, pass: String },
23    ApiKey(String),
24}
25
26impl fmt::Debug for Auth {
27    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
28        match self {
29            Self::Anonymous => f.write_str("Anonymous"),
30            Self::Bearer(_) => f.debug_tuple("Bearer").field(&"<redacted>").finish(),
31            Self::Basic { .. } => f
32                .debug_struct("Basic")
33                .field("user", &"<redacted>")
34                .field("pass", &"<redacted>")
35                .finish(),
36            Self::ApiKey(_) => f.debug_tuple("ApiKey").field(&"<redacted>").finish(),
37        }
38    }
39}
40
41#[derive(Debug)]
42pub enum HttpError {
43    Network(String),
44    Http { status: u16, body: String },
45    Decode(String),
46}
47
48impl fmt::Display for HttpError {
49    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50        match self {
51            Self::Network(m) => write!(f, "network: {m}"),
52            Self::Http { status, body } => write!(f, "HTTP {status}: {body}"),
53            Self::Decode(m) => write!(f, "decode: {m}"),
54        }
55    }
56}
57
58impl std::error::Error for HttpError {}
59
60type Result<T> = std::result::Result<T, HttpError>;
61
62/// Single-shot query: POST `<base_url>/query` with a JSON body and
63/// return the response body as a string. The caller decides what to
64/// do with the response shape (typically pretty-print it).
65///
66/// Synchronous: callers from an async context should wrap in
67/// `tokio::task::spawn_blocking`.
68pub fn query_one_shot(base_url: &str, sql: &str, auth: &Auth) -> Result<String> {
69    let url = format!("{}/query", base_url.trim_end_matches('/'));
70    let body = serde_json::json!({ "query": sql }).to_string();
71    let mut req = ureq::post(&url).header("content-type", "application/json");
72    match auth {
73        Auth::Anonymous => {}
74        Auth::Bearer(token) => {
75            req = req.header(AUTHORIZATION_HEADER, &bearer_authorization_value(token));
76        }
77        Auth::ApiKey(key) => {
78            req = req.header(AUTHORIZATION_HEADER, &apikey_authorization_value(key));
79        }
80        Auth::Basic { .. } => {}
81    }
82    let mut resp = req
83        .send(body.as_bytes())
84        .map_err(|e| HttpError::Network(e.to_string()))?;
85    let status = resp.status().as_u16();
86    let body_text = resp
87        .body_mut()
88        .read_to_string()
89        .map_err(|e| HttpError::Decode(e.to_string()))?;
90    if status >= 400 {
91        return Err(HttpError::Http {
92            status,
93            body: body_text,
94        });
95    }
96    Ok(body_text)
97}