Skip to main content

haystack_client/transport/
http.rs

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