Skip to main content

haystack_client/transport/
http.rs

1use reqwest::Client;
2
3use crate::error::ClientError;
4use crate::transport::Transport;
5use haystack_core::codecs::codec_for;
6use haystack_core::data::HGrid;
7use haystack_core::kinds::Kind;
8
9/// Operations that use GET (noSideEffects).
10const GET_OPS: &[&str] = &["about", "ops", "formats"];
11
12/// HTTP transport for communicating with a Haystack server.
13///
14/// Sends requests as encoded grids over HTTP using the configured wire format
15/// (default: `text/zinc`). GET is used for side-effect-free ops; POST for all others.
16pub struct HttpTransport {
17    client: Client,
18    base_url: String,
19    auth_token: String,
20    format: String,
21}
22
23impl HttpTransport {
24    /// Create a new HTTP transport.
25    ///
26    /// `base_url` should be the server API root (e.g. `http://localhost:8080/api`).
27    /// `auth_token` is the bearer token obtained from SCRAM authentication.
28    pub fn new(base_url: &str, auth_token: String) -> Self {
29        Self {
30            client: Client::new(),
31            base_url: base_url.trim_end_matches('/').to_string(),
32            auth_token,
33            format: "text/zinc".to_string(),
34        }
35    }
36
37    /// Create a new HTTP transport with a specific wire format.
38    pub fn with_format(base_url: &str, auth_token: String, format: &str) -> Self {
39        Self {
40            client: Client::new(),
41            base_url: base_url.trim_end_matches('/').to_string(),
42            auth_token,
43            format: format.to_string(),
44        }
45    }
46}
47
48impl Transport for HttpTransport {
49    async fn call(&self, op: &str, req: &HGrid) -> Result<HGrid, ClientError> {
50        let url = format!("{}/{}", self.base_url, op);
51        let codec = codec_for(&self.format)
52            .ok_or_else(|| ClientError::Codec(format!("unsupported format: {}", self.format)))?;
53
54        let response = if GET_OPS.contains(&op) {
55            // GET request for side-effect-free ops
56            self.client
57                .get(&url)
58                .header(
59                    "Authorization",
60                    format!("BEARER authToken={}", self.auth_token),
61                )
62                .header("Accept", codec.mime_type())
63                .send()
64                .await
65                .map_err(|e| ClientError::Transport(e.to_string()))?
66        } else {
67            // POST request for all other ops
68            let body = codec
69                .encode_grid(req)
70                .map_err(|e| ClientError::Codec(e.to_string()))?;
71
72            self.client
73                .post(&url)
74                .header(
75                    "Authorization",
76                    format!("BEARER authToken={}", self.auth_token),
77                )
78                .header("Content-Type", codec.mime_type())
79                .header("Accept", codec.mime_type())
80                .body(body)
81                .send()
82                .await
83                .map_err(|e| ClientError::Transport(e.to_string()))?
84        };
85
86        let status = response.status();
87        let resp_body = response
88            .text()
89            .await
90            .map_err(|e| ClientError::Transport(e.to_string()))?;
91
92        if !status.is_success() {
93            return Err(ClientError::ServerError(format!(
94                "HTTP {} — {}",
95                status, resp_body
96            )));
97        }
98
99        // Decode the response grid
100        let grid = codec
101            .decode_grid(&resp_body)
102            .map_err(|e| ClientError::Codec(e.to_string()))?;
103
104        // Check for error grid (meta has "err" marker)
105        if grid.is_err() {
106            let dis = grid
107                .meta
108                .get("dis")
109                .and_then(|k| {
110                    if let Kind::Str(s) = k {
111                        Some(s.as_str())
112                    } else {
113                        None
114                    }
115                })
116                .unwrap_or("unknown server error");
117            return Err(ClientError::ServerError(dis.to_string()));
118        }
119
120        Ok(grid)
121    }
122
123    async fn close(&self) -> Result<(), ClientError> {
124        // HTTP is stateless; nothing to close.
125        Ok(())
126    }
127}