use std::error::Error;
use std::io::ErrorKind;
use std::{env, fs, path::PathBuf};
use directories::ProjectDirs;
use chrono::{DateTime, Local};
use serde::{Deserialize, Serialize};
pub type Stack = Vec<Item>;
type ItemHistory = Vec<(String, DateTime<Local>)>;
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Item {
pub contents: String,
pub history: ItemHistory,
}
impl Item {
pub fn new(contents: &str) -> Self {
Item {
contents: contents.to_string(),
history: vec![("created".to_string(), Local::now())],
}
}
pub fn mark_completed(&mut self) {
let event = ("completed".to_string(), Local::now());
self.history.push(event);
}
pub fn mark_deleted(&mut self) {
let event = ("deleted".to_string(), Local::now());
self.history.push(event);
}
pub fn mark_restored(&mut self) {
let event = ("restored".to_string(), Local::now());
self.history.push(event);
}
}
pub struct DataStore {
pub working_dir: WorkingDir,
pub data_format: DataFormat,
}
#[derive(Clone)]
pub enum WorkingDir {
HomeDir,
Dir(String),
}
pub enum DataFormat {
SigiJson,
}
impl DataStore {
pub fn load(&self, stack_name: &str) -> Result<Stack, impl Error> {
match self.data_format {
DataFormat::SigiJson => load_json_from(stack_name, &self.dir()),
}
}
pub fn save(&self, stack_name: &str, items: Stack) -> Result<(), impl Error> {
match self.data_format {
DataFormat::SigiJson => save_json_to(stack_name, &self.dir(), items),
}
}
pub fn list_stacks(&self) -> Result<Vec<String>, impl Error> {
match self.data_format {
DataFormat::SigiJson => list_json_from(&self.dir()),
}
}
fn dir(&self) -> String {
match self.working_dir.clone() {
WorkingDir::HomeDir => sigi_path(),
WorkingDir::Dir(dir) => dir,
}
}
}
fn save_json_to(stack_name: &str, dest_dir: &str, items: Stack) -> Result<(), impl Error> {
let data_path: String = sigi_file(dest_dir, stack_name);
let json: String = serde_json::to_string(&items).unwrap();
let result = fs::write(&data_path, &json);
if result.is_err() && result.as_ref().unwrap_err().kind() == ErrorKind::NotFound {
fs::create_dir_all(dest_dir).unwrap();
fs::write(data_path, json)
} else {
result
}
}
fn load_json_from(stack_name: &str, dest_dir: &str) -> Result<Stack, impl Error> {
let data_path: String = sigi_file(dest_dir, stack_name);
let read_result = fs::read_to_string(data_path);
if read_result.is_err() && read_result.as_ref().unwrap_err().kind() == ErrorKind::NotFound {
return Ok(vec![]);
}
let json = read_result.unwrap();
let result = serde_json::from_str(&json);
if result.is_err() {
let v1result = v1_load(&json);
if let Ok(v1stack) = v1result {
return Ok(v1_to_modern(v1stack));
}
}
result
}
fn list_json_from(dest_dir: &str) -> Result<Vec<String>, impl Error> {
let dot_json = ".json";
fs::read_dir(dest_dir).map(|files| {
files
.map(|file| file.unwrap().file_name().into_string().unwrap())
.filter(|filename| filename.ends_with(dot_json))
.map(|filename| filename.strip_suffix(dot_json).unwrap().to_string())
.collect::<Vec<_>>()
})
}
fn v1_sigi_path() -> PathBuf {
let home = env::var("HOME").or_else(|_| env::var("HOMEDRIVE")).unwrap();
let path = format!("{}/.local/share/sigi", home);
PathBuf::from(&path)
}
fn sigi_path() -> String {
if let Ok(dir) = env::var("SIGI_HOME") {
return dir;
}
let sigi_base = ProjectDirs::from("org", "sigi-cli", "sigi").unwrap();
let sigi_path = sigi_base.data_dir();
let v1_path = v1_sigi_path();
if v1_path.exists() && !sigi_path.exists() {
fs::rename(v1_path, sigi_path).unwrap();
}
sigi_path.to_string_lossy().to_string()
}
fn sigi_file(sigi_dir: &str, filename: &str) -> String {
let path = format!("{}/{}.json", sigi_dir, filename);
PathBuf::from(&path).to_string_lossy().to_string()
}
#[derive(Serialize, Deserialize, Debug, Clone)]
struct V1Item {
name: String,
created: DateTime<Local>,
succeeded: Option<DateTime<Local>>,
failed: Option<DateTime<Local>>,
}
type V1Stack = Vec<V1Item>;
fn v1_load(json_blob: &str) -> Result<V1Stack, impl Error> {
serde_json::from_str(json_blob)
}
fn v1_to_modern(v1stack: V1Stack) -> Stack {
v1stack
.into_iter()
.map(|v1item| {
let mut history: ItemHistory = vec![
Some(("created", v1item.created)),
v1item.succeeded.map(|dt| ("completed", dt)),
v1item.failed.map(|dt| ("deleted", dt)),
]
.into_iter()
.flatten()
.map(|(s, dt)| (s.to_string(), dt))
.collect();
history.sort_by_key(|(_, dt)| *dt);
Item {
contents: v1item.name,
history,
}
})
.collect()
}