acari-lib 0.1.8

Pragmatic client for the mite timetracking API
Documentation
use crate::error::AcariError;
use crate::model::{Account, Customer, Minutes, MiteEntity, Project, ProjectId, Service, ServiceId, TimeEntry, TimeEntryId, Tracker, User};
use crate::query::{DateSpan, Day};
use crate::Client;
use reqwest::Method;
use serde::de::DeserializeOwned;
use serde::ser::Serialize;
use serde_json::json;
use url::Url;

use reqwest::{blocking, header, StatusCode};

const USER_AGENT: &str = "acari-lib (https://github.com/untoldwind/acari)";

#[derive(Debug)]
pub struct StdClient {
  base_url: Url,
  client: blocking::Client,
}

impl StdClient {
  pub fn new(domain: &str, token: &str) -> Result<StdClient, AcariError> {
    Ok(Self::new_form_url(format!("https://{}@{}", token, domain).parse()?))
  }

  pub fn new_form_url(base_url: Url) -> StdClient {
    StdClient {
      base_url,
      client: blocking::Client::new(),
    }
  }

  fn base_request(&self, method: Method, uri: &str) -> Result<blocking::RequestBuilder, AcariError> {
    Ok(
      self
        .client
        .request(method, self.base_url.join(uri)?.as_str())
        .header(header::USER_AGENT, USER_AGENT)
        .header(header::HOST, self.base_url.host_str().unwrap_or(""))
        .header("X-MiteApiKey", self.base_url.username()),
    )
  }

  fn request<T: DeserializeOwned>(&self, method: Method, uri: &str) -> Result<T, AcariError> {
    let response = self.base_request(method, uri)?.send()?;

    handle_response(response)
  }

  fn request_empty(&self, method: Method, uri: &str) -> Result<(), AcariError> {
    let response = self.base_request(method, uri)?.send()?;

    handle_empty_response(response)
  }

  fn request_with_body<T: DeserializeOwned, D: Serialize>(&self, method: Method, uri: &str, data: D) -> Result<T, AcariError> {
    let response = self.base_request(method, uri)?.json(&data).send()?;

    handle_response(response)
  }

  fn request_empty_with_body<D: Serialize>(&self, method: Method, uri: &str, data: D) -> Result<(), AcariError> {
    let response = self.base_request(method, uri)?.json(&data).send()?;

    handle_empty_response(response)
  }
}

impl Client for StdClient {
  fn get_account(&self) -> Result<Account, AcariError> {
    match self.request(Method::GET, "/account.json")? {
      MiteEntity::Account(account) => Ok(account),
      response => Err(AcariError::Mite(400, format!("Unexpected response: {:?}", response))),
    }
  }

  fn get_myself(&self) -> Result<User, AcariError> {
    match self.request(Method::GET, "/myself.json")? {
      MiteEntity::User(user) => Ok(user),
      response => Err(AcariError::Mite(400, format!("Unexpected response: {:?}", response))),
    }
  }

  fn get_customers(&self) -> Result<Vec<Customer>, AcariError> {
    Ok(
      self
        .request::<Vec<MiteEntity>>(Method::GET, "/customers.json")?
        .into_iter()
        .filter_map(|entity| match entity {
          MiteEntity::Customer(customer) => Some(customer),
          _ => None,
        })
        .collect(),
    )
  }

  fn get_projects(&self) -> Result<Vec<Project>, AcariError> {
    Ok(
      self
        .request::<Vec<MiteEntity>>(Method::GET, "/projects.json")?
        .into_iter()
        .filter_map(|entity| match entity {
          MiteEntity::Project(project) => Some(project),
          _ => None,
        })
        .collect(),
    )
  }

  fn get_services(&self) -> Result<Vec<Service>, AcariError> {
    Ok(
      self
        .request::<Vec<MiteEntity>>(Method::GET, "/services.json")?
        .into_iter()
        .filter_map(|entity| match entity {
          MiteEntity::Service(service) => Some(service),
          _ => None,
        })
        .collect(),
    )
  }

