acari-lib 0.1.8

Pragmatic client for the mite timetracking API
Documentation
use crate::error::AcariError;
use crate::user_error;
use chrono::{DateTime, NaiveDate, Utc};
use serde::{Deserialize, Serialize};
use std::fmt;
use std::ops;
use std::str::FromStr;

macro_rules! id_wrapper {
  ($name: ident) => {
    #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy)]
    #[serde(transparent)]
    pub struct $name(pub u32);

    impl fmt::Display for $name {
      fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{}", self.0)
      }
    }
  };
}

id_wrapper!(AccountId);
id_wrapper!(UserId);
id_wrapper!(CustomerId);
id_wrapper!(ProjectId);
id_wrapper!(ServiceId);
id_wrapper!(TimeEntryId);

#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
pub struct Account {
  pub id: AccountId,
  pub name: String,
  pub title: String,
  pub currency: String,
  pub created_at: DateTime<Utc>,
  pub updated_at: DateTime<Utc>,
}

#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
pub struct User {
  pub id: UserId,
  pub name: String,
  pub email: String,
  pub note: String,
  pub role: String,
  pub language: String,
  pub archived: bool,
  pub created_at: DateTime<Utc>,
  pub updated_at: DateTime<Utc>,
}

#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
pub struct Customer {
  pub id: CustomerId,
  pub name: String,
  pub note: String,
  pub hourly_rate: Option<u32>,
  pub archived: bool,
  pub created_at: DateTime<Utc>,
  pub updated_at: DateTime<Utc>,
}

#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
pub struct Project {
  pub id: ProjectId,
  pub name: String,
  pub customer_id: CustomerId,
  pub customer_name: String,
  pub note: String,
  pub budget: u32,
  pub budget_type: String,
  pub hourly_rate: Option<u32>,
  pub archived: bool,
  pub created_at: DateTime<Utc>,
  pub updated_at: DateTime<Utc>,
}

#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
pub struct Service {
  pub id: ServiceId,
  pub name: String,
  pub note: String,
  pub hourly_rate: Option<u32>,
  pub billable: bool,
  pub archived: bool,
  pub created_at: DateTime<Utc>,
  pub updated_at: DateTime<Utc>,
}

#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy, Default)]
pub struct Minutes(pub u32);

impl fmt::Display for Minutes {
  fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
    write!(f, "{}:{:02}", self.0 / 60, self.0 % 60)
  }
}

impl ops::Add for Minutes {
  type Output = Minutes;
  fn add(self, rhs: Minutes) -> Self::Output {
    Minutes(self.0 + rhs.0)
  }
}

impl std::iter::Sum<Minutes> for Minutes {
  fn sum<I: Iterator<Item = Minutes>>(iter: I) -> Self {
    Minutes(iter.map(|m| m.0).sum())
  }
}

impl FromStr for Minutes {
  type Err = AcariError;

  fn from_str(s: &str) -> Result<Self, Self::Err> {
    match s.find(':') {
      Some(idx) => {
        let hours = s[..idx].parse::<u32>().map_err(|e| user_error!("Invalid time format: {}", e))?;
        let minutes = s[idx + 1..].parse::<u32>().map_err(|e| user_error!("Invalid time format: {}", e))?;

        if minutes >= 60 {
          Err(AcariError::UserError("No more than 60 minutes per hour".to_string()))
        } else if hours >= 24 {
          Err(AcariError::UserError("No more than 24 hour per day".to_string()))
        } else {
          Ok(Minutes(hours * 60 + minutes))
        }
      }
      None => Ok(Minutes(s.parse::<u32>().map_err(|e| user_error!("Invalid time format: {}", e))?)),
    }
  }
}

#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
pub struct TimeEntry {
  pub id: TimeEntryId,
  pub date_at: NaiveDate,
  pub minutes: Minutes,
  pub customer_id: CustomerId,
  pub customer_name: String,
  pub project_id: ProjectId,
  pub project_name: String,
  pub service_id: ServiceId,
  pub service_name: String,
  pub user_id: UserId,
  pub user_name: String,
  pub note: String,
  pub billable: bool,
  pub locked: bool,
  pub hourly_rate: u32,
  pub created_at: DateTime<Utc>,
  pub updated_at: DateTime<Utc>,
}

#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
pub struct TrackingTimeEntry {
  pub id: TimeEntryId,
  pub minutes: Minutes,
  pub since: Option<DateTime<Utc>>,
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct Tracker {
  pub tracking_time_entry: Option<TrackingTimeEntry>,
  pub stopped_time_entry: Option<TrackingTimeEntry>,
}

#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "snake_case")]
pub enum MiteEntity {
  Account(Account),
  User(User),
  Customer(Customer),
  Project(Project),
  Service(Service),
  TimeEntry(TimeEntry),
  Tracker(Tracker),
  Error(String),
}

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

  #[test]
  fn test_parse_minutes() -> Result<(), Box<dyn std::error::Error>> {
    assert_eq!("123".parse::<Minutes>()?, Minutes(123));
    assert_eq!("0:40".parse::<Minutes>()?, Minutes(40));
    assert_eq!("5:35".parse::<Minutes>()?, Minutes(5 * 60 + 35));

    Ok(())
  }
}