ct_tracker_lib/projects/
project.rs

1use super::errors;
2use chrono::{DateTime, Duration, Utc};
3use serde::{Deserialize, Serialize};
4use std::ops::Add;
5use std::path::PathBuf;
6
7type DT = DateTime<Utc>;
8
9#[derive(Debug, Serialize, Deserialize)]
10pub struct Project {
11    name: String,
12    initial_date: DT,
13    sessions: Vec<Session>,
14}
15
16impl Project {
17    /// Get a reference to the project's name.
18    pub fn name(&self) -> &String {
19        &self.name
20    }
21
22    /// Get a reference to the project's initial date.
23    pub fn initial_date(&self) -> &DT {
24        &self.initial_date
25    }
26
27    /// Get a reference to the project's sessions.
28    pub fn sessions(&self) -> &Vec<Session> {
29        &self.sessions
30    }
31
32    // Only pub(super), because pub(in ...) can only contain ancestors and not siblings :(
33    pub(super) fn create(name: &str) -> errors::CtResult<Project> {
34        //// check if project already exists -> either fail or return existing project
35        // This shouldn't be done, that's what ProjectFrame is for
36        if super::has(name)? {
37            Err(errors::CtError::Own("Project already exists"))
38        } else {
39            Ok(Project {
40                name: name.to_owned(),
41                // Maybe make time provider configurable so it can be mocked for easier testing
42                initial_date: Utc::now(),
43                sessions: vec![],
44            })
45        }
46    }
47
48    pub(super) fn from_json(json: &str) -> errors::CtResult<Project> {
49        serde_json::from_str(json).map_err(|e| e.into())
50    }
51
52    pub fn json(&self, pretty: bool) -> errors::CtResult<String> {
53        if pretty {
54            serde_json::to_string_pretty(self).map_err(|e| e.into())
55        } else {
56            serde_json::to_string(self).map_err(|e| e.into())
57        }
58    }
59
60    pub(super) fn start(&mut self) {
61        // First check if a session is running
62        // Should actually always be the last one
63        for s in self.sessions.iter() {
64            if s.end.is_none() {
65                // We have an open session
66                return;
67            }
68        }
69        // We have no open session, so we can push one
70        self.sessions.push(Session::new());
71    }
72
73    pub(super) fn stop(&mut self) {
74        let open: Vec<&mut Session> = self.sessions.iter_mut().filter(|s| s.is_open()).collect();
75        // For now assert that theres only one open session (or none)
76        assert!(open.len() < 2);
77
78        // We have asserted that there's less than one session,
79        // so we can just use a loop to close them all
80        for s in open {
81            s.close();
82        }
83    }
84
85    pub fn is_open(&self) -> bool {
86        self.sessions.iter().fold(false, |acc, x| acc | x.is_open())
87    }
88
89    pub fn duration(&self) -> Duration {
90        self.sessions
91            .iter()
92            .map(|s| s.timespan())
93            .fold(Duration::zero(), Duration::add)
94    }
95
96    pub fn session_count(&self) -> usize {
97        self.sessions.len()
98    }
99
100    pub fn load(path: &PathBuf) -> errors::CtResult<Project> {
101        assert!(path.exists());
102        assert!(path.is_file());
103        // Project already exists: Try to load it and return a new frame
104        let json = std::fs::read_to_string(&path)?;
105        Project::from_json(json.as_str())
106    }
107
108    pub fn load_from_name(name: &str) -> errors::CtResult<Project> {
109        Self::load(&super::project_path(name)?)
110    }
111}
112
113#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
114pub struct Session {
115    start: DT,
116    end: Option<DT>,
117}
118
119impl Session {
120    fn new() -> Session {
121        Session {
122            start: Utc::now(),
123            end: None,
124        }
125    }
126
127    pub fn is_open(&self) -> bool {
128        self.end.is_none()
129    }
130
131    fn close(&mut self) {
132        assert!(self.end == None);
133        self.end = Some(Utc::now());
134    }
135
136    pub fn timespan(&self) -> Duration {
137        let start = self.start;
138        // If this session is running, its current spanning time runs up until Utc::now()
139        let end = self.end.unwrap_or_else(Utc::now);
140        end.signed_duration_since(start)
141    }
142
143    pub fn start_time(&self) -> &DT {
144        &self.start
145    }
146
147    pub fn end_time(&self) -> Option<&DT> {
148        if let Some(t) = &self.end {
149            Some(t)
150        } else {
151            None
152        }
153    }
154}
155
156#[cfg(test)]
157mod tests {
158    use chrono::{Duration, Utc};
159    use std::ops::Add;
160
161    use super::{Project, Session, DT};
162
163    struct TimeProvider {
164        /// The current internal state
165        state: i32,
166        /// The number of timestamps to give
167        num: i32,
168        /// The base timestamp
169        base_time: DT,
170        timestep: Duration,
171    }
172
173    impl TimeProvider {
174        fn new(timestep: Duration) -> TimeProvider {
175            Self::lim(-1, timestep)
176        }
177
178        fn lim(num: i32, timestep: Duration) -> TimeProvider {
179            TimeProvider {
180                state: 0,
181                num,
182                base_time: Utc::now(),
183                timestep,
184            }
185        }
186    }
187
188    impl Iterator for TimeProvider {
189        type Item = DT;
190
191        fn next(&mut self) -> Option<Self::Item> {
192            if self.num <= 0 || self.state <= self.num {
193                self.state += 1;
194                self.base_time = self.base_time.add(self.timestep);
195                Some(self.base_time)
196            } else {
197                None
198            }
199        }
200    }
201
202    impl Project {
203        fn create_test(session_count: usize, timestep: Duration) -> Project {
204            let mut t = TimeProvider::new(timestep);
205            let initial_date = t.next().unwrap();
206            let mut sessions = Vec::with_capacity(session_count);
207            for _ in 1..=session_count {
208                sessions.push(Session::mock(&mut t));
209            }
210            Project {
211                name: "TEST".to_owned(),
212                initial_date,
213                sessions,
214            }
215        }
216    }
217
218    impl Session {
219        fn mock(t: &mut TimeProvider) -> Session {
220            if let Some(time) = t.next() {
221                Session {
222                    start: time,
223                    end: t.next(),
224                }
225            } else {
226                Session {
227                    start: Utc::now(),
228                    end: Some(Utc::now()),
229                }
230            }
231        }
232    }
233
234    #[test]
235    fn duration() {
236        let timestep = Duration::seconds(5);
237        let session_count = 10;
238        let p = Project::create_test(session_count, timestep);
239        assert_eq!(
240            Duration::seconds(timestep.num_seconds() * session_count as i64),
241            p.duration()
242        );
243    }
244
245    #[test]
246    fn multiple_start() {
247        let mut p = Project::create("TEST").unwrap();
248
249        // Multiple starts shouldn't lead to multiple sessions
250        p.start();
251        p.start();
252        p.start();
253
254        println!("Project has {} sessions", p.sessions.len());
255        assert_eq!(p.sessions.len(), 1);
256
257        // ...therefore one stop should suffice
258        p.stop();
259
260        let has_open_sessions = p.sessions.iter().fold(false, |acc, x| acc | x.is_open());
261        println!("Project has open sessions? {}", has_open_sessions);
262        assert!(!has_open_sessions);
263
264        println!("Test finished successfully");
265    }
266
267    #[test]
268    fn session_timespan() {
269        let dur = Duration::seconds(5);
270        let mut t = TimeProvider::new(dur);
271        let s = Session::mock(&mut t);
272        assert_eq!(dur, s.timespan());
273    }
274}