metaflux_client/rest/
mod.rs1use std::time::Duration;
18
19use reqwest::Client as HttpClient;
20use serde::Serialize;
21use serde_json::Value;
22
23use crate::error::ClientError;
24
25pub mod exchange;
26pub mod exchange_typed;
27pub mod explorer;
28pub mod info;
29
30#[derive(Debug, Clone)]
32pub struct RestClient {
33 base_url: String,
34 http: HttpClient,
35}
36
37impl RestClient {
38 pub fn new(base_url: impl Into<String>) -> Result<Self, ClientError> {
46 let base_url = base_url.into();
47 if !base_url.starts_with("http://") && !base_url.starts_with("https://") {
48 return Err(ClientError::Builder(format!(
49 "base_url must start with http(s)://, got `{base_url}`"
50 )));
51 }
52 let base_url = base_url.trim_end_matches('/').to_string();
53 let http = HttpClient::builder()
54 .user_agent(concat!("metaflux-client/", env!("CARGO_PKG_VERSION")))
55 .timeout(Duration::from_secs(30))
56 .pool_idle_timeout(Duration::from_secs(60))
57 .build()
58 .map_err(|e| ClientError::Builder(e.to_string()))?;
59 Ok(Self { base_url, http })
60 }
61
62 #[must_use]
64 pub fn from_http(base_url: impl Into<String>, http: HttpClient) -> Self {
65 let base_url = base_url.into().trim_end_matches('/').to_string();
66 Self { base_url, http }
67 }
68
69 #[must_use]
71 pub fn info(&self) -> info::Info<'_> {
72 info::Info { client: self }
73 }
74
75 #[must_use]
77 pub fn exchange(&self) -> exchange::Exchange<'_> {
78 exchange::Exchange { client: self }
79 }
80
81 #[must_use]
83 pub fn explorer(&self) -> explorer::Explorer<'_> {
84 explorer::Explorer { client: self }
85 }
86
87 #[must_use]
89 pub fn base_url(&self) -> &str {
90 &self.base_url
91 }
92
93 #[allow(dead_code)]
95 pub(crate) fn http(&self) -> &HttpClient {
96 &self.http
97 }
98
99 pub(crate) async fn post_json<Req, Resp>(
108 &self,
109 path: &str,
110 body: &Req,
111 ) -> Result<Resp, ClientError>
112 where
113 Req: Serialize + ?Sized,
114 Resp: serde::de::DeserializeOwned,
115 {
116 let url = format!("{}{path}", self.base_url);
117 let resp = self.http.post(&url).json(body).send().await?;
118 let status = resp.status();
119 let bytes = resp.bytes().await?;
120
121 if !status.is_success() {
122 if let Ok(env) = serde_json::from_slice::<Value>(&bytes) {
124 if let Some(msg) = env.get("error").and_then(Value::as_str) {
125 return Err(ClientError::ProtocolError {
126 code: status.as_u16(),
127 msg: msg.into(),
128 });
129 }
130 }
131 return Err(ClientError::ProtocolError {
132 code: status.as_u16(),
133 msg: String::from_utf8_lossy(&bytes).into_owned(),
134 });
135 }
136
137 let value: Value = serde_json::from_slice(&bytes)?;
138 let payload = peel_envelope(value);
139 serde_json::from_value(payload).map_err(ClientError::from)
140 }
141}
142
143fn peel_envelope(value: Value) -> Value {
154 if let Value::Object(ref map) = value {
155 if map.contains_key("data") && map.contains_key("type") {
156 if let Value::Object(mut map) = value {
158 return map.remove("data").unwrap_or(Value::Null);
159 }
160 }
161 }
162 value
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168
169 #[test]
170 fn rejects_non_http_url() {
171 let err = RestClient::new("ftp://devnet-gateway.mtf.exchange").unwrap_err();
172 assert!(matches!(err, ClientError::Builder(_)));
173 }
174
175 #[test]
176 fn strips_trailing_slash() {
177 let c = RestClient::new("https://devnet-gateway.mtf.exchange/").unwrap();
178 assert_eq!(c.base_url(), "https://devnet-gateway.mtf.exchange");
179 }
180
181 #[test]
182 fn peels_data_from_typed_envelope() {
183 let env = serde_json::json!({
184 "type": "node_info",
185 "data": { "chain_id": 114514, "epoch": 1 }
186 });
187 let inner = super::peel_envelope(env);
188 assert_eq!(inner, serde_json::json!({ "chain_id": 114514, "epoch": 1 }));
189 }
190
191 #[test]
192 fn passes_bare_object_through_unchanged() {
193 let bare = serde_json::json!({ "accepted": true, "mempool_depth": 3 });
195 assert_eq!(super::peel_envelope(bare.clone()), bare);
196 }
197
198 #[test]
199 fn passes_array_through_unchanged() {
200 let arr = serde_json::json!([1, 2, 3]);
201 assert_eq!(super::peel_envelope(arr.clone()), arr);
202 }
203
204 #[test]
205 fn does_not_peel_a_data_field_without_type() {
206 let payload = serde_json::json!({ "data": { "x": 1 } });
209 assert_eq!(super::peel_envelope(payload.clone()), payload);
210 }
211}