  fn get_time_entry(&self, entry_id: TimeEntryId) -> Result<TimeEntry, AcariError> {
    match self.request(Method::GET, &format!("/time_entries/{}.json", entry_id))? {
      MiteEntity::TimeEntry(time_entry) => Ok(time_entry),
      response => Err(AcariError::Mite(400, format!("Unexpected response: {:?}", response))),
    }
  }

  fn get_time_entries(&self, date_span: DateSpan) -> Result<Vec<TimeEntry>, AcariError> {
    Ok(
      self
        .request::<Vec<MiteEntity>>(Method::GET, &format!("/time_entries.json?user=current&{}", date_span.query_param()))?
        .into_iter()
        .filter_map(|entity| match entity {
          MiteEntity::TimeEntry(time_entry) => Some(time_entry),
          _ => None,
        })
        .collect(),
    )
  }

  fn create_time_entry(&self, day: Day, project_id: ProjectId, service_id: ServiceId, minutes: Minutes) -> Result<TimeEntry, AcariError> {
    match self.request_with_body(
      Method::POST,
      "/time_entries.json",
      json!({
        "time_entry": {
          "date_at": day.as_date(),
          "project_id": project_id,
          "service_id": service_id,
          "minutes": minutes,
        }
      }),
    )? {
      MiteEntity::TimeEntry(time_entry) => Ok(time_entry),
      response => Err(AcariError::Mite(400, format!("Unexpected response: {:?}", response))),
    }
  }

  fn update_time_entry(&self, entry_id: TimeEntryId, minutes: Minutes) -> Result<(), AcariError> {
    self.request_empty_with_body(
      Method::PATCH,
      &format!("/time_entries/{}.json", entry_id),
      json!({
        "time_entry": {
          "minutes": minutes,
        }
      }),
    )
  }

  fn delete_time_entry(&self, entry_id: TimeEntryId) -> Result<(), AcariError> {
    self.request_empty(Method::DELETE, &format!("/time_entries/{}.json", entry_id))
  }

  fn get_tracker(&self) -> Result<Tracker, AcariError> {
    match self.request(Method::GET, "/tracker.json")? {
      MiteEntity::Tracker(tracker) => Ok(tracker),
      response => Err(AcariError::Mite(400, format!("Unexpected response: {:?}", response))),
    }
  }

  fn create_tracker(&self, entry_id: TimeEntryId) -> Result<Tracker, AcariError> {
    match self.request(Method::PATCH, &format!("/tracker/{}.json", entry_id))? {
      MiteEntity::Tracker(tracker) => Ok(tracker),
      response => Err(AcariError::Mite(400, format!("Unexpected response: {:?}", response))),
    }
  }

  fn delete_tracker(&self, entry_id: TimeEntryId) -> Result<Tracker, AcariError> {
    match self.request(Method::DELETE, &format!("/tracker/{}.json", entry_id))? {
      MiteEntity::Tracker(tracker) => Ok(tracker),
      response => Err(AcariError::Mite(400, format!("Unexpected response: {:?}", response))),
    }
  }
}

fn handle_empty_response(response: blocking::Response) -> Result<(), AcariError> {
  match response.status() {
    StatusCode::OK | StatusCode::CREATED => Ok(()),
    status => match response.json::<MiteEntity>() {
      Ok(MiteEntity::Error(msg)) => Err(AcariError::Mite(status.as_u16(), msg)),
      _ => Err(AcariError::Mite(status.as_u16(), status.to_string())),
    },
  }
}

fn handle_response<T: DeserializeOwned>(response: blocking::Response) -> Result<T, AcariError> {
  match response.status() {
    StatusCode::OK | StatusCode::CREATED => Ok(response.json()?),
    status => match response.json::<MiteEntity>() {
      Ok(MiteEntity::Error(msg)) => Err(AcariError::Mite(status.as_u16(), msg)),
      _ => Err(AcariError::Mite(status.as_u16(), status.to_string())),
    },
  }
}