use std::{
fmt::{Debug, Display},
path::{Path, PathBuf},
};
use anyhow::Result;
use crate::config::{TukaiLayoutName, TypingDuration};
use crate::file_handler::FileHandler;
use super::stats::Stat;
#[derive(Debug)]
pub struct StorageHandlerError {
message: String,
}
impl Display for StorageHandlerError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "StorageHandlerError: {}", self.message)
}
}
impl std::error::Error for StorageHandlerError {}
#[allow(unused)]
impl StorageHandlerError {
fn new(message: String) -> Self {
Self { message }
}
}
pub type StorageData = (Vec<Stat>, TypingDuration, TukaiLayoutName, bool, usize);
static DEFAULT_STORAGE_DATA: StorageData = (
Vec::<Stat>::new(),
TypingDuration::Minute,
TukaiLayoutName::Iced,
false,
0,
);
pub struct StorageHandler {
file_path: PathBuf,
data: Option<StorageData>,
}
pub struct StatOverview {
pub total_stats_count: usize,
pub total_average_wpm: usize,
pub total_average_accuracy: f64,
}
impl StorageHandler {
pub fn new<P: AsRef<Path>>(file_path: P) -> Self {
let local_dir_path = dirs::data_local_dir().unwrap_or(PathBuf::from("/tmp"));
let full_path = local_dir_path.join("tukai").join(file_path);
Self {
file_path: full_path,
data: None,
}
}
#[cfg(test)]
pub fn delete_file(&self) -> Result<()> {
std::fs::remove_file(&self.file_path)?;
Ok(())
}
fn init_empty_data(&mut self) -> Result<()> {
let data = self
.data
.get_or_insert_with(|| DEFAULT_STORAGE_DATA.clone());
let data_bytes = bincode::serialize(&data).unwrap();
FileHandler::write_bytes_into_file(&self.file_path, &data_bytes)?;
Ok(())
}
pub fn init(mut self) -> Result<Self> {
if !self.file_path.exists() {
self.init_empty_data()?;
return Ok(self);
}
let data_bytes = FileHandler::read_bytes_from_file(&self.file_path)?;
match bincode::deserialize(&data_bytes) {
Ok(data) => self.data = data,
Err(_) => self.init_empty_data()?,
};
Ok(self)
}
pub fn get_data(&self) -> &StorageData {
if let Some(storage_data) = &self.data {
storage_data
} else {
&DEFAULT_STORAGE_DATA
}
}
pub fn get_data_mut(&mut self) -> Option<&mut StorageData> {
self.data.as_mut()
}
pub fn get_data_for_overview(&self) -> StatOverview {
let stats = &self.get_data().0;
let (sum_wpm, sum_accuracy) = stats.iter().fold((0, 0.0), |(wpm, acc), stat| {
(wpm + stat.get_average_wpm(), acc + stat.get_accuracy())
});
let accuracy = (sum_accuracy / stats.len() as f64).round();
StatOverview {
total_stats_count: stats.len(),
total_average_wpm: sum_wpm.checked_div(stats.len()).unwrap_or(0),
total_average_accuracy: if accuracy.is_nan() { 0.0 } else { accuracy },
}
}
pub fn get_data_for_chart(&self) -> (usize, Vec<(f64, f64)>) {
let stats = &self.get_data().0;
let mut best_wpm = 0_usize;
let dataset = stats
.iter()
.enumerate()
.rev()
.map(|(index, stat)| {
let stat_wpm = stat.get_average_wpm();
best_wpm = best_wpm.max(stat_wpm);
(index as f64, stat_wpm as f64)
})
.collect::<Vec<(f64, f64)>>();
(best_wpm.max(100), dataset)
}
pub fn get_data_stats_reversed(&self) -> Vec<Stat> {
let stats = &self.get_data().0;
stats.iter().rev().cloned().collect::<Vec<Stat>>()
}
pub fn get_data_stats_best(&self) -> Vec<Stat> {
let mut data = self.get_data().0.clone();
data.sort_by_key(|b| std::cmp::Reverse(b.get_average_wpm()));
data
}
pub fn get_typing_duration(&self) -> TypingDuration {
self.get_data().1.clone()
}
pub fn get_layout_name(&self) -> TukaiLayoutName {
self.get_data().2.clone()
}
pub fn get_language_index(&self) -> usize {
self.get_data().4
}
pub fn get_has_transparent_bg(&self) -> bool {
self.get_data().3
}
pub fn flush(&self) -> Result<()> {
let data_bytes = bincode::serialize(&self.data)?;
FileHandler::write_bytes_into_file(&self.file_path, &data_bytes)
}
pub fn insert_into_stats(&mut self, stat: &Stat) -> bool {
if let Some(storage_data) = self.get_data_mut() {
storage_data.0.push(stat.clone());
}
self.flush().is_ok()
}
pub fn set_typing_duration(&mut self, typin_duration: TypingDuration) {
if let Some(storage_data) = self.get_data_mut() {
storage_data.1 = typin_duration;
}
}
pub fn set_layout(&mut self, layout_name_changed: TukaiLayoutName) {
if let Some(storage_data) = self.get_data_mut() {
storage_data.2 = layout_name_changed;
}
}
pub fn set_language_index(&mut self, language_index: usize) {
if let Some(storage_data) = self.get_data_mut() {
storage_data.4 = language_index;
}
}
pub fn set_transparent_bg(&mut self, state: bool) {
if let Some(storage_data) = self.get_data_mut() {
storage_data.3 = state;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::TypingDuration;
use uuid::Uuid;
fn get_storage_handler() -> StorageHandler {
let storage_helper = StorageHandler::new(format!("tests/{}.tukai", Uuid::new_v4()))
.init()
.expect("Failed to initialize storage file");
storage_helper
}
fn get_test_stat() -> Stat {
Stat::new(TypingDuration::Minute, 80, 5)
}
#[test]
fn storage_load() {
let storage_handler = get_storage_handler();
let _storage_data = storage_handler.get_data();
storage_handler
.delete_file()
.expect("Error occured while deleting file");
}
#[test]
fn storage_insert_into_data_stats() {
let mut storage_handler = get_storage_handler();
let stat = get_test_stat();
assert!(
storage_handler.insert_into_stats(&stat),
"Insert into the storage error occured"
);
let _stats = storage_handler.get_data_stats_reversed();
}
#[test]
fn flush_data() {
let mut storage_handler = get_storage_handler();
storage_handler.insert_into_stats(&get_test_stat());
assert!(storage_handler.flush().is_ok());
storage_handler
.delete_file()
.expect("Error occured while deleting file");
}
#[test]
fn load_flushed_data() {
let mut storage_handler = get_storage_handler();
storage_handler.insert_into_stats(&get_test_stat());
println!("{:?}", storage_handler.get_data());
let data = storage_handler.get_data();
println!("{:?}", data);
storage_handler
.delete_file()
.expect("Error occured while deleting file");
}
}