at_jet/
client.rs

1//! HTTP Client for AT-Jet
2//!
3//! Provides a dual-format HTTP client for calling AT-Jet services.
4//! - Protobuf: Production format (default, efficient)
5//! - JSON: Debug format (requires debug key)
6
7use {crate::{content_types::{APPLICATION_JSON,
8                             APPLICATION_PROTOBUF},
9             dual_format::DEBUG_FORMAT_HEADER,
10             error::{JetError,
11                     Result}},
12     bytes::Bytes,
13     prost::Message,
14     reqwest::{Client,
15               Response},
16     serde::{Serialize,
17             de::DeserializeOwned},
18     std::time::Duration,
19     tracing::debug};
20
21/// AT-Jet HTTP Client
22///
23/// A dual-format HTTP client supporting both Protobuf (production) and JSON (debug).
24///
25/// # Example - Protobuf (Production)
26///
27/// ```rust,ignore
28/// use at_jet::prelude::*;
29///
30/// let client = JetClient::new("https://api.example.com")?;
31///
32/// // Type-safe Protobuf requests
33/// let user: User = client.get("/api/user/123").await?;
34/// let created: User = client.post("/api/user", &request).await?;
35/// ```
36///
37/// # Example - JSON (Debug)
38///
39/// ```rust,ignore
40/// use at_jet::prelude::*;
41///
42/// let client = JetClient::builder()
43///     .base_url("https://api.example.com")
44///     .debug_key("dev-debug-key")
45///     .build()?;
46///
47/// // Human-readable JSON requests (for debugging)
48/// let user: User = client.get_json("/api/user/123").await?;
49/// let created: User = client.post_json("/api/user", &request).await?;
50/// ```
51#[derive(Clone)]
52pub struct JetClient {
53  base_url:  String,
54  client:    Client,
55  debug_key: Option<String>,
56}
57
58impl JetClient {
59  /// Create a new JetClient (Protobuf only)
60  ///
61  /// For JSON debug support, use `JetClient::builder().debug_key("key").build()`.
62  pub fn new(base_url: &str) -> Result<Self> {
63    let client = Client::builder().timeout(Duration::from_secs(30)).gzip(true).build()?;
64
65    Ok(Self {
66      base_url: base_url.trim_end_matches('/').to_string(),
67      client,
68      debug_key: None,
69    })
70  }
71
72  /// Create a new JetClient with custom configuration
73  pub fn builder() -> JetClientBuilder {
74    JetClientBuilder::new()
75  }
76
77  // ===========================================================================
78  // Protobuf Methods (Production)
79  // ===========================================================================
80
81  /// Make a GET request and decode Protobuf response
82  pub async fn get<T>(&self, path: &str) -> Result<T>
83  where
84    T: Message + Default, {
85    let url = format!("{}{}", self.base_url, path);
86    debug!("GET {} (protobuf)", url);
87
88    let response = self
89      .client
90      .get(&url)
91      .header("Accept", APPLICATION_PROTOBUF)
92      .send()
93      .await?;
94
95    self.decode_protobuf_response(response).await
96  }
97
98  /// Make a POST request with Protobuf body and decode Protobuf response
99  pub async fn post<Req, Res>(&self, path: &str, body: &Req) -> Result<Res>
100  where
101    Req: Message,
102    Res: Message + Default, {
103    let url = format!("{}{}", self.base_url, path);
104    debug!("POST {} (protobuf)", url);
105
106    let encoded = body.encode_to_vec();
107
108    let response = self
109      .client
110      .post(&url)
111      .header("Content-Type", APPLICATION_PROTOBUF)
112      .header("Accept", APPLICATION_PROTOBUF)
113      .body(encoded)
114      .send()
115      .await?;
116
117    self.decode_protobuf_response(response).await
118  }
119
120  /// Make a PUT request with Protobuf body and decode Protobuf response
121  pub async fn put<Req, Res>(&self, path: &str, body: &Req) -> Result<Res>
122  where
123    Req: Message,
124    Res: Message + Default, {
125    let url = format!("{}{}", self.base_url, path);
126    debug!("PUT {} (protobuf)", url);
127
128    let encoded = body.encode_to_vec();
129
130    let response = self
131      .client
132      .put(&url)
133      .header("Content-Type", APPLICATION_PROTOBUF)
134      .header("Accept", APPLICATION_PROTOBUF)
135      .body(encoded)
136      .send()
137      .await?;
138
139    self.decode_protobuf_response(response).await
140  }
141
142  /// Make a DELETE request and decode Protobuf response
143  pub async fn delete<T>(&self, path: &str) -> Result<T>
144  where
145    T: Message + Default, {
146    let url = format!("{}{}", self.base_url, path);
147    debug!("DELETE {} (protobuf)", url);
148
149    let response = self
150      .client
151      .delete(&url)
152      .header("Accept", APPLICATION_PROTOBUF)
153      .send()
154      .await?;
155
156    self.decode_protobuf_response(response).await
157  }
158
159  /// Make a POST request and return raw bytes
160  pub async fn post_raw(&self, path: &str, body: Bytes) -> Result<Bytes> {
161    let url = format!("{}{}", self.base_url, path);
162    debug!("POST (raw) {}", url);
163
164    let response = self
165      .client
166      .post(&url)
167      .header("Content-Type", APPLICATION_PROTOBUF)
168      .body(body)
169      .send()
170      .await?;
171
172    let bytes = response.bytes().await?;
173    Ok(bytes)
174  }
175
176  // ===========================================================================
177  // JSON Methods (Debug)
178  // ===========================================================================
179
180  /// Make a GET request and decode JSON response (debug format)
181  ///
182  /// Requires debug_key to be configured via builder.
183  pub async fn get_json<T>(&self, path: &str) -> Result<T>
184  where
185    T: DeserializeOwned, {
186    let url = format!("{}{}", self.base_url, path);
187    debug!("GET {} (json)", url);
188
189    let mut request = self.client.get(&url).header("Accept", APPLICATION_JSON);
190
191    if let Some(key) = &self.debug_key {
192      request = request.header(DEBUG_FORMAT_HEADER, key.as_str());
193    }
194
195    let response = request.send().await?;
196    self.decode_json_response(response).await
197  }
198
199  /// Make a POST request with JSON body and decode JSON response (debug format)
200  ///
201  /// Requires debug_key to be configured via builder.
202  pub async fn post_json<Req, Res>(&self, path: &str, body: &Req) -> Result<Res>
203  where
204    Req: Serialize,
205    Res: DeserializeOwned, {
206    let url = format!("{}{}", self.base_url, path);
207    debug!("POST {} (json)", url);
208
209    let json_body = serde_json::to_vec(body)?;
210
211    let mut request = self
212      .client
213      .post(&url)
214      .header("Content-Type", APPLICATION_JSON)
215      .header("Accept", APPLICATION_JSON)
216      .body(json_body);
217
218    if let Some(key) = &self.debug_key {
219      request = request.header(DEBUG_FORMAT_HEADER, key.as_str());
220    }
221
222    let response = request.send().await?;
223    self.decode_json_response(response).await
224  }
225
226  /// Make a PUT request with JSON body and decode JSON response (debug format)
227  pub async fn put_json<Req, Res>(&self, path: &str, body: &Req) -> Result<Res>
228  where
229    Req: Serialize,
230    Res: DeserializeOwned, {
231    let url = format!("{}{}", self.base_url, path);
232    debug!("PUT {} (json)", url);
233
234    let json_body = serde_json::to_vec(body)?;
235
236    let mut request = self
237      .client
238      .put(&url)
239      .header("Content-Type", APPLICATION_JSON)
240      .header("Accept", APPLICATION_JSON)
241      .body(json_body);
242
243    if let Some(key) = &self.debug_key {
244      request = request.header(DEBUG_FORMAT_HEADER, key.as_str());
245    }
246
247    let response = request.send().await?;
248    self.decode_json_response(response).await
249  }
250
251  /// Make a DELETE request and decode JSON response (debug format)
252  pub async fn delete_json<T>(&self, path: &str) -> Result<T>
253  where
254    T: DeserializeOwned, {
255    let url = format!("{}{}", self.base_url, path);
256    debug!("DELETE {} (json)", url);
257
258    let mut request = self.client.delete(&url).header("Accept", APPLICATION_JSON);
259
260    if let Some(key) = &self.debug_key {
261      request = request.header(DEBUG_FORMAT_HEADER, key.as_str());
262    }
263
264    let response = request.send().await?;
265    self.decode_json_response(response).await
266  }
267
268  /// Make a GET request and return raw JSON string (for inspection)
269  pub async fn get_json_raw(&self, path: &str) -> Result<String> {
270    let url = format!("{}{}", self.base_url, path);
271    debug!("GET {} (json raw)", url);
272
273    let mut request = self.client.get(&url).header("Accept", APPLICATION_JSON);
274
275    if let Some(key) = &self.debug_key {
276      request = request.header(DEBUG_FORMAT_HEADER, key.as_str());
277    }
278
279    let response = request.send().await?;
280    let status = response.status();
281
282    if !status.is_success() {
283      let error_text = response.text().await.unwrap_or_default();
284      return Err(JetError::Internal(format!("HTTP {}: {}", status, error_text)));
285    }
286
287    let text = response.text().await?;
288    Ok(text)
289  }
290
291  // ===========================================================================
292  // Internal Helpers
293  // ===========================================================================
294
295  /// Decode Protobuf response
296  async fn decode_protobuf_response<T>(&self, response: Response) -> Result<T>
297  where
298    T: Message + Default, {
299    let status = response.status();
300
301    if !status.is_success() {
302      let error_text = response.text().await.unwrap_or_default();
303      return Err(JetError::Internal(format!("HTTP {}: {}", status, error_text)));
304    }
305
306    let bytes = response.bytes().await?;
307    let decoded = T::decode(bytes)?;
308    Ok(decoded)
309  }
310
311  /// Decode JSON response
312  async fn decode_json_response<T>(&self, response: Response) -> Result<T>
313  where
314    T: DeserializeOwned, {
315    let status = response.status();
316
317    if !status.is_success() {
318      let error_text = response.text().await.unwrap_or_default();
319      return Err(JetError::Internal(format!("HTTP {}: {}", status, error_text)));
320    }
321
322    let bytes = response.bytes().await?;
323    let decoded = serde_json::from_slice(&bytes)?;
324    Ok(decoded)
325  }
326}
327
328/// Builder for JetClient
329pub struct JetClientBuilder {
330  base_url:  Option<String>,
331  timeout:   Duration,
332  gzip:      bool,
333  debug_key: Option<String>,
334}
335
336impl JetClientBuilder {
337  fn new() -> Self {
338    Self {
339      base_url:  None,
340      timeout:   Duration::from_secs(30),
341      gzip:      true,
342      debug_key: None,
343    }
344  }
345
346  /// Set base URL
347  pub fn base_url(mut self, url: &str) -> Self {
348    self.base_url = Some(url.trim_end_matches('/').to_string());
349    self
350  }
351
352  /// Set request timeout
353  pub fn timeout(mut self, timeout: Duration) -> Self {
354    self.timeout = timeout;
355    self
356  }
357
358  /// Enable/disable gzip compression
359  pub fn gzip(mut self, enabled: bool) -> Self {
360    self.gzip = enabled;
361    self
362  }
363
364  /// Set debug key for JSON format requests
365  ///
366  /// This key will be sent as `X-Debug-Format` header when using
367  /// `get_json`, `post_json`, etc. methods.
368  pub fn debug_key(mut self, key: &str) -> Self {
369    self.debug_key = Some(key.to_string());
370    self
371  }
372
373  /// Build the client
374  pub fn build(self) -> Result<JetClient> {
375    let base_url = self
376      .base_url
377      .ok_or_else(|| JetError::Internal("Base URL is required".to_string()))?;
378
379    let client = Client::builder().timeout(self.timeout).gzip(self.gzip).build()?;
380
381    Ok(JetClient {
382      base_url,
383      client,
384      debug_key: self.debug_key,
385    })
386  }
387}