ct_tracker_lib/projects/
project.rs1use 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 pub fn name(&self) -> &String {
19 &self.name
20 }
21
22 pub fn initial_date(&self) -> &DT {
24 &self.initial_date
25 }
26
27 pub fn sessions(&self) -> &Vec<Session> {
29 &self.sessions
30 }
31
32 pub(super) fn create(name: &str) -> errors::CtResult<Project> {
34 if super::has(name)? {
37 Err(errors::CtError::Own("Project already exists"))
38 } else {
39 Ok(Project {
40 name: name.to_owned(),
41 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 for s in self.sessions.iter() {
64 if s.end.is_none() {
65 return;
67 }
68 }
69 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 assert!(open.len() < 2);
77
78 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 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 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 state: i32,
166 num: i32,
168 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 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 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}