rtw 2.0.0-alpha

time tracker command line tool
Documentation
//! Logic above an activity storage
use crate::rtw_core::activity::{intersect, overlap, Activity, OngoingActivity};
use crate::rtw_core::datetimew::DateTimeW;
use crate::rtw_core::service::ActivityService;
use crate::rtw_core::storage::Storage;
use crate::rtw_core::ActivityId;
use anyhow::anyhow;

pub struct Service<S>
where
    S: Storage,
{
    storage: S,
}

impl<S> Service<S>
where
    S: Storage,
{
    pub fn new(storage: S) -> Self {
        Service { storage }
    }
}

impl<S> ActivityService for Service<S>
where
    S: Storage,
{
    fn get_current_activity(&self) -> anyhow::Result<Option<OngoingActivity>> {
        self.storage.get_current_activity().map_err(|e| e.into())
    }

    fn start_activity(&mut self, activity: OngoingActivity) -> anyhow::Result<OngoingActivity> {
        let finished = self.storage.get_finished_activities()?;
        let intersections = time_intersections(finished.as_slice(), &activity.start_time);
        if intersections.is_empty() {
            self.stop_current_activity(activity.start_time)?;
            self.storage.set_current_activity(activity.clone())?;
            Ok(activity)
        } else {
            Err(anyhow!("{:?} would overlap {:?}", activity, intersections))
        }
    }

    fn stop_current_activity(&mut self, time: DateTimeW) -> anyhow::Result<Option<Activity>> {
        let current = self.storage.get_current_activity()?;
        match current {
            None => Ok(None),
            Some(current_activity) => {
                let stopped = current_activity.clone().into_activity(time)?;
                let finished = self.storage.get_finished_activities()?;
                let intersections = activity_intersections(finished.as_slice(), &stopped);
                if intersections.is_empty() {
                    self.storage.write_activity(stopped)?;
                    self.storage.reset_current_activity()?;
                    Ok(Some(current_activity.into_activity(time)?))
                } else {
                    Err(anyhow!("{:?} would overlap {:?}", stopped, intersections))
                }
            }
        }
    }

    fn cancel_current_activity(&mut self) -> anyhow::Result<Option<OngoingActivity>> {
        self.storage.reset_current_activity().map_err(|e| e.into())
    }

    fn filter_activities<P>(&self, p: P) -> anyhow::Result<Vec<(ActivityId, Activity)>>
    where
        P: Fn(&(ActivityId, Activity)) -> bool,
    {
        self.storage.filter_activities(p).map_err(|e| e.into())
    }

    fn get_finished_activities(&self) -> anyhow::Result<Vec<(ActivityId, Activity)>> {
        self.storage.get_finished_activities().map_err(|e| e.into())
    }

    fn delete_activity(&self, id: ActivityId) -> anyhow::Result<Option<Activity>> {
        self.storage.delete_activity(id).map_err(|e| e.into())
    }

    fn track_activity(&mut self, activity: Activity) -> anyhow::Result<Activity> {
        let finished = self.storage.get_finished_activities()?;
        let intersections = activity_intersections(finished.as_slice(), &activity);
        if intersections.is_empty() {
            self.storage.write_activity(activity.clone())?;
            Ok(activity)
        } else {
            Err(anyhow!("{:?} would overlap {:?}", activity, intersections))
        }
    }
}

fn activity_intersections(
    activities: &[(ActivityId, Activity)],
    activity: &Activity,
) -> Vec<Activity> {
    activities
        .iter()
        .filter_map(|(_, a)| overlap(a, activity))
        .collect()
}

fn time_intersections(
    activities: &[(ActivityId, Activity)],
    start_time: &DateTimeW,
) -> Vec<Activity> {
    activities
        .iter()
        .filter_map(|(_, a)| intersect(a, start_time))
        .collect()
}

#[cfg(test)]
mod tests {
    use crate::chrono_clock::ChronoClock;
    use crate::json_storage::JsonStorage;
    use crate::rtw_core::activity::OngoingActivity;
    use crate::rtw_core::clock::Clock;
    use crate::rtw_core::datetimew::DateTimeW;
    use crate::rtw_core::service::ActivityService;
    use crate::service::Service;
    use chrono::{Local, TimeZone};
    use tempfile::{tempdir, TempDir};

