acari_lib/
model.rs

1use crate::error::AcariError;
2use crate::user_error;
3use chrono::{DateTime, NaiveDate, Utc};
4use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
5use serde::de::{Error, Visitor};
6use serde::{Deserialize, Deserializer, Serialize, Serializer};
7use std::fmt;
8use std::ops;
9use std::str::FromStr;
10
11macro_rules! id_wrapper {
12  ($name: ident) => {
13    #[derive(Debug, PartialEq, Eq, Clone, Hash)]
14    pub enum $name {
15      Num(u64),
16      Str(String),
17    }
18
19    impl $name {
20      pub fn str_encoded(&self) -> String {
21        match self {
22          $name::Num(n) => format!("n{}", n),
23          $name::Str(s) => format!("s{}", s),
24        }
25      }
26
27      pub fn parse_encoded(s: &str) -> Result<$name, AcariError> {
28        match s.chars().next() {
29          Some('n') => Ok($name::Num(s[1..].parse::<u64>()?)),
30          Some('s') => Ok($name::Str(s[1..].to_string())),
31          _ => Err(AcariError::InternalError("Invalid id format".to_string())),
32        }
33      }
34
35      pub fn path_encoded(&self) -> String {
36        match self {
37          $name::Num(n) => n.to_string(),
38          $name::Str(s) => utf8_percent_encode(&s, NON_ALPHANUMERIC).to_string(),
39        }
40      }
41    }
42
43    impl Default for $name {
44      fn default() -> Self {
45        $name::Num(0)
46      }
47    }
48
49    impl Serialize for $name {
50      fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
51      where
52        S: Serializer,
53      {
54        match self {
55          $name::Num(n) => serializer.serialize_u64(*n),
56          $name::Str(s) => serializer.serialize_str(s),
57        }
58      }
59    }
60
61    impl<'de> Deserialize<'de> for $name {
62      fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
63      where
64        D: Deserializer<'de>,
65      {
66        struct EnumVisitor;
67
68        impl<'de> Visitor<'de> for EnumVisitor {
69          type Value = $name;
70
71          fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
72            formatter.write_str("integer or string")
73          }
74
75          fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
76          where
77            E: Error,
78          {
79            Ok($name::Num(v))
80          }
81
82          fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
83          where
84            E: Error,
85          {
86            Ok($name::Str(v.to_string()))
87          }
88        }
89
90        deserializer.deserialize_any(EnumVisitor)
91      }
92    }
93
94    impl fmt::Display for $name {
95      fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
96        match self {
97          $name::Num(n) => write!(f, "{}", n),
98          $name::Str(s) => write!(f, "{}", s),
99        }
100      }
101    }
102  };
103}
104
105id_wrapper!(AccountId);
106id_wrapper!(UserId);
107id_wrapper!(CustomerId);
108id_wrapper!(ProjectId);
109id_wrapper!(ServiceId);
110id_wrapper!(TimeEntryId);
111
112#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
113pub struct Account {
114  pub id: AccountId,
115  pub name: String,
116  pub title: String,
117  pub currency: String,
118  pub created_at: DateTime<Utc>,
119}
120
121#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
122pub struct User {
123  pub id: UserId,
124  pub name: String,
125  pub email: String,
126  pub note: String,
127  pub role: String,
128  pub language: String,
129  pub archived: bool,
130  pub created_at: DateTime<Utc>,
131}
132
133#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
134pub struct Customer {
135  pub id: CustomerId,
136  pub name: String,
137  pub note: String,
138  pub archived: bool,
139  pub created_at: DateTime<Utc>,
140}
141
142#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
143pub struct Project {
144  pub id: ProjectId,
145  pub name: String,
146  pub customer_id: CustomerId,
147  pub customer_name: String,
148  pub note: String,
149  pub archived: bool,
150  pub created_at: DateTime<Utc>,
151}
152
153#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
154pub struct Service {
155  pub id: ServiceId,
156  pub name: String,
157  pub note: String,
158  pub billable: bool,
159  pub archived: bool,
160  pub created_at: DateTime<Utc>,
161}
162
163#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
164pub struct TimeEntry {
165  pub id: TimeEntryId,
166  pub date_at: NaiveDate,
167  pub minutes: Minutes,
168  pub customer_id: CustomerId,
169  pub customer_name: String,
170  pub project_id: ProjectId,
171  pub project_name: String,
172  pub service_id: ServiceId,
173  pub service_name: String,
174  pub user_id: UserId,
175  pub user_name: String,
176  pub note: String,
177  pub billable: bool,
178  pub locked: bool,
179  pub created_at: DateTime<Utc>,
180}
181
182#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
183pub struct Tracker {
184  pub since: Option<DateTime<Utc>>,
185  pub tracking_time_entry: Option<TimeEntry>,
186  pub stopped_time_entry: Option<TimeEntry>,
187}
188
189#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy, Default)]
190pub struct Minutes(pub u32);
191
192impl fmt::Display for Minutes {
193  fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
194    write!(f, "{}:{:02}", self.0 / 60, self.0 % 60)
195  }
196}
197
198impl ops::Add for Minutes {
199  type Output = Minutes;
200  fn add(self, rhs: Minutes) -> Self::Output {
201    Minutes(self.0 + rhs.0)
202  }
203}
204
205impl ops::AddAssign for Minutes {
206  fn add_assign(&mut self, rhs: Self) {
207    self.0 += rhs.0;
208  }
209}
210
211impl std::iter::Sum<Minutes> for Minutes {
212  fn sum<I: Iterator<Item = Minutes>>(iter: I) -> Self {
213    Minutes(iter.map(|m| m.0).sum())
214  }
215}
216
217impl FromStr for Minutes {
218  type Err = AcariError;
219
220  fn from_str(s: &str) -> Result<Self, Self::Err> {
221    match s.find(':') {
222      Some(idx) => {
223        let hours = s[..idx].parse::<u32>().map_err(|e| user_error!("Invalid time format: {}", e))?;
224        let minutes = s[idx + 1..].parse::<u32>().map_err(|e| user_error!("Invalid time format: {}", e))?;
225
226        if minutes >= 60 {
227          Err(AcariError::UserError("No more than 60 minutes per hour".to_string()))
228        } else if hours >= 24 {
229          Err(AcariError::UserError("No more than 24 hour per day".to_string()))
230        } else {
231          Ok(Minutes(hours * 60 + minutes))
232        }
233      }
234      None => Ok(Minutes(s.parse::<u32>().map_err(|e| user_error!("Invalid time format: {}", e))?)),
235    }
236  }
237}
238
239#[cfg(test)]
240mod tests {
241  use super::*;
242  use pretty_assertions::assert_eq;
243
244  #[test]
245  fn test_parse_minutes() -> Result<(), Box<dyn std::error::Error>> {
246    assert_eq!("123".parse::<Minutes>()?, Minutes(123));
247    assert_eq!("0:40".parse::<Minutes>()?, Minutes(40));
248    assert_eq!("5:35".parse::<Minutes>()?, Minutes(5 * 60 + 35));
249
250    Ok(())
251  }
252}