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}