    fn build_json_service(test_dir: &TempDir) -> Service<JsonStorage> {
        let finished_path = test_dir.path().join(".rtwh.json");
        let current_path = test_dir.path().join(".rtwc.json");
        Service::new(JsonStorage::new(current_path, finished_path))
    }

    #[test]
    fn test_no_activity() {
        let clock = ChronoClock {};
        let test_dir = tempdir().expect("error while creating tempdir");
        let mut service = build_json_service(&test_dir);
        assert!(service.stop_current_activity(clock.get_time()).is_ok());
        assert!(service.get_current_activity().unwrap().is_none());
    }

    #[test]
    fn test_start_activity() {
        let clock = ChronoClock {};
        let test_dir = tempdir().expect("error while creating tempdir");
        let mut service = build_json_service(&test_dir);
        assert!(service.stop_current_activity(clock.get_time()).is_ok());
        let start = service.start_activity(OngoingActivity {
            start_time: clock.get_time(),
            tags: vec![String::from("a")],
        });
        start.unwrap();
        let current = service.get_current_activity();
        assert!(current.is_ok());
        assert!(current.unwrap().is_some());
    }

    #[test]
    fn test_stop_activity_with_active() {
        let clock = ChronoClock {};
        let test_dir = tempdir().expect("error while creating tempdir");
        let mut service = build_json_service(&test_dir);
        let start = service.start_activity(OngoingActivity {
            start_time: clock.get_time(),
            tags: vec![String::from("a")],
        });
        start.unwrap();
        assert!(service.get_current_activity().unwrap().is_some());
        assert!(service.stop_current_activity(clock.get_time()).is_ok());
        assert!(service.get_current_activity().unwrap().is_none());
    }

    #[test]
    fn test_start_stop_start() {
        let clock = ChronoClock {};
        let test_dir = tempdir().expect("error while creating tempdir");
        let mut service = build_json_service(&test_dir);
        let start_0 = service.start_activity(OngoingActivity {
            start_time: clock.get_time(),
            tags: vec![String::from("a")],
        });
        assert!(start_0.is_ok());
        assert!(service.get_current_activity().unwrap().is_some());
        let stop = service.stop_current_activity(clock.get_time());
        assert!(stop.is_ok());
        assert!(service.get_current_activity().unwrap().is_none());
        let start_1 = service.start_activity(OngoingActivity {
            start_time: clock.get_time(),
            tags: vec![String::from("b")],
        });
        assert!(start_1.is_ok());
        assert!(service.get_current_activity().unwrap().is_some());
    }

    #[test]
    fn test_start_intersecting_activity() {
        let test_dir = tempdir().expect("error while creating tempdir");
        let mut service = build_json_service(&test_dir);
        let finished = OngoingActivity::new(
            Local
                .datetime_from_str("2020-12-25T09:00:00", "%Y-%m-%dT%H:%M:%S")
                .unwrap()
                .into(),
            vec![],
        )
        .into_activity(
            Local
                .datetime_from_str("2020-12-25T10:00:00", "%Y-%m-%dT%H:%M:%S")
                .unwrap()
                .into(),
        )
        .unwrap();
        let tracked = service.track_activity(finished);
        assert!(tracked.is_ok());
        let other = OngoingActivity::new(
            Local
                .datetime_from_str("2020-12-25T09:30:00", "%Y-%m-%dT%H:%M:%S")
                .unwrap()
                .into(),
            vec![],
        );
        let started = service.start_activity(other);
        assert!(started.is_err());
    }

