/* Copyright 2024, Savanni D'Gerinel This file is part of FitnessTrax. FitnessTrax is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. FitnessTrax is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with FitnessTrax. If not, see . */ use crate::app::{ReadError, RecordProvider}; use dimensioned::si; use emseries::{Record, RecordId, Recordable}; use ft_core::{TimeDistance, TimeDistanceActivity, TraxRecord}; use std::{ collections::HashMap, ops::Deref, sync::{Arc, RwLock}, }; // These are actually a used imports. Clippy isn't detecting their use, probably because of complexity around the async trait macros. #[allow(unused_imports)] use crate::app::WriteError; #[allow(unused_imports)] use chrono::NaiveDate; #[derive(Clone, Debug)] enum RecordState { Original(Record), New(Record), Updated(Record), Deleted(Record), } impl RecordState { fn exists(&self) -> bool { match self { RecordState::Original(_) => true, RecordState::New(_) => true, RecordState::Updated(_) => true, RecordState::Deleted(_) => false, } } #[allow(unused)] fn data(&self) -> Option<&Record> { match self { RecordState::Original(ref r) => Some(r), RecordState::New(ref r) => None, RecordState::Updated(ref r) => Some(r), RecordState::Deleted(ref r) => Some(r), } } fn set_value(&mut self, value: T) { *self = match self { RecordState::Original(r) => RecordState::Updated(Record { data: value, ..*r }), RecordState::New(r) => RecordState::New(Record { data: value, ..*r }), RecordState::Updated(r) => RecordState::Updated(Record { data: value, ..*r }), RecordState::Deleted(r) => RecordState::Updated(Record { data: value, ..*r }), }; } fn with_value(mut self, value: T) -> RecordState { self.set_value(value); self } #[allow(unused)] fn with_delete(self) -> Option> { match self { RecordState::Original(r) => Some(RecordState::Deleted(r)), RecordState::New(r) => None, RecordState::Updated(r) => Some(RecordState::Deleted(r)), RecordState::Deleted(r) => Some(RecordState::Deleted(r)), } } } impl Deref for RecordState { type Target = T; fn deref(&self) -> &Self::Target { match self { RecordState::Original(ref r) => &r.data, RecordState::New(ref r) => &r.data, RecordState::Updated(ref r) => &r.data, RecordState::Deleted(ref r) => &r.data, } } } impl std::ops::DerefMut for RecordState { fn deref_mut(&mut self) -> &mut Self::Target { match self { RecordState::Original(ref mut r) => &mut r.data, RecordState::New(ref mut r) => &mut r.data, RecordState::Updated(ref mut r) => &mut r.data, RecordState::Deleted(ref mut r) => &mut r.data, } } } #[derive(Clone)] pub struct DayDetailViewModel { provider: Arc, pub date: chrono::NaiveDate, weight: Arc>>>, steps: Arc>>>, records: Arc>>>, } impl DayDetailViewModel { pub async fn new( date: chrono::NaiveDate, provider: impl RecordProvider + 'static, ) -> Result { let s = Self { provider: Arc::new(provider), date, weight: Arc::new(RwLock::new(None)), steps: Arc::new(RwLock::new(None)), records: Arc::new(RwLock::new(HashMap::new())), }; s.populate_records().await; Ok(s) } pub fn weight(&self) -> Option> { (*self.weight.read().unwrap()).as_ref().map(|w| w.weight) } pub fn set_weight(&self, new_weight: si::Kilogram) { let mut record = self.weight.write().unwrap(); let new_record = match *record { Some(ref rstate) => rstate.clone().with_value(ft_core::Weight { date: self.date, weight: new_weight, }), None => RecordState::New(Record { id: RecordId::default(), data: ft_core::Weight { date: self.date, weight: new_weight, }, }), }; *record = Some(new_record); } pub fn steps(&self) -> Option { (*self.steps.read().unwrap()).as_ref().map(|w| w.count) } pub fn set_steps(&self, new_count: u32) { let mut record = self.steps.write().unwrap(); let new_record = match *record { Some(ref rstate) => rstate.clone().with_value(ft_core::Steps { date: self.date, count: new_count, }), None => RecordState::New(Record { id: RecordId::default(), data: ft_core::Steps { date: self.date, count: new_count, }, }), }; *record = Some(new_record); } pub fn new_time_distance(&self, activity: TimeDistanceActivity) -> Record { let id = RecordId::default(); let workout = TimeDistance { datetime: chrono::Local::now().into(), activity, distance: None, duration: None, comments: None, }; let tr = TraxRecord::from(workout.clone()); self.records .write() .unwrap() .insert(id, RecordState::New(Record { id, data: tr })); Record { id, data: workout } } pub fn time_distance_records(&self) -> Vec> { self.records .read() .unwrap() .iter() .filter(|(_, record)| record.exists()) .filter_map(|(id, record_state)| match **record_state { TraxRecord::TimeDistance(ref workout) => Some(Record { id: *id, data: workout.clone(), }), _ => None, }) .collect() } pub fn time_distance_summary( &self, activity: TimeDistanceActivity, ) -> (si::Meter, si::Second) { self.time_distance_records() .into_iter() .filter(|rec| rec.data.activity == activity) .fold( (0. * si::M, 0. * si::S), |(distance, duration), workout| match (workout.data.distance, workout.data.duration) { (Some(distance_), Some(duration_)) => { (distance + distance_, duration + duration_) } (Some(distance_), None) => (distance + distance_, duration), (None, Some(duration_)) => (distance, duration + duration_), (None, None) => (distance, duration), }, ) } pub fn update_record(&self, update: Record) { let mut records = self.records.write().unwrap(); records .entry(update.id) .and_modify(|record| record.set_value(update.data)); } pub fn records(&self) -> Vec> { let read_lock = self.records.read().unwrap(); read_lock .iter() .filter_map(|(_, record_state)| record_state.data()) .cloned() .collect::>>() } #[allow(unused)] fn get_record(&self, id: &RecordId) -> Option> { let record_set = self.records.read().unwrap(); record_set.get(id).map(|record| Record { id: *id, data: (**record).clone(), }) } pub fn remove_record(&self, id: RecordId) { let mut record_set = self.records.write().unwrap(); let updated_record = match record_set.remove(&id) { Some(RecordState::Original(r)) => Some(RecordState::Deleted(r)), Some(RecordState::New(_)) => None, Some(RecordState::Updated(r)) => Some(RecordState::Deleted(r)), Some(RecordState::Deleted(r)) => Some(RecordState::Deleted(r)), None => None, }; if let Some(updated_record) = updated_record { record_set.insert(id, updated_record); } } pub fn save(&self) { let s = self.clone(); glib::spawn_future(async move { s.async_save().await }); } pub async fn async_save(&self) { let weight_record = self.weight.read().unwrap().clone(); match weight_record { Some(RecordState::New(data)) => { let _ = self .provider .put_record(TraxRecord::Weight(data.data)) .await; } Some(RecordState::Original(_)) => {} Some(RecordState::Updated(weight)) => { let _ = self .provider .update_record(Record { id: weight.id, data: TraxRecord::Weight(weight.data), }) .await; } Some(RecordState::Deleted(_)) => {} None => {} } let steps_record = self.steps.read().unwrap().clone(); match steps_record { Some(RecordState::New(data)) => { let _ = self.provider.put_record(TraxRecord::Steps(data.data)).await; } Some(RecordState::Original(_)) => {} Some(RecordState::Updated(steps)) => { let _ = self .provider .update_record(Record { id: steps.id, data: TraxRecord::Steps(steps.data), }) .await; } Some(RecordState::Deleted(_)) => {} None => {} } let records = self .records .write() .unwrap() .drain() .map(|(_, record)| record) .collect::>>(); for record in records { println!("saving record: {:?}", record); match record { RecordState::New(data) => { let _ = self.provider.put_record(data.data).await; } RecordState::Original(_) => {} RecordState::Updated(r) => { let _ = self.provider.update_record(r.clone()).await; } RecordState::Deleted(r) => { let _ = self.provider.delete_record(r.id).await; } } } self.populate_records().await; } pub async fn revert(&self) { self.populate_records().await; } async fn populate_records(&self) { let records = self.provider.records(self.date, self.date).await.unwrap(); let (weight_records, records): (Vec>, Vec>) = records.into_iter().partition(|r| r.data.is_weight()); let (step_records, records): (Vec>, Vec>) = records.into_iter().partition(|r| r.data.is_steps()); *self.weight.write().unwrap() = weight_records .first() .and_then(|r| match r.data { TraxRecord::Weight(ref w) => Some((r.id, w.clone())), _ => None, }) .map(|(id, w)| RecordState::Original(Record { id, data: w })); *self.steps.write().unwrap() = step_records .first() .and_then(|r| match r.data { TraxRecord::Steps(ref w) => Some((r.id, w.clone())), _ => None, }) .map(|(id, w)| RecordState::Original(Record { id, data: w })); *self.records.write().unwrap() = records .into_iter() .map(|r| (r.id, RecordState::Original(r))) .collect::>>(); } } #[cfg(test)] mod test { use super::*; use async_trait::async_trait; use chrono::{DateTime, FixedOffset}; use dimensioned::si; use emseries::Record; #[derive(Clone, Debug)] struct MockProvider { records: Arc>>>, put_records: Arc>>>, updated_records: Arc>>>, deleted_records: Arc>>, } impl MockProvider { fn new(records: Vec>) -> Self { let record_map = records .into_iter() .map(|r| (r.id, r)) .collect::>>(); Self { records: Arc::new(RwLock::new(record_map)), put_records: Arc::new(RwLock::new(vec![])), updated_records: Arc::new(RwLock::new(vec![])), deleted_records: Arc::new(RwLock::new(vec![])), } } } #[async_trait] impl RecordProvider for MockProvider { async fn records( &self, start: NaiveDate, end: NaiveDate, ) -> Result>, ReadError> { let start = emseries::Timestamp::Date(start); let end = emseries::Timestamp::Date(end); Ok(self .records .read() .unwrap() .iter() .map(|(_, r)| r) .filter(|r| r.timestamp() >= start && r.timestamp() <= end) .cloned() .collect::>>()) } async fn put_record(&self, record: TraxRecord) -> Result { let id = RecordId::default(); let record = Record { id: id, data: record, }; self.put_records.write().unwrap().push(record.clone()); self.records.write().unwrap().insert(id, record); Ok(id) } async fn update_record(&self, record: Record) -> Result<(), WriteError> { println!("updated record: {:?}", record); self.updated_records.write().unwrap().push(record.clone()); self.records.write().unwrap().insert(record.id, record); Ok(()) } async fn delete_record(&self, id: RecordId) -> Result<(), WriteError> { self.deleted_records.write().unwrap().push(id); let _ = self.records.write().unwrap().remove(&id); Ok(()) } } async fn create_empty_view_model() -> (DayDetailViewModel, MockProvider) { let provider = MockProvider::new(vec![]); let oct_13 = chrono::NaiveDate::from_ymd_opt(2023, 10, 13).unwrap(); let model = DayDetailViewModel::new(oct_13, provider.clone()) .await .unwrap(); (model, provider) } async fn create_view_model() -> (DayDetailViewModel, MockProvider) { let oct_12 = chrono::NaiveDate::from_ymd_opt(2023, 10, 12).unwrap(); let oct_13 = chrono::NaiveDate::from_ymd_opt(2023, 10, 13).unwrap(); let oct_13_am: DateTime = oct_13 .clone() .and_hms_opt(3, 28, 0) .unwrap() .and_utc() .with_timezone(&FixedOffset::east_opt(5 * 3600).unwrap()); let provider = MockProvider::new(vec![ Record { id: RecordId::default(), data: TraxRecord::Weight(ft_core::Weight { date: oct_12, weight: 93. * si::KG, }), }, Record { id: RecordId::default(), data: TraxRecord::Weight(ft_core::Weight { date: oct_13, weight: 95. * si::KG, }), }, Record { id: RecordId::default(), data: TraxRecord::Steps(ft_core::Steps { date: oct_13, count: 2500, }), }, Record { id: RecordId::default(), data: TraxRecord::TimeDistance(ft_core::TimeDistance { datetime: oct_13_am.clone(), activity: TimeDistanceActivity::BikeRide, distance: Some(15000. * si::M), duration: Some(3600. * si::S), comments: Some("somecomments present".to_owned()), }), }, ]); let model = DayDetailViewModel::new(oct_13, provider.clone()) .await .unwrap(); (model, provider) } #[tokio::test] async fn it_honors_only_the_first_weight_and_step_record() { let (view_model, _provider) = create_view_model().await; assert_eq!(view_model.weight(), Some(95. * si::KG)); assert_eq!(view_model.steps(), Some(2500)); } #[tokio::test] async fn it_can_create_a_weight_and_stepcount() { let (view_model, provider) = create_empty_view_model().await; assert_eq!(view_model.weight(), None); assert_eq!(view_model.steps(), None); view_model.set_weight(95. * si::KG); view_model.set_steps(250); assert_eq!(view_model.weight(), Some(95. * si::KG)); assert_eq!(view_model.steps(), Some(250)); view_model.set_weight(93. * si::KG); view_model.set_steps(255); assert_eq!(view_model.weight(), Some(93. * si::KG)); assert_eq!(view_model.steps(), Some(255)); view_model.async_save().await; println!("provider: {:?}", provider); assert_eq!(provider.put_records.read().unwrap().len(), 2); assert_eq!(provider.updated_records.read().unwrap().len(), 0); assert_eq!(provider.deleted_records.read().unwrap().len(), 0); } #[tokio::test] async fn it_can_construct_new_records() { let (view_model, provider) = create_empty_view_model().await; assert_eq!( view_model.time_distance_summary(TimeDistanceActivity::BikeRide), (0. * si::M, 0. * si::S) ); let mut record = view_model.new_time_distance(TimeDistanceActivity::BikeRide); record.data.duration = Some(60. * si::S); view_model.async_save().await; assert_eq!(provider.put_records.read().unwrap().len(), 1); assert_eq!(provider.updated_records.read().unwrap().len(), 0); assert_eq!(provider.deleted_records.read().unwrap().len(), 0); } #[tokio::test] async fn it_can_update_a_new_record_before_saving() { let (view_model, provider) = create_empty_view_model().await; assert_eq!( view_model.time_distance_summary(TimeDistanceActivity::BikeRide), (0. * si::M, 0. * si::S) ); let mut record = view_model.new_time_distance(TimeDistanceActivity::BikeRide); record.data.duration = Some(60. * si::S); let record = record.map(TraxRecord::TimeDistance); view_model.update_record(record.clone()); assert_eq!(view_model.get_record(&record.id), Some(record)); assert_eq!( view_model.time_distance_summary(TimeDistanceActivity::BikeRide), (0. * si::M, 60. * si::S) ); assert_eq!( view_model.time_distance_summary(TimeDistanceActivity::Running), (0. * si::M, 0. * si::S) ); view_model.async_save().await; assert_eq!(provider.put_records.read().unwrap().len(), 1); assert_eq!(provider.updated_records.read().unwrap().len(), 0); assert_eq!(provider.deleted_records.read().unwrap().len(), 0); } #[tokio::test] async fn it_can_update_an_existing_record() { let (view_model, provider) = create_view_model().await; let mut workout = view_model.time_distance_records().first().cloned().unwrap(); workout.data.duration = Some(1800. * si::S); view_model.update_record(workout.map(TraxRecord::TimeDistance)); assert_eq!( view_model.time_distance_summary(TimeDistanceActivity::BikeRide), (15000. * si::M, 1800. * si::S) ); view_model.async_save().await; assert_eq!(provider.put_records.read().unwrap().len(), 0); assert_eq!(provider.updated_records.read().unwrap().len(), 1); assert_eq!(provider.deleted_records.read().unwrap().len(), 0); } #[tokio::test] async fn it_can_remove_a_new_record() { let (view_model, provider) = create_empty_view_model().await; assert_eq!( view_model.time_distance_summary(TimeDistanceActivity::BikeRide), (0. * si::M, 0. * si::S) ); let record = view_model.new_time_distance(TimeDistanceActivity::BikeRide); view_model.remove_record(record.id); view_model.save(); assert_eq!(provider.put_records.read().unwrap().len(), 0); assert_eq!(provider.updated_records.read().unwrap().len(), 0); assert_eq!(provider.deleted_records.read().unwrap().len(), 0); } #[tokio::test] async fn it_can_delete_an_existing_record() { let (view_model, provider) = create_view_model().await; let workout = view_model.time_distance_records().first().cloned().unwrap(); view_model.remove_record(workout.id); assert_eq!( view_model.time_distance_summary(TimeDistanceActivity::BikeRide), (0. * si::M, 0. * si::S) ); view_model.async_save().await; assert_eq!(provider.put_records.read().unwrap().len(), 0); assert_eq!(provider.updated_records.read().unwrap().len(), 0); assert_eq!(provider.deleted_records.read().unwrap().len(), 1); } }