Skip to main content

gitlab_time_report/
model.rs

1//! Contains the data model for the time log data.
2
3use chrono::{DateTime, Duration, Local, NaiveDate};
4use serde::Deserialize;
5use serde_with::{DisplayFromStr, DurationSeconds, NoneAsEmptyString, serde_as};
6
7/// The queried GitLab repository.
8#[derive(Debug, PartialEq, Default)]
9pub struct Project {
10    /// The name of the repository.
11    pub name: String,
12    /// The time logs of the repository.
13    pub time_logs: Vec<TimeLog>,
14    /// Total Time spent on the project
15    pub total_spent_time: Duration,
16}
17
18impl Project {
19    /// Merges two projects into one. The name of the resulting project is a comma-seperated string
20    /// of the input projects.
21    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/// A single entry of time spent on an issue or merge request.
29#[serde_as]
30#[derive(Debug, PartialEq, Default, Deserialize)]
31#[serde(rename_all = "camelCase")]
32#[serde(rename = "Nodes")]
33pub struct TimeLog {
34    /// The date the time was spent. Is always set to a valid date in the API.
35    pub spent_at: DateTime<Local>,
36    /// The entered time that was spent.
37    #[serde_as(as = "DurationSeconds<i64>")]
38    pub time_spent: Duration,
39    /// The optional summary of what was done during the time.
40    /// Empty summaries are returned as empty strings by the GitLab API and turned into `None`.
41    #[serde_as(as = "NoneAsEmptyString")]
42    pub summary: Option<String>,
43    /// The user who spent the time.
44    pub user: User,
45    /// The Issue or Merge Request the time that was spent on.
46    #[serde(flatten)]
47    pub trackable_item: TrackableItem,
48}
49
50/// A list of GitLab users.
51#[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/// The details of a GitLab user.
58#[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/// A trackable item is either an issue or a merge request.
71#[derive(Debug, PartialEq, Default, Eq, Hash, PartialOrd, Ord, Clone)]
72pub struct TrackableItem {
73    /// Fields shared by all trackable items.
74    // Implementation note: The fields are in a separate struct to use serde attributes, as they
75    // are only supported when Deserialize is derived.
76    pub common: TrackableItemFields,
77    /// The type of the trackable item. Contains the fields that are only available for the specific type.
78    pub kind: TrackableItemKind,
79}
80
81/// The type of the trackable item. Each variant contains a struct with the fields that are
82/// only available for the specific type.
83#[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/// Contains the fields that are common to all trackable items.
105#[serde_as]
106#[derive(Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Clone, Default, Deserialize)]
107#[serde(rename_all = "camelCase")]
108pub struct TrackableItemFields {
109    /// The issue or merge request ID.
110    #[serde(rename = "iid")]
111    #[serde_as(as = "DisplayFromStr")]
112    pub id: u32,
113    /// The title of the issue or merge request.
114    pub title: String,
115    /// The estimated time set for this item.
116    #[serde_as(as = "DurationSeconds<i64>")]
117    pub time_estimate: Duration,
118    /// The total time spent on this item.
119    #[serde_as(as = "DurationSeconds<i64>")]
120    pub total_time_spent: Duration,
121    /// The users assigned to this item. On the GitLab Free version, only one user can be assigned.
122    pub assignees: UserNodes,
123    /// The milestone assigned to this item.
124    pub milestone: Option<Milestone>,
125    /// The labels assigned to this item
126    pub labels: Labels,
127}
128
129/// Contains fields that are only available for issues (there are none).
130#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Default, Deserialize)]
131pub struct Issue {}
132
133/// Contains fields that are only available for merge requests.
134#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Default, Deserialize)]
135pub struct MergeRequest {
136    /// The users reviewing this MR. On the GitLab Free version, only one user can be assigned.
137    pub reviewers: UserNodes,
138}
139
140/// A milestone is a due date assigned to an issue or merge request.
141#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Default, Deserialize)]
142#[serde(rename_all = "camelCase")]
143pub struct Milestone {
144    /// The name of the milestone.
145    pub title: String,
146    /// When the milestone is due.
147    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/// A list of labels assigned to an issue or merge request.
157#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Default, Deserialize)]
158pub struct Labels {
159    /// The labels assigned to this item.
160    #[serde(rename = "nodes")]
161    pub labels: Vec<Label>,
162}
163
164/// A single label assigned to an issue or merge request.
165#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Default, Deserialize)]
166pub struct Label {
167    /// The title of the label.
168    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}