Skip to main content

m4a_agent/backend/
wire.rs

1//! The smallest HTTP shape the login needs, so the protocol code is the same over any transport
2//! (and testable without a network).
3
4use serde_json::Value;
5
6use crate::error::{AgentError, Result};
7
8pub trait Wire: Send + Sync {
9    /// POST JSON; the status and the JSON answer (`Null` when the body is not JSON).
10    fn post(&self, path: &str, body: &Value, bearer: Option<&str>) -> Result<(u16, Value)>;
11    fn get(&self, path: &str, bearer: Option<&str>) -> Result<(u16, Value)>;
12}
13
14#[cfg(any(feature = "tier-server", feature = "tier-matrix"))]
15pub struct HttpWire {
16    base: String,
17    http: reqwest::blocking::Client,
18}
19
20#[cfg(any(feature = "tier-server", feature = "tier-matrix"))]
21impl HttpWire {
22    pub fn new(base: &str) -> Result<Self> {
23        let http = reqwest::blocking::Client::builder().timeout(std::time::Duration::from_secs(30)).build().map_err(|e| AgentError::Transport(e.to_string()))?;
24        Ok(Self { base: base.trim_end_matches('/').to_string(), http })
25    }
26    fn finish(r: reqwest::Result<reqwest::blocking::Response>) -> Result<(u16, Value)> {
27        let r = r.map_err(|e| AgentError::Transport(e.to_string()))?;
28        let status = r.status().as_u16();
29        Ok((status, r.json::<Value>().unwrap_or(Value::Null)))
30    }
31}
32
33#[cfg(any(feature = "tier-server", feature = "tier-matrix"))]
34impl Wire for HttpWire {
35    fn post(&self, path: &str, body: &Value, bearer: Option<&str>) -> Result<(u16, Value)> {
36        let mut r = self.http.post(format!("{}{path}", self.base)).json(body);
37        if let Some(b) = bearer {
38            r = r.bearer_auth(b);
39        }
40        Self::finish(r.send())
41    }
42    fn get(&self, path: &str, bearer: Option<&str>) -> Result<(u16, Value)> {
43        let mut r = self.http.get(format!("{}{path}", self.base));
44        if let Some(b) = bearer {
45            r = r.bearer_auth(b);
46        }
47        Self::finish(r.send())
48    }
49}
50
51/// Reads a string field, or fails as a protocol error.
52pub fn field(v: &Value, key: &str) -> Result<String> {
53    v.get(key).and_then(Value::as_str).map(str::to_string).ok_or_else(|| AgentError::Protocol(format!("answer without {key}")))
54}