/* 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, WriteError}, types::WeightFormatter, }; use chrono::NaiveDate; use emseries::{Record, RecordId, Recordable}; use ft_core::TraxRecord; use std::{ collections::HashMap, ops::Deref, sync::{Arc, RwLock}, }; #[derive(Clone, Debug)] enum RecordState { Original(Record), New(T), Updated(Record), #[allow(unused)] Deleted(Record), } impl RecordState { #[allow(unused)] fn id(&self) -> Option<&RecordId> { match self { RecordState::Original(ref r) => Some(&r.id), RecordState::New(ref r) => None, RecordState::Updated(ref r) => Some(&r.id), RecordState::Deleted(ref r) => Some(&r.id), } } fn with_value(self, value: T) -> RecordState { match self { RecordState::Original(r) => RecordState::Updated(Record { data: value, ..r }), RecordState::New(_) => RecordState::New(value), RecordState::Updated(r) => RecordState::Updated(Record { data: value, ..r }), RecordState::Deleted(r) => RecordState::Updated(Record { data: value, ..r }), } } #[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, RecordState::Updated(ref r) => &r.data, RecordState::Deleted(ref r) => &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 (weight_records, records): (Vec>, Vec>) = provider .records(date, date) .await? .into_iter() .partition(|r| r.data.is_weight()); let (step_records, records): (Vec>, Vec>) = records.into_iter().partition(|r| r.data.is_steps()); Ok(Self { provider: Arc::new(provider), date, weight: Arc::new(RwLock::new( weight_records .first() .and_then(|r| match r.data { TraxRecord::Weight(ref w) => Some((r.id.clone(), w.clone())), _ => None, }) .map(|(id, w)| RecordState::Original(Record { id, data: w })), )), steps: Arc::new(RwLock::new( step_records .first() .and_then(|r| match r.data { TraxRecord::Steps(ref w) => Some((r.id.clone(), w.clone())), _ => None, }) .map(|(id, w)| RecordState::Original(Record { id, data: w })), )), records: Arc::new(RwLock::new( records .into_iter() .map(|r| (r.id.clone(), RecordState::Original(r))) .collect::>>(), )), }) } pub fn weight(&self) -> Option { (*self.weight.read().unwrap()) .as_ref() .map(|w| WeightFormatter::from(w.weight)) } pub fn set_weight(&self, new_weight: WeightFormatter) { 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(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(ft_core::Steps { date: self.date, count: new_count, }), }; *record = Some(new_record); } pub fn save(&self) { glib::spawn_future({ let s = self.clone(); async move { let weight_record = s.weight.read().unwrap().clone(); match weight_record { Some(RecordState::New(data)) => { let _ = s.provider.put_record(TraxRecord::Weight(data)).await; } Some(RecordState::Original(_)) => {} Some(RecordState::Updated(weight)) => { let _ = s .provider .update_record(Record { id: weight.id, data: TraxRecord::Weight(weight.data), }) .await; } Some(RecordState::Deleted(_)) => {} None => {} } let steps_record = s.steps.read().unwrap().clone(); match steps_record { Some(RecordState::New(data)) => { let _ = s.provider.put_record(TraxRecord::Steps(data)).await; } Some(RecordState::Original(_)) => {} Some(RecordState::Updated(steps)) => { let _ = s .provider .update_record(Record { id: steps.id, data: TraxRecord::Steps(steps.data), }) .await; } Some(RecordState::Deleted(_)) => {} None => {} } let records = s .records .write() .unwrap() .drain() .map(|(_, record)| record) .collect::>>(); for record in records { match record { RecordState::New(data) => { let _ = s.provider.put_record(data).await; } RecordState::Original(_) => {} RecordState::Updated(r) => { let _ = s.provider.update_record(r.clone()).await; } RecordState::Deleted(_) => unimplemented!(), } } } }); } pub fn revert(&self) { unimplemented!(); } } #[cfg(test)] mod test { use super::*; use async_trait::async_trait; use dimensioned::si; use emseries::Record; struct MockProvider { records: 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 .iter() .filter(|r| r.timestamp() >= start && r.timestamp() <= end) .cloned() .collect::>>()) } async fn put_record(&self, record: TraxRecord) -> Result { Err(WriteError::NoDatabase) } async fn update_record(&self, record: Record) -> Result<(), WriteError> { Err(WriteError::NoDatabase) } async fn delete_record(&self, id: RecordId) -> Result<(), WriteError> { Err(WriteError::NoDatabase) } } async fn with_view_model(test: TestFn) where TestFn: Fn(DayDetailViewModel), { 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 provider = MockProvider { records: vec![ Record { id: RecordId::default(), data: TraxRecord::Weight(ft_core::Weight { date: oct_12.clone(), weight: 93. * si::KG, }), }, Record { id: RecordId::default(), data: TraxRecord::Weight(ft_core::Weight { date: oct_13.clone(), weight: 95. * si::KG, }), }, Record { id: RecordId::default(), data: TraxRecord::Steps(ft_core::Steps { date: oct_13.clone(), count: 2500, }), }, Record { id: RecordId::default(), data: TraxRecord::Weight(ft_core::Weight { date: oct_13.clone(), weight: 91. * si::KG, }), }, Record { id: RecordId::default(), data: TraxRecord::Steps(ft_core::Steps { date: oct_13.clone(), count: 2750, }), }, ], }; let model = DayDetailViewModel::new(oct_13, provider).await.unwrap(); test(model) } #[tokio::test] async fn it_honors_only_the_first_weight_and_step_record() { with_view_model(|view_model| { assert_eq!(view_model.weight().map(|val| *val), Some(95. * si::KG)); assert_eq!(view_model.steps(), Some(2500)); }) .await; } #[test] #[ignore] fn it_enforces_one_weight_and_stepcount_per_day() {} #[test] #[ignore] fn it_can_construct_new_records() {} #[test] #[ignore] fn it_can_update_an_existing_record() {} #[test] #[ignore] fn it_can_remove_a_new_record() {} #[test] #[ignore] fn it_can_delete_an_existing_record() {} #[test] #[ignore] fn it_retrieve_records_by_workout() {} #[test] #[ignore] fn it_summarizes_records_by_workout_type() {} }