restcrab 0.2.0

Procedural macro to automatically generate a REST client from a trait definition.
Documentation
use std::str::FromStr;

use snafu::prelude::*;

pub struct Options {
  pub base_url: http::Uri,
}
pub struct Reqwest {
  options: Options,
  client: reqwest_lib::blocking::Client,
}

impl crate::Restcrab for Reqwest {
  type Error = Error;
  type Options = Options;
  type Crab = Reqwest;

  fn options(&self) -> &Self::Options {
    &self.options
  }

  fn options_mut(&mut self) -> &mut Self::Options {
    &mut self.options
  }

  fn from_options(options: Options) -> Result<Reqwest, Error> {
    Ok(Reqwest {
      options,
      client: reqwest_lib::blocking::Client::new(),
    })
  }

  fn call<REQ: serde::Serialize, RES: for<'de> serde::Deserialize<'de>>(&self, request: crate::Request<REQ>) -> Result<Option<RES>, Self::Error> {
    let url: http::Uri = if request.url.host().is_some() && request.url.scheme().is_some() {
      request.url.to_owned()
    } else {
      let mut base_parts = http::uri::Parts::from(self.options.base_url.clone());
      let parts = request.url.to_owned().into_parts();

      if parts.scheme.is_some() {
        base_parts.scheme = parts.scheme;
      }

      if parts.authority.is_some() {
        base_parts.authority = parts.authority;
      }

      if let Some(path_and_query) = parts.path_and_query {
        let mut path = path_and_query.path().to_string();
        if !path.starts_with('/') {
          let base_path = if let Some(path_and_query) = base_parts.path_and_query {
            path_and_query.path().to_owned()
          } else {
            "/".to_string()
          };

          if !path.ends_with('/') {
            path += "/";
          }

          path = base_path + &path;
        }

        base_parts.path_and_query = Some(http::uri::PathAndQuery::from_str(&(path + path_and_query.query().unwrap_or_default())).map_err(|source| Error::ConstructingUrl { source })?);
      }

      http::Uri::from_parts(base_parts)?
    };

    let mut req_builder = match &request.method {
      &http::Method::HEAD => self.client.head(url.to_string()),
      &http::Method::GET => self.client.get(url.to_string()),
      &http::Method::POST => self.client.post(url.to_string()),
      &http::Method::PUT => self.client.put(url.to_string()),
      &http::Method::PATCH => self.client.patch(url.to_string()),
      &http::Method::DELETE => self.client.delete(url.to_string()),
      &http::Method::OPTIONS => self.client.request(reqwest_lib::Method::OPTIONS, url.to_string()),
      &http::Method::CONNECT => self.client.request(reqwest_lib::Method::CONNECT, url.to_string()),
      &http::Method::TRACE => self.client.request(reqwest_lib::Method::TRACE, url.to_string()),
      method => return Err(Error::InvalidMethod { method: method.clone() }),
    };

    for (key, value) in &request.headers {
      req_builder = req_builder.header(key, value);
    }

    req_builder = req_builder.query(&request.queries.iter().collect::<Vec<_>>());

    if let Some(body) = &request.body {
      req_builder = req_builder.body(serde_json::to_string(body).context(SerializingBodySnafu)?);
    }

    let response = req_builder.send().context(SendingRequestSnafu)?;

    ensure!(response.status().is_success(), UnsuccessfulResponseCodeSnafu { response });

    let text = response.text().context(DecodingResponseBodySnafu)?;

    if !text.is_empty() {
      serde_json::from_str::<RES>(text.as_str()).map(Some).context(DeserializingBodySnafu)
    } else {
      Ok(None)
    }
  }
}

#[derive(Debug, snafu::Snafu)]
pub enum Error {
  #[snafu(display("Error parsing url: {source}"), context(false))]
  ParsingUrl { source: http::uri::InvalidUriParts },

  #[snafu(display("Error serializing body: {source}"))]
  SerializingBody { source: serde_json::Error },

  #[snafu(display("Error deserializing body: {source}"))]
  DeserializingBody { source: serde_json::Error },

  #[snafu(display("Error sending request: {source}"))]
  SendingRequest { source: reqwest_lib::Error },

  #[snafu(display("Unsuccessful response code: {response:?}"))]
  UnsuccessfulResponseCode { response: reqwest_lib::blocking::Response },

  #[snafu(display("Error converting response body to text: {source}"))]
  DecodingResponseBody { source: reqwest_lib::Error },

  #[snafu(display("Invalid method: {method}"))]
  InvalidMethod { method: http::Method },

  #[snafu(display("Error constructing url: {source}"))]
  ConstructingUrl { source: http::uri::InvalidUri },

  #[snafu(context(false))]
  Restcrab { source: crate::Error },
}