/* 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 async_trait::async_trait; 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, } #[derive(Debug, Error)] pub enum ReadError { #[error("no database loaded")] NoDatabase, } #[derive(Debug, Error)] pub enum WriteError { #[error("no database loaded")] NoDatabase, #[error("unhandled error")] Unhandled, } #[async_trait] pub trait RecordProvider: Send + Sync { async fn records( &self, start: NaiveDate, end: NaiveDate, ) -> Result>, ReadError>; async fn put_record(&self, record: TraxRecord) -> Result; async fn update_record(&self, record: Record) -> Result<(), WriteError>; async fn delete_record(&self, id: RecordId) -> Result<(), WriteError>; } /// 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(), ); Self { runtime, database: Arc::new(RwLock::new(database)), } } 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() } pub async fn get_record(&self, id: RecordId) -> Result>, AppError> { let db = self.database.clone(); self.runtime .spawn_blocking(move || { if let Some(ref db) = *db.read().unwrap() { Ok(db.get(&id)) } else { Err(AppError::NoDatabase) } }) .await .unwrap() } } #[async_trait] impl RecordProvider for App { async fn records( &self, start: NaiveDate, end: NaiveDate, ) -> Result>, ReadError> { 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, )) .cloned() .collect::>>(); Ok(records) } else { Err(ReadError::NoDatabase) } }) .await .unwrap() } 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(|_| WriteError::Unhandled) } async fn update_record(&self, record: Record) -> Result<(), WriteError> { 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(|_| WriteError::Unhandled) } async fn delete_record(&self, _id: RecordId) -> Result<(), WriteError> { unimplemented!() } }