    #[test]
    fn test_stop_intersecting_activity() {
        let test_dir = tempdir().expect("error while creating tempdir");
        let mut service = build_json_service(&test_dir);
        let finished = OngoingActivity::new(
            Local
                .datetime_from_str("2020-12-25T09:00:00", "%Y-%m-%dT%H:%M:%S")
                .unwrap()
                .into(),
            vec![],
        )
        .into_activity(
            Local
                .datetime_from_str("2020-12-25T10:00:00", "%Y-%m-%dT%H:%M:%S")
                .unwrap()
                .into(),
        )
        .unwrap();
        let tracked = service.track_activity(finished);
        assert!(tracked.is_ok());
        let other = OngoingActivity::new(
            Local
                .datetime_from_str("2020-12-25T08:30:00", "%Y-%m-%dT%H:%M:%S")
                .unwrap()
                .into(),
            vec![],
        );
        let started = service.start_activity(other);
        assert!(started.is_ok());
        let stopped = service.stop_current_activity(
            Local
                .datetime_from_str("2020-12-25T09:30:00", "%Y-%m-%dT%H:%M:%S")
                .unwrap()
                .into(),
        );
        assert!(stopped.is_err());
    }

    #[test]
    fn test_summary_nothing() {
        let clock = ChronoClock {};
        let test_dir = tempdir().expect("error while creating tempdir");
        let service = build_json_service(&test_dir);
        let (today_start, today_end) = clock.today_range();
        let activities = service.filter_activities(|(_id, a)| {
            today_start <= a.get_start_time() && a.get_start_time() <= today_end
        });
        assert!(activities.is_ok());
    }

    #[test]
    fn test_summary_something() {
        let test_dir = tempdir().expect("error while creating tempdir");
        let mut service = build_json_service(&test_dir);
        let today = chrono::Local::today();
        let range_start: DateTimeW = today.and_hms(8, 0, 0).into();
        let activity_start: DateTimeW = today.and_hms(8, 30, 0).into();
        let activity_end: DateTimeW = today.and_hms(8, 45, 0).into();
        let range_end: DateTimeW = today.and_hms(9, 0, 0).into();
        let _start = service.start_activity(OngoingActivity::new(
            activity_start,
            vec![String::from("a")],
        ));
        let _stop = service.stop_current_activity(activity_end);
        let activities = service.filter_activities(|(_id, a)| {
            range_start <= a.get_start_time() && a.get_start_time() <= range_end
        });
        assert!(!activities.unwrap().is_empty());
    }

    #[test]
    fn test_summary_not_in_range() {
        let test_dir = tempdir().expect("error while creating tempdir");
        let mut service = build_json_service(&test_dir);
        let today = chrono::Local::today();
        let range_start: DateTimeW = today.and_hms(9, 0, 0).into();
        let activity_start: DateTimeW = today.and_hms(8, 30, 0).into();
        let activity_end: DateTimeW = today.and_hms(8, 45, 0).into();
        let range_end: DateTimeW = today.and_hms(10, 0, 0).into();
        let _start = service.start_activity(OngoingActivity::new(
            activity_start,
            vec![String::from("a")],
        ));
        let _stop = service.stop_current_activity(activity_end);
        let activities = service.filter_activities(|(_id, a)| {
            range_start <= a.get_start_time() && a.get_start_time() <= range_end
        });
        assert!(activities.unwrap().is_empty());
    }

    #[test]
    fn test_track_intersecting_activity() {
        let test_dir = tempdir().expect("error while creating tempdir");
        let mut service = build_json_service(&test_dir);
        let finished = OngoingActivity::new(
            Local
                .datetime_from_str("2020-12-25T09:00:00", "%Y-%m-%dT%H:%M:%S")
                .unwrap()
                .into(),
            vec![],
        )
        .into_activity(
            Local
                .datetime_from_str("2020-12-25T10:00:00", "%Y-%m-%dT%H:%M:%S")
                .unwrap()
                .into(),
        )
        .unwrap();
        let tracked = service.track_activity(finished);
        assert!(tracked.is_ok());
        let other = OngoingActivity::new(
            Local
                .datetime_from_str("2020-12-25T09:30:00", "%Y-%m-%dT%H:%M:%S")
                .unwrap()
                .into(),
            vec![],
        )
        .into_activity(
            Local
                .datetime_from_str("2020-12-25T10:30:00", "%Y-%m-%dT%H:%M:%S")
                .unwrap()
                .into(),
        )
        .unwrap();
        let tracked = service.track_activity(other);
        assert!(tracked.is_err());
    }
}