at-jet 0.7.2

High-performance HTTP + Protobuf API framework for mobile services
Documentation
//! HTTP Client for AT-Jet
//!
//! Provides a dual-format HTTP client for calling AT-Jet services.
//! - Protobuf: Production format (default, efficient)
//! - JSON: Debug format (requires debug key)

use {crate::{content_types::{APPLICATION_JSON,
                             APPLICATION_PROTOBUF},
             dual_format::DEBUG_FORMAT_HEADER,
             error::{JetError,
                     Result}},
     bytes::Bytes,
     prost::Message,
     reqwest::{Client,
               Response},
     serde::{Serialize,
             de::DeserializeOwned},
     std::time::Duration,
     tracing::debug};

/// AT-Jet HTTP Client
///
/// A dual-format HTTP client supporting both Protobuf (production) and JSON (debug).
///
/// # Example - Protobuf (Production)
///
/// ```rust,ignore
/// use at_jet::prelude::*;
///
/// let client = JetClient::new("https://api.example.com")?;
///
/// // Type-safe Protobuf requests
/// let user: User = client.get("/api/user/123").await?;
/// let created: User = client.post("/api/user", &request).await?;
/// ```
///
/// # Example - JSON (Debug)
///
/// ```rust,ignore
/// use at_jet::prelude::*;
///
/// let client = JetClient::builder()
///     .base_url("https://api.example.com")
///     .debug_key("dev-debug-key")
///     .build()?;
///
/// // Human-readable JSON requests (for debugging)
/// let user: User = client.get_json("/api/user/123").await?;
/// let created: User = client.post_json("/api/user", &request).await?;
/// ```
#[derive(Clone)]
pub struct JetClient {
  base_url:  String,
  client:    Client,
  debug_key: Option<String>,
}

impl JetClient {
  /// Create a new JetClient (Protobuf only)
  ///
  /// For JSON debug support, use `JetClient::builder().debug_key("key").build()`.
  pub fn new(base_url: &str) -> Result<Self> {
    let client = Client::builder().timeout(Duration::from_secs(30)).gzip(true).build()?;

    Ok(Self {
      base_url: base_url.trim_end_matches('/').to_string(),
      client,
      debug_key: None,
    })
  }

  /// Create a new JetClient with custom configuration
  pub fn builder() -> JetClientBuilder {
    JetClientBuilder::new()
  }

  // ===========================================================================
  // Protobuf Methods (Production)
  // ===========================================================================

  /// Make a GET request and decode Protobuf response
  pub async fn get<T>(&self, path: &str) -> Result<T>
  where
    T: Message + Default, {
    let url = format!("{}{}", self.base_url, path);
    debug!("GET {} (protobuf)", url);

    let response = self
      .client
      .get(&url)
      .header("Accept", APPLICATION_PROTOBUF)
      .send()
      .await?;

    self.decode_protobuf_response(response).await
  }

  /// Make a POST request with Protobuf body and decode Protobuf response
  pub async fn post<Req, Res>(&self, path: &str, body: &Req) -> Result<Res>
  where
    Req: Message,
    Res: Message + Default, {
    let url = format!("{}{}", self.base_url, path);
    debug!("POST {} (protobuf)", url);

    let encoded = body.encode_to_vec();

    let response = self
      .client
      .post(&url)
      .header("Content-Type", APPLICATION_PROTOBUF)
      .header("Accept", APPLICATION_PROTOBUF)
      .body(encoded)
      .send()
      .await?;

    self.decode_protobuf_response(response).await
  }

  /// Make a PUT request with Protobuf body and decode Protobuf response
  pub async fn put<Req, Res>(&self, path: &str, body: &Req) -> Result<Res>
  where
    Req: Message,
    Res: Message + Default, {
    let url = format!("{}{}", self.base_url, path);
    debug!("PUT {} (protobuf)", url);

    let encoded = body.encode_to_vec();

    let response = self
      .client
      .put(&url)
      .header("Content-Type", APPLICATION_PROTOBUF)
      .header("Accept", APPLICATION_PROTOBUF)
      .body(encoded)
      .send()
      .await?;

    self.decode_protobuf_response(response).await
  }

  /// Make a DELETE request and decode Protobuf response
  pub async fn delete<T>(&self, path: &str) -> Result<T>
  where
    T: Message + Default, {
    let url = format!("{}{}", self.base_url, path);
    debug!("DELETE {} (protobuf)", url);

    let response = self
      .client
      .delete(&url)
      .header("Accept", APPLICATION_PROTOBUF)
      .send()
      .await?;

    self.decode_protobuf_response(response).await
  }

  /// Make a POST request and return raw bytes
  pub async fn post_raw(&self, path: &str, body: Bytes) -> Result<Bytes> {
    let url = format!("{}{}", self.base_url, path);
    debug!("POST (raw) {}", url);

    let response = self
      .client
      .post(&url)
      .header("Content-Type", APPLICATION_PROTOBUF)
      .body(body)
      .send()
      .await?;

    let bytes = response.bytes().await?;
    Ok(bytes)
  }

  // ===========================================================================
  // JSON Methods (Debug)
  // ===========================================================================

  /// Make a GET request and decode JSON response (debug format)
  ///
  /// Requires debug_key to be configured via builder.
  pub async fn get_json<T>(&self, path: &str) -> Result<T>
  where
    T: DeserializeOwned, {
    let url = format!("{}{}", self.base_url, path);
    debug!("GET {} (json)", url);

    let mut request = self.client.get(&url).header("Accept", APPLICATION_JSON);

    if let Some(key) = &self.debug_key {
      request = request.header(DEBUG_FORMAT_HEADER, key.as_str());
    }

    let response = request.send().await?;
    self.decode_json_response(response).await
  }

