178 lines
5.3 KiB
Rust
178 lines
5.3 KiB
Rust
/*
|
|
Copyright 2023, Savanni D'Gerinel <savanni@luminescent-dreams.com>
|
|
|
|
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 <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
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<Vec<Record<TraxRecord>>, ReadError>;
|
|
async fn put_record(&self, record: TraxRecord) -> Result<RecordId, WriteError>;
|
|
async fn update_record(&self, record: Record<TraxRecord>) -> 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<Runtime>,
|
|
database: Arc<RwLock<Option<Series<TraxRecord>>>>,
|
|
}
|
|
|
|
impl App {
|
|
pub fn new(db_path: Option<PathBuf>) -> 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<Option<Record<TraxRecord>>, 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<Vec<Record<TraxRecord>>, 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::<Vec<Record<TraxRecord>>>();
|
|
Ok(records)
|
|
} else {
|
|
Err(ReadError::NoDatabase)
|
|
}
|
|
})
|
|
.await
|
|
.unwrap()
|
|
}
|
|
|
|
async fn put_record(&self, record: TraxRecord) -> Result<RecordId, WriteError> {
|
|
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<TraxRecord>) -> 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!()
|
|
}
|
|
}
|