/* Copyright 2023, 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::types::DayInterval; use chrono::NaiveDate; use emseries::{time_range, Record, RecordId, Series, Timestamp}; use ft_core::TraxRecord; use std::{ path::PathBuf, sync::{Arc, RwLock}, }; use thiserror::Error; use tokio::runtime::Runtime; #[derive(Debug, Error)] pub enum AppError { #[error("no database loaded")] NoDatabase, #[error("failed to open the database")] FailedToOpenDatabase, #[error("unhandled error")] Unhandled, } /// The real, headless application. This is where all of the logic will reside. #[derive(Clone)] pub struct App { runtime: Arc, database: Arc>>>, } impl App { pub fn new(db_path: Option) -> Self { let database = db_path.map(|path| Series::open(path).unwrap()); let runtime = Arc::new( tokio::runtime::Builder::new_multi_thread() .enable_all() .build() .unwrap(), ); let s = Self { runtime, database: Arc::new(RwLock::new(database)), }; s } pub async fn records( &self, start: NaiveDate, end: NaiveDate, ) -> Result>, AppError> { let db = self.database.clone(); self.runtime .spawn_blocking(move || { if let Some(ref db) = *db.read().unwrap() { let records = db .search(time_range( Timestamp::Date(start), true, Timestamp::Date(end), true, )) .map(|record| record.clone()) .collect::>>(); Ok(records) } else { Err(AppError::NoDatabase) } }) .await .unwrap() } pub async fn put_record(&self, record: TraxRecord) -> Result { let db = self.database.clone(); self.runtime .spawn_blocking(move || { if let Some(ref mut db) = *db.write().unwrap() { let id = db.put(record).unwrap(); Ok(id) } else { Err(AppError::NoDatabase) } }) .await .unwrap() .map_err(|_| AppError::Unhandled) } pub async fn update_record(&self, record: Record) -> Result<(), AppError> { let db = self.database.clone(); self.runtime .spawn_blocking(move || { if let Some(ref mut db) = *db.write().unwrap() { db.update(record).map_err(|_| AppError::Unhandled) } else { Err(AppError::NoDatabase) } }) .await .unwrap() .map_err(|_| AppError::Unhandled) } pub async fn open_db(&self, path: PathBuf) -> Result<(), AppError> { let db_ref = self.database.clone(); self.runtime .spawn_blocking(move || { let db = Series::open(path).map_err(|_| AppError::FailedToOpenDatabase)?; *db_ref.write().unwrap() = Some(db); Ok(()) }) .await .unwrap() } }