use std::{collections::BTreeMap, fmt::Display, sync::Arc};
use enum_dispatch::enum_dispatch;
use itertools::Itertools;
use pace_time::{date::PaceDate, duration::PaceDurationRange, time_range::TimeRangeOptions};
use tracing::debug;
use crate::{
commands::{
hold::HoldOptions, resume::ResumeOptions, DeleteOptions, EndOptions, KeywordOptions,
UpdateOptions,
},
config::{ActivityLogStorageKind, PaceConfig},
domain::{
activity::{Activity, ActivityGuid, ActivityItem, ActivityKind},
filter::{ActivityFilterKind, FilteredActivities},
status::ActivityStatusKind,
},
error::{PaceErrorKind, PaceOptResult, PaceResult},
service::activity_store::ActivityStore,
storage::{file::TomlActivityStorage, in_memory::InMemoryActivityStorage},
};
pub mod file;
pub mod in_memory;
pub fn get_storage_from_config(config: &PaceConfig) -> PaceResult<Arc<StorageKind>> {
let storage: StorageKind = match config.general().activity_log_options().storage_kind() {
ActivityLogStorageKind::File => {
TomlActivityStorage::new(config.general().activity_log_options().path())?.into()
}
ActivityLogStorageKind::Database => {
return Err(PaceErrorKind::DatabaseStorageNotImplemented.into())
}
#[cfg(test)]
ActivityLogStorageKind::InMemory => InMemoryActivityStorage::new().into(),
};
debug!("Using storage backend: {}", storage);
Ok(Arc::new(storage))
}
#[enum_dispatch]
pub enum StorageKind {
ActivityStore,
InMemoryActivityStorage,
TomlActivityStorage,
}
impl Display for StorageKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ActivityStore(_) => write!(f, "StorageKind: ActivityStore"),
Self::InMemoryActivityStorage(_) => {
write!(f, "StorageKind: InMemoryActivityStorage")
}
Self::TomlActivityStorage(_) => write!(f, "StorageKind: TomlActivityStorage"),
}
}
}
#[enum_dispatch(StorageKind)]
pub trait SyncStorage {
fn sync(&self) -> PaceResult<()>;
}
#[enum_dispatch(StorageKind)]
pub trait ActivityStorage:
ActivityReadOps + ActivityWriteOps + ActivityStateManagement + SyncStorage + ActivityQuerying
{
fn setup_storage(&self) -> PaceResult<()>;
}
#[enum_dispatch(StorageKind)]
pub trait ActivityReadOps {
fn read_activity(&self, activity_id: ActivityGuid) -> PaceResult<ActivityItem>;
fn list_activities(&self, filter: ActivityFilterKind) -> PaceOptResult<FilteredActivities>;
}
#[enum_dispatch(StorageKind)]
pub trait ActivityWriteOps: ActivityReadOps {
fn create_activity(&self, activity: Activity) -> PaceResult<ActivityItem>;
fn update_activity(
&self,
activity_id: ActivityGuid,
updated_activity: Activity,
update_opts: UpdateOptions,
) -> PaceResult<ActivityItem>;
fn delete_activity(
&self,
activity_id: ActivityGuid,
delete_opts: DeleteOptions,
) -> PaceResult<ActivityItem>;
}
#[enum_dispatch(StorageKind)]
pub trait ActivityStateManagement: ActivityReadOps + ActivityWriteOps + ActivityQuerying {
fn begin_activity(&self, mut activity: Activity) -> PaceResult<ActivityItem> {
let _ = self.end_all_activities(EndOptions::default())?;
activity.make_active();
self.create_activity(activity)
}
fn hold_activity(
&self,
activity_id: ActivityGuid,
hold_opts: HoldOptions,
) -> PaceResult<ActivityItem>;
fn resume_activity(
&self,
activity_id: ActivityGuid,
resume_opts: ResumeOptions,
) -> PaceResult<ActivityItem>;
fn resume_most_recent_activity(
&self,
resume_opts: ResumeOptions,
) -> PaceOptResult<ActivityItem>;
fn end_activity(
&self,
activity_id: ActivityGuid,
end_opts: EndOptions,
) -> PaceResult<ActivityItem>;
fn end_all_activities(&self, end_opts: EndOptions) -> PaceOptResult<Vec<ActivityItem>>;
fn end_all_active_intermissions(
&self,
end_opts: EndOptions,
) -> PaceOptResult<Vec<ActivityGuid>>;
fn end_last_unfinished_activity(&self, end_opts: EndOptions) -> PaceOptResult<ActivityItem>;
fn hold_most_recent_active_activity(
&self,
hold_opts: HoldOptions,
) -> PaceOptResult<ActivityItem>;
}
#[enum_dispatch(StorageKind)]
pub trait ActivityQuerying: ActivityReadOps {
fn group_activities_by_duration_range(
&self,
) -> PaceOptResult<BTreeMap<PaceDurationRange, Vec<ActivityItem>>>;
fn group_activities_by_start_date(
&self,
) -> PaceOptResult<BTreeMap<PaceDate, Vec<ActivityItem>>>;
fn list_activities_with_intermissions(
&self,
) -> PaceOptResult<BTreeMap<ActivityGuid, Vec<ActivityItem>>>;
fn group_activities_by_keywords(
&self,
keyword_opts: KeywordOptions,
) -> PaceOptResult<BTreeMap<String, Vec<ActivityItem>>>;
fn group_activities_by_kind(&self) -> PaceOptResult<BTreeMap<ActivityKind, Vec<ActivityItem>>>;
fn list_activities_by_time_range(
&self,
time_range_opts: TimeRangeOptions,
) -> PaceOptResult<Vec<ActivityGuid>>;
fn group_activities_by_status(
&self,
) -> PaceOptResult<BTreeMap<ActivityStatusKind, Vec<ActivityItem>>>;
fn list_current_activities(
&self,
filter: ActivityFilterKind,
) -> PaceOptResult<Vec<ActivityGuid>> {
Ok(self
.list_activities(filter)?
.map(FilteredActivities::into_vec))
}
fn list_activities_by_id(&self) -> PaceOptResult<BTreeMap<ActivityGuid, Activity>>;
fn list_active_intermissions(&self) -> PaceOptResult<Vec<ActivityGuid>> {
Ok(self
.list_activities(ActivityFilterKind::ActiveIntermission)?
.map(FilteredActivities::into_vec))
}
fn list_most_recent_activities(&self, count: usize) -> PaceOptResult<Vec<ActivityGuid>> {
let filtered = self
.list_activities(ActivityFilterKind::OnlyActivities)?
.map(FilteredActivities::into_vec);
let Some(filtered) = filtered else {
debug!("No recent activities found");
return Ok(None);
};
if filtered.len() > count {
debug!(
"Found more than {} recent activities, dropping some...",
count
);
Ok(Some(
(*filtered)
.iter()
.sorted()
.rev()
.take(count)
.rev()
.copied()
.collect(),
))
} else {
debug!("Found {} recent activities", filtered.len());
Ok(Some(filtered))
}
}
fn is_activity_active(&self, activity_id: ActivityGuid) -> PaceResult<bool> {
let activity = self.read_activity(activity_id)?;
debug!(
"Checking if Activity with id {:?} is active: {}",
activity_id,
if activity.activity().is_in_progress() {
"yes"
} else {
"no"
}
);
Ok(activity.activity().is_in_progress())
}
fn list_intermissions_for_activity_id(
&self,
activity_id: ActivityGuid,
) -> PaceOptResult<Vec<ActivityItem>> {
let Some(filtered) = self
.list_activities(ActivityFilterKind::Intermission)?
.map(FilteredActivities::into_vec)
else {
debug!("No intermissions found.");
return Ok(None);
};
let intermissions = filtered
.iter()
.filter_map(|activity| {
let activity_item = self.read_activity(*activity).ok()?;
if activity_item.activity().parent_id() == Some(activity_id) {
debug!("Found intermission for activity: {}", activity_id);
Some(activity_item)
} else {
debug!("Not an intermission for activity: {}", activity_id);
None
}
})
.collect::<Vec<ActivityItem>>();
if intermissions.is_empty() {
debug!("No intermissions found for activity: {}", activity_id);
return Ok(None);
}
debug!(
"Activity with id {:?} has intermissions: {:?}",
activity_id, intermissions
);
Ok(Some(intermissions))
}
fn list_active_intermissions_for_activity_id(
&self,
activity_id: ActivityGuid,
) -> PaceOptResult<Vec<ActivityGuid>> {
let guids = self.list_active_intermissions()?.map(|log| {
log.iter()
.filter_map(|active_intermission_id| {
if self
.read_activity(*active_intermission_id)
.ok()?
.activity()
.parent_id()
== Some(activity_id)
{
debug!("Found active intermission for activity: {}", activity_id);
Some(*active_intermission_id)
} else {
debug!("No active intermission found for activity: {}", activity_id);
None
}
})
.collect::<Vec<ActivityGuid>>()
});
debug!(
"Activity with id {:?} has active intermissions: {:?}",
activity_id, guids
);
Ok(guids)
}
fn most_recent_active_activity(&self) -> PaceOptResult<ActivityItem> {
let Some(current) = self.list_current_activities(ActivityFilterKind::Active)? else {
debug!("No active activities found");
return Ok(None);
};
current
.into_iter()
.sorted()
.rev()
.find(|activity_id| {
self.read_activity(*activity_id)
.map(|activity| {
activity.activity().is_in_progress()
&& activity.activity().kind().is_activity()
&& !activity.activity().is_active_intermission()
})
.unwrap_or(false)
})
.map(|activity_id| self.read_activity(activity_id))
.transpose()
}
fn most_recent_held_activity(&self) -> PaceOptResult<ActivityItem> {
let Some(current) = self.list_current_activities(ActivityFilterKind::Held)? else {
debug!("No held activities found");
return Ok(None);
};
current
.into_iter()
.sorted()
.rev()
.find(|activity_id| {
self.read_activity(*activity_id)
.map(|activity| {
activity.activity().is_paused()
&& activity.activity().kind().is_activity()
&& !activity.activity().is_active_intermission()
})
.unwrap_or(false)
})
.map(|activity_id| self.read_activity(activity_id))
.transpose()
}
}