jet 0.2.3

Jet API Client for Rust
Documentation
use chrono::{DateTime, Duration, Utc};
use error::*;
use reqwest;
pub use reqwest::{Method, RequestBuilder, Response, StatusCode};
use serde::de::DeserializeOwned;
use std::cell::RefCell;
use std::io::Read;
use std::sync::Mutex;

const ENDPOINT: &'static str = "https://merchant-api.jet.com/api";

#[derive(Debug, Deserialize)]
pub(crate) struct Token {
  id_token: String,
  token_type: String,
  expires_on: DateTime<Utc>,
}

pub struct ClientOptions {
  pub api_user: String,
  pub secret: String,
  pub merchant_id: String,
}

pub struct Client {
  options: ClientOptions,
  token: Mutex<RefCell<Option<Token>>>,
  client: reqwest::Client,
}

impl Client {
  pub fn new(opts: ClientOptions) -> Result<Client> {
    Ok(Client {
      options: opts,
      token: Mutex::new(RefCell::new(None)),
      client: reqwest::Client::new(),
    })
  }

  pub fn with_http_client(opts: ClientOptions, http_client: reqwest::Client) -> Client {
    Client {
      options: opts,
      token: Mutex::new(RefCell::new(None)),
      client: http_client,
    }
  }

  pub(crate) fn with_token<T, F>(&self, f: F) -> Result<T>
  where
    F: FnOnce(&Token) -> Result<T>,
  {
    let guard = self.token.lock().expect("lock token");
    let token: &mut Option<Token> = &mut guard.borrow_mut();
    match *token {
      Some(ref token) if token.expires_on - Duration::minutes(15) >= Utc::now() => f(&token),
      _ => {
        use std::mem::replace;
        replace(token, Some(self.get_token()?));
        f(&token.as_ref().unwrap())
      }
    }
  }

  fn get_token(&self) -> Result<Token> {
    #[derive(Serialize)]
    pub struct TokenRequest<'a> {
      pub user: &'a str,
      pub pass: &'a str,
    }

    let mut res = self
      .client
      .post(&format!("{}/token", ENDPOINT))
      .json(&TokenRequest {
        user: &self.options.api_user,
        pass: &self.options.secret,
      })
      .send()?;

    if !res.status().is_success() {
      let mut body = String::new();
      res.read_to_string(&mut body)?;
      return Err(ErrorKind::GetTokenRequest(res.status(), body).into());
    }

    res.json().chain_err(|| ErrorKind::InvalidResponse)
  }

  pub(crate) fn request<T, F>(&self, method: Method, path: &str, f: F) -> Result<T>
  where
    T: DeserializeOwned,
    F: FnOnce(&mut RequestBuilder) -> Result<()>,
  {
    use reqwest::header::{Authorization, Bearer};

    let mut req = self.with_token(|token| -> Result<RequestBuilder> {
      let mut req = self
        .client
        .request(method, &format!("{}{}", ENDPOINT, path));
      req.header(Authorization(Bearer {
        token: token.id_token.clone(),
      }));
      Ok(req)
    })?;

    f(&mut req)?;

    let mut res = req.send()?;

    if !res.status().is_success() {
      let mut body = String::new();
      res.read_to_string(&mut body)?;
      return Err(ErrorKind::Request(path.to_owned(), res.status(), body).into());
    }

    res.json().chain_err(|| ErrorKind::InvalidResponse)
  }

  pub(crate) fn request_no_content<F>(&self, method: Method, path: &str, f: F) -> Result<()>
  where
    F: FnOnce(&mut RequestBuilder) -> Result<()>,
  {
    use reqwest::header::{Authorization, Bearer};

    let mut req = self.with_token(|token| -> Result<RequestBuilder> {
      let mut req = self
        .client
        .request(method, &format!("{}{}", ENDPOINT, path));
      req.header(Authorization(Bearer {
        token: token.id_token.clone(),
      }));
      Ok(req)
    })?;

    f(&mut req)?;

    let mut res = req.send()?;

    if !res.status().is_success() {
      let mut body = String::new();
      res.read_to_string(&mut body)?;
      return Err(ErrorKind::Request(path.to_owned(), res.status(), body).into());
    }

    Ok(())
  }
}

#[cfg(test)]
pub(crate) fn get_test_client() -> Client {
  use dotenv::dotenv;
  use std::env;
  dotenv().ok();

  Client::new(ClientOptions {
    api_user: env::var("API_USER").unwrap(),
    secret: env::var("SECRET").unwrap(),
    merchant_id: env::var("MERCHANT_ID").unwrap(),
  }).unwrap()
}

#[cfg(test)]
mod tests {
  use super::*;

  #[test]
  fn test_with_token() {
    let client = get_test_client();
    let mut last_token: Option<String> = None;
    client
      .with_token(|token| -> Result<()> {
        last_token = Some(token.id_token.clone());
        Ok(())
      })
      .unwrap();

    client
      .with_token(|token| -> Result<()> {
        assert_eq!(&token.id_token, last_token.as_ref().unwrap());
        Ok(())
      })
      .unwrap();
  }
}