  /// Make a POST request with JSON body and decode JSON response (debug format)
  ///
  /// Requires debug_key to be configured via builder.
  pub async fn post_json<Req, Res>(&self, path: &str, body: &Req) -> Result<Res>
  where
    Req: Serialize,
    Res: DeserializeOwned, {
    let url = format!("{}{}", self.base_url, path);
    debug!("POST {} (json)", url);

    let json_body = serde_json::to_vec(body)?;

    let mut request = self
      .client
      .post(&url)
      .header("Content-Type", APPLICATION_JSON)
      .header("Accept", APPLICATION_JSON)
      .body(json_body);

    if let Some(key) = &self.debug_key {
      request = request.header(DEBUG_FORMAT_HEADER, key.as_str());
    }

    let response = request.send().await?;
    self.decode_json_response(response).await
  }

  /// Make a PUT request with JSON body and decode JSON response (debug format)
  pub async fn put_json<Req, Res>(&self, path: &str, body: &Req) -> Result<Res>
  where
    Req: Serialize,
    Res: DeserializeOwned, {
    let url = format!("{}{}", self.base_url, path);
    debug!("PUT {} (json)", url);

    let json_body = serde_json::to_vec(body)?;

    let mut request = self
      .client
      .put(&url)
      .header("Content-Type", APPLICATION_JSON)
      .header("Accept", APPLICATION_JSON)
      .body(json_body);

    if let Some(key) = &self.debug_key {
      request = request.header(DEBUG_FORMAT_HEADER, key.as_str());
    }

    let response = request.send().await?;
    self.decode_json_response(response).await
  }

  /// Make a DELETE request and decode JSON response (debug format)
  pub async fn delete_json<T>(&self, path: &str) -> Result<T>
  where
    T: DeserializeOwned, {
    let url = format!("{}{}", self.base_url, path);
    debug!("DELETE {} (json)", url);

    let mut request = self.client.delete(&url).header("Accept", APPLICATION_JSON);

    if let Some(key) = &self.debug_key {
      request = request.header(DEBUG_FORMAT_HEADER, key.as_str());
    }

    let response = request.send().await?;
    self.decode_json_response(response).await
  }

  /// Make a GET request and return raw JSON string (for inspection)
  pub async fn get_json_raw(&self, path: &str) -> Result<String> {
    let url = format!("{}{}", self.base_url, path);
    debug!("GET {} (json raw)", url);

    let mut request = self.client.get(&url).header("Accept", APPLICATION_JSON);

    if let Some(key) = &self.debug_key {
      request = request.header(DEBUG_FORMAT_HEADER, key.as_str());
    }

    let response = request.send().await?;
    let status = response.status();

    if !status.is_success() {
      let error_text = response.text().await.unwrap_or_default();
      return Err(JetError::Internal(format!("HTTP {}: {}", status, error_text)));
    }

    let text = response.text().await?;
    Ok(text)
  }

  // ===========================================================================
  // Internal Helpers
  // ===========================================================================

  /// Decode Protobuf response
  async fn decode_protobuf_response<T>(&self, response: Response) -> Result<T>
  where
    T: Message + Default, {
    let status = response.status();

    if !status.is_success() {
      let error_text = response.text().await.unwrap_or_default();
      return Err(JetError::Internal(format!("HTTP {}: {}", status, error_text)));
    }

    let bytes = response.bytes().await?;
    let decoded = T::decode(bytes)?;
    Ok(decoded)
  }

  /// Decode JSON response
  async fn decode_json_response<T>(&self, response: Response) -> Result<T>
  where
    T: DeserializeOwned, {
    let status = response.status();

    if !status.is_success() {
      let error_text = response.text().await.unwrap_or_default();
      return Err(JetError::Internal(format!("HTTP {}: {}", status, error_text)));
    }

    let bytes = response.bytes().await?;
    let decoded = serde_json::from_slice(&bytes)?;
    Ok(decoded)
  }
}

/// Builder for JetClient
pub struct JetClientBuilder {
  base_url:  Option<String>,
  timeout:   Duration,
  gzip:      bool,
  debug_key: Option<String>,
}

impl JetClientBuilder {
  fn new() -> Self {
    Self {
      base_url:  None,
      timeout:   Duration::from_secs(30),
      gzip:      true,
      debug_key: None,
    }
  }

  /// Set base URL
  pub fn base_url(mut self, url: &str) -> Self {
    self.base_url = Some(url.trim_end_matches('/').to_string());
    self
  }

  /// Set request timeout
  pub fn timeout(mut self, timeout: Duration) -> Self {
    self.timeout = timeout;
    self
  }

  /// Enable/disable gzip compression
  pub fn gzip(mut self, enabled: bool) -> Self {
    self.gzip = enabled;
    self
  }

  /// Set debug key for JSON format requests
  ///
  /// This key will be sent as `X-Debug-Format` header when using
  /// `get_json`, `post_json`, etc. methods.
  pub fn debug_key(mut self, key: &str) -> Self {
    self.debug_key = Some(key.to_string());
    self
  }

  /// Build the client
  pub fn build(self) -> Result<JetClient> {
    let base_url = self
      .base_url
      .ok_or_else(|| JetError::Internal("Base URL is required".to_string()))?;

    let client = Client::builder().timeout(self.timeout).gzip(self.gzip).build()?;

    Ok(JetClient {
      base_url,
      client,
      debug_key: self.debug_key,
    })
  }
}