OXYGENHUB

Coverage / crates/oh/src/client.rs 62.2%

46 of 74 lines run, 39 of 153 functions · run #529 on main · run missed

1
//! The hub's JSON API over HTTPS, with the checkout's CA when the hub's
2
//! certificate is self-signed.
3
4
use crate::target::Target;
5
use anyhow::{bail, Context, Result};
6
use serde::de::DeserializeOwned;
7
use serde::Serialize;
8
use std::time::Duration;
9
10
pub struct Client {
11
    agent: ureq::Agent,
12
    base: String,
13
    token: Option<String>,
14
}
15
16
impl Client {
1746×
    pub fn new(t: &Target) -> Result<Client> {
1846×
        let tls = oxy_core::protocol::tls_client_config(t.ca.as_deref()).map_err(|e| anyhow::anyhow!("{e}"))?;
1946×
        let agent = ureq::AgentBuilder::new().tls_config(tls).timeout(Duration::from_secs(120)).build();
2046×
        Ok(Client { agent, base: format!("{}/api/v1", t.hub), token: t.token.clone() })
2146×
    }
22
2347×
    fn req(&self, method: &str, path: &str) -> ureq::Request {
2447×
        let r = self.agent.request(method, &format!("{}{path}", self.base)).set("Accept", "application/json");
2547×
        match &self.token {
2645×
            Some(t) => r.set("Authorization", &format!("Bearer {t}")),
272×
            None => r,
28
        }
2947×
    }
30
3147×
    fn finish(res: Result<ureq::Response, ureq::Error>, what: &str) -> Result<String> {
324×
        match res {
3343×
            Ok(r) => Ok(r.into_string()?),
344×
            Err(ureq::Error::Status(code, r)) => {
354×
                let body = r.into_string().unwrap_or_default();
36
                // Problem documents (RFC 7807) carry the reason in `detail`.
374×
                let detail = serde_json::from_str::<serde_json::Value>(&body)
384×
                    .ok()
394×
                    .and_then(|v| v["detail"].as_str().map(String::from))
404×
                    .unwrap_or(body);
414×
                let hint = match code {
421×
                    401 => " (sign in: `oh auth login`, or push with a token first)",
430×
                    403 => " (your token lacks the scope or the role for this)",
443×
                    _ => "",
45
                };
464×
                bail!("{what}: HTTP {code}: {}{hint}", detail.trim())
47
            }
480×
            Err(e) => Err(e).context(what.to_string()),
49
        }
5047×
    }
51
52
    /// GET, as raw JSON text.
5324×
    pub fn get_text(&self, path: &str) -> Result<String> {
5424×
        Self::finish(self.req("GET", path).call(), &format!("GET {path}"))
5524×
    }
56
5716×
    pub fn get<T: DeserializeOwned>(&self, path: &str) -> Result<T> {
5816×
        let text = self.get_text(path)?;
5915×
        serde_json::from_str(&text).with_context(|| format!("GET {path}: unexpected answer"))
6016×
    }
61
6222×
    pub fn send_text(&self, method: &str, path: &str, body: &impl Serialize) -> Result<String> {
6322×
        let json = serde_json::to_string(body)?;
6422×
        Self::finish(self.req(method, path).set("Content-Type", "application/json").send_string(&json), &format!("{method} {path}"))
6522×
    }
66
6717×
    pub fn send<T: DeserializeOwned>(&self, method: &str, path: &str, body: &impl Serialize) -> Result<T> {
6817×
        let text = self.send_text(method, path, body)?;
6914×
        serde_json::from_str(&text).with_context(|| format!("{method} {path}: unexpected answer"))
7017×
    }
71
72
    /// POST JSON, returning the status and body whatever the status (the
73
    /// device sign-in's answers while it waits are errors by design).
740×
    pub fn post_raw(&self, path: &str, body: &impl Serialize) -> Result<(u16, String)> {
750×
        let json = serde_json::to_string(body)?;
760×
        match self.req("POST", path).set("Content-Type", "application/json").send_string(&json) {
770×
            Ok(r) => Ok((r.status(), r.into_string()?)),
780×
            Err(ureq::Error::Status(code, r)) => Ok((code, r.into_string().unwrap_or_default())),
790×
            Err(e) => Err(e).context(format!("POST {path}")),
80
        }
810×
    }
82
83
    /// GET raw bytes from the API (a report's file).
840×
    pub fn get_bytes(&self, path: &str) -> Result<Vec<u8>> {
850×
        match self.req("GET", path).call() {
860×
            Ok(r) => {
870×
                let mut out = Vec::new();
880×
                std::io::Read::read_to_end(&mut r.into_reader(), &mut out)?;
890×
                Ok(out)
90
            }
910×
            Err(e) => Self::finish(Err(e), &format!("GET {path}")).map(String::into_bytes),
92
        }
930×
    }
94
95
    /// GET a path at the hub's root, outside the API (`/.well-known/…`).
960×
    pub fn get_root_bytes(&self, path: &str) -> Result<Vec<u8>> {
970×
        let hub = self.base.trim_end_matches("/api/v1");
980×
        let r = self.agent.get(&format!("{hub}{path}")).call().with_context(|| format!("GET {path}"))?;
990×
        let mut out = Vec::new();
1000×
        std::io::Read::read_to_end(&mut r.into_reader(), &mut out)?;
1010×
        Ok(out)
1020×
    }
103
104
    /// POST raw bytes (a release asset).
1051×
    pub fn upload(&self, path: &str, bytes: &[u8]) -> Result<String> {
1061×
        Self::finish(
1071×
            self.req("POST", path).set("Content-Type", "application/octet-stream").timeout(Duration::from_secs(1800)).send_bytes(bytes),
1081×
            &format!("POST {path}"),
109
        )
1101×
    }
111
}