1use chrono::{DateTime, Duration, Local, NaiveDate};
4use serde::Deserialize;
5use serde_with::{DisplayFromStr, DurationSeconds, NoneAsEmptyString, serde_as};
6
7#[derive(Debug, PartialEq, Default)]
9pub struct Project {
10 pub name: String,
12 pub time_logs: Vec<TimeLog>,
14 pub total_spent_time: Duration,
16}
17
18impl Project {
19 pub fn merge(&mut self, other: Project) {
22 self.name = format!("{}, {}", self.name, other.name);
23 self.total_spent_time += other.total_spent_time;
24 self.time_logs.extend(other.time_logs);
25 }
26}
27
28#[serde_as]
30#[derive(Debug, PartialEq, Default, Deserialize)]
31#[serde(rename_all = "camelCase")]
32#[serde(rename = "Nodes")]
33pub struct TimeLog {
34 pub spent_at: DateTime<Local>,
36 #[serde_as(as = "DurationSeconds<i64>")]
38 pub time_spent: Duration,
39 #[serde_as(as = "NoneAsEmptyString")]
42 pub summary: Option<String>,
43 pub user: User,
45 #[serde(flatten)]
47 pub trackable_item: TrackableItem,
48}
49
50#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Default, Deserialize)]
52pub struct UserNodes {
53 #[serde(rename = "nodes")]
54 pub users: Vec<User>,
55}
56
57#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Default, Deserialize)]
59pub struct User {
60 pub name: String,
61 pub username: String,
62}
63
64impl std::fmt::Display for User {
65 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66 write!(f, "{}", self.name)
67 }
68}
69
70#[derive(Debug, PartialEq, Default, Eq, Hash, PartialOrd, Ord, Clone)]
72pub struct TrackableItem {
73 pub common: TrackableItemFields,
77 pub kind: TrackableItemKind,
79}
80
81#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
84pub enum TrackableItemKind {
85 Issue(Issue),
86 MergeRequest(MergeRequest),
87}
88
89impl Default for TrackableItemKind {
90 fn default() -> Self {
91 Self::Issue(Issue::default())
92 }
93}
94
95impl std::fmt::Display for TrackableItemKind {
96 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
97 match self {
98 TrackableItemKind::Issue(_) => write!(f, "Issue"),
99 TrackableItemKind::MergeRequest(_) => write!(f, "Merge Request"),
100 }
101 }
102}
103
104#[serde_as]
106#[derive(Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Clone, Default, Deserialize)]
107#[serde(rename_all = "camelCase")]
108pub struct TrackableItemFields {
109 #[serde(rename = "iid")]
111 #[serde_as(as = "DisplayFromStr")]
112 pub id: u32,
113 pub title: String,
115 #[serde_as(as = "DurationSeconds<i64>")]
117 pub time_estimate: Duration,
118 #[serde_as(as = "DurationSeconds<i64>")]
120 pub total_time_spent: Duration,
121 pub assignees: UserNodes,
123 pub milestone: Option<Milestone>,
125 pub labels: Labels,
127}
128
129#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Default, Deserialize)]
131pub struct Issue {}
132
133#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Default, Deserialize)]
135pub struct MergeRequest {
136 pub reviewers: UserNodes,
138}
139
140#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Default, Deserialize)]
142#[serde(rename_all = "camelCase")]
143pub struct Milestone {
144 pub title: String,
146 pub due_date: Option<NaiveDate>,
148}
149
150impl std::fmt::Display for Milestone {
151 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
152 write!(f, "{}", self.title)
153 }
154}
155
156#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Default, Deserialize)]
158pub struct Labels {
159 #[serde(rename = "nodes")]
161 pub labels: Vec<Label>,
162}
163
164#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Default, Deserialize)]
166pub struct Label {
167 pub title: String,
169}
170
171impl std::fmt::Display for Label {
172 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
173 write!(f, "{}", self.title)
174 }
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180
181 #[test]
182 fn test_merge_projects() {
183 let mut project1 = Project {
184 name: "Frontend".to_string(),
185 total_spent_time: Duration::hours(2),
186 time_logs: vec![],
187 };
188 project1.time_logs.push(TimeLog {
189 spent_at: Local::now() - Duration::days(1),
190 time_spent: Duration::hours(2),
191 ..Default::default()
192 });
193
194 let mut project2 = Project {
195 name: "Backend".to_string(),
196 total_spent_time: Duration::hours(1),
197 time_logs: vec![],
198 };
199 project2.time_logs.push(TimeLog {
200 spent_at: Local::now() - Duration::weeks(1),
201 time_spent: Duration::hours(1),
202 ..Default::default()
203 });
204
205 project1.merge(project2);
206 assert_eq!(project1.name, "Frontend, Backend");
207 assert_eq!(project1.total_spent_time, Duration::hours(3));
208 assert_eq!(project1.time_logs.len(), 2);
209 }
210}