Skip to main content

objectiveai_cli/
viewer_client.rs

1//! Synchronous-response HTTP client for the viewer's HTTP server.
2//!
3//! Bypasses the SDK's fire-and-forget viewer path because callers
4//! (`viewer send`) want to see the response. Constructed in
5//! [`crate::context::Context::viewer_client`], which resolves the
6//! address (config `viewer.address`, else the `viewer spawn` flow's
7//! published lock URL) and the signature (env `VIEWER_SIGNATURE`,
8//! else config `viewer.signature`).
9
10use crate::error::Error;
11
12#[derive(Debug, Clone)]
13pub struct ViewerClient {
14    http: reqwest::Client,
15    /// Base URL, e.g. `http://127.0.0.1:51234`.
16    address: String,
17    /// `X-VIEWER-SIGNATURE` header value, when configured.
18    signature: Option<String>,
19}
20
21impl ViewerClient {
22    pub fn new(address: String, signature: Option<String>) -> Self {
23        Self {
24            http: reqwest::Client::new(),
25            address,
26            signature,
27        }
28    }
29
30    /// POST `body` as JSON to `{address}{path}`. `path` must start
31    /// with `/` ([`Error::ViewerPathMissingSlash`]). A non-2xx status
32    /// is NOT an error — the response is returned as-is; the body is
33    /// parsed as JSON, falling back to `Value::String(raw_text)`.
34    pub async fn send(
35        &self,
36        path: &str,
37        body: &serde_json::Value,
38    ) -> Result<(u16, serde_json::Value), Error> {
39        if !path.starts_with('/') {
40            return Err(Error::ViewerPathMissingSlash(path.to_string()));
41        }
42        let url = format!("{}{}", self.address.trim_end_matches('/'), path);
43        let mut req = self
44            .http
45            .post(&url)
46            .header("Content-Type", "application/json")
47            .json(body);
48        if let Some(sig) = &self.signature {
49            req = req.header("X-VIEWER-SIGNATURE", sig);
50        }
51        let response = req
52            .send()
53            .await
54            .map_err(|e| Error::ViewerSendHttp(e.to_string()))?;
55
56        let status = response.status().as_u16();
57        let text = response.text().await.unwrap_or_default();
58        let body = serde_json::from_str::<serde_json::Value>(&text)
59            .unwrap_or(serde_json::Value::String(text));
60        Ok((status, body))
61    }
62}