haystack_client/transport/
http.rs1use std::time::Duration;
2
3use reqwest::Client;
4
5use crate::error::ClientError;
6use crate::transport::Transport;
7use haystack_core::codecs::codec_for;
8use haystack_core::data::HGrid;
9use haystack_core::kinds::Kind;
10
11const GET_OPS: &[&str] = &["about", "ops", "formats"];
13
14pub struct HttpTransport {
19 client: Client,
20 base_url: String,
21 auth_token: zeroize::Zeroizing<String>,
22 format: String,
23}
24
25impl HttpTransport {
26 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: zeroize::Zeroizing::new(auth_token),
38 format: "text/zinc".to_string(),
39 }
40 }
41
42 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: zeroize::Zeroizing::new(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
60 let response = if GET_OPS.contains(&op) {
61 self.client
63 .get(&url)
64 .header(
65 "Authorization",
66 format!("BEARER authToken={}", &*self.auth_token),
67 )
68 .header("Accept", &self.format)
69 .send()
70 .await
71 .map_err(|e| ClientError::Transport(e.to_string()))?
72 } else {
73 let codec = codec_for(&self.format).ok_or_else(|| {
74 ClientError::Codec(format!("unsupported format: {}", self.format))
75 })?;
76 let text = codec
77 .encode_grid(req)
78 .map_err(|e| ClientError::Codec(e.to_string()))?;
79 let body_bytes = text.into_bytes();
80 let content_type = codec.mime_type();
81
82 self.client
83 .post(&url)
84 .header(
85 "Authorization",
86 format!("BEARER authToken={}", &*self.auth_token),
87 )
88 .header("Content-Type", content_type)
89 .header("Accept", &self.format)
90 .body(body_bytes)
91 .send()
92 .await
93 .map_err(|e| ClientError::Transport(e.to_string()))?
94 };
95
96 let status = response.status();
97
98 let resp_body = response
99 .text()
100 .await
101 .map_err(|e| ClientError::Transport(e.to_string()))?;
102 if !status.is_success() {
103 return Err(ClientError::ServerError(format!(
104 "HTTP {} — {}",
105 status, resp_body
106 )));
107 }
108 let codec = codec_for(&self.format)
109 .ok_or_else(|| ClientError::Codec(format!("unsupported format: {}", self.format)))?;
110 let grid = codec
111 .decode_grid(&resp_body)
112 .map_err(|e| ClientError::Codec(e.to_string()))?;
113
114 if grid.is_err() {
116 let dis = grid
117 .meta
118 .get("dis")
119 .and_then(|k| {
120 if let Kind::Str(s) = k {
121 Some(s.as_str())
122 } else {
123 None
124 }
125 })
126 .unwrap_or("unknown server error");
127 return Err(ClientError::ServerError(dis.to_string()));
128 }
129
130 Ok(grid)
131 }
132
133 async fn close(&self) -> Result<(), ClientError> {
134 Ok(())
136 }
137}