use dirs::home_dir;
use log::{debug, trace};
use serde::{Deserialize, Serialize};
use std::fmt;
use std::fs;
use std::io::{self, Read, Write};
#[derive(Serialize, Deserialize, Clone)]
pub struct Item {
pub id: u64,
pub name: String,
pub obtained: bool,
pub created_at: String,
}
impl fmt::Display for &Item {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let obtained_s = if self.obtained { "[X]" } else { "[ ]" };
write!(f, "{}: {} {}", self.id, self.name, obtained_s)
}
}
pub trait Store {
fn persist_items(&self) -> io::Result<()>;
fn load_items(&mut self) -> io::Result<()>;
fn get_items(&self) -> io::Result<&Vec<Item>>;
fn get_item(&self, id: u64) -> io::Result<&Item>;
fn add_item(&mut self, name: String) -> io::Result<&Item>;
fn remove_item(&mut self, id: u64) -> io::Result<u64>;
}
pub struct FileStorage {
items: Vec<Item>,
}
impl Default for FileStorage {
fn default() -> Self {
Self { items: Vec::new() }
}
}
impl FileStorage {
pub fn new() -> Self {
FileStorage::default()
}
pub fn json_file_path(&self) -> io::Result<String> {
let config_path = self.config_dir_path()?;
Ok(format!("{}/{}", config_path, "items.json"))
}
pub fn config_dir_path(&self) -> io::Result<String> {
home_dir()
.map(|home| format!("{}/{}", home.display(), ".pickup"))
.ok_or_else(|| io::Error::from(io::ErrorKind::NotFound))
}
pub fn config_dir_exists(&self) -> bool {
self.config_dir_path().and_then(fs::metadata).is_ok()
}
pub fn create_config_dir(&self) -> io::Result<()> {
if self.config_dir_exists() {
return Ok(());
}
fs::create_dir_all(self.config_dir_path()?)?;
Ok(())
}
pub fn items_file_exists(&self) -> bool {
self.json_file_path().and_then(fs::metadata).is_ok()
}
pub fn create_items_file(&self) -> io::Result<()> {
if self.config_dir_exists() {
fs::File::create(self.json_file_path()?)?;
}
Ok(())
}
}
impl Store for FileStorage {
fn load_items(&mut self) -> io::Result<()> {
trace!("Getting store from user config...");
let path = self.json_file_path()?;
let mut file = fs::File::open(path)?;
let mut buf = String::new();
file.read_to_string(&mut buf)?;
let items: Vec<Item> = serde_json::from_str(&buf)?;
self.items = items;
debug!("JSON file parsed. Adding storage...");
Ok(())
}
fn persist_items(&self) -> io::Result<()> {
trace!("Persisting items to disk...");
let path = self.json_file_path()?;
let mut file = fs::OpenOptions::new()
.write(true)
.truncate(true)
.open(path)?;
let buf = serde_json::to_string(&self.items)?;
file.write_all(buf.as_bytes())?;
file.flush()?;
Ok(())
}
fn get_item(&self, id: u64) -> io::Result<&Item> {
for item in &self.items {
if item.id == id {
return Ok(&item);
}
}
Err(io::Error::new(
io::ErrorKind::NotFound,
"Invalid path to storage file",
))
}
fn get_items(&self) -> io::Result<&Vec<Item>> {
Ok(&self.items)
}
fn add_item(&mut self, name: String) -> io::Result<&Item> {
let items = self.get_items()?;
let id = items.last().map(|i| i.id + 1).unwrap_or(1);
let now = chrono::Local::now();
let item = Item {
id,
name,
obtained: false,
created_at: now.to_rfc3339(),
};
self.items.push(item);
self.persist_items()?;
Ok(self.get_item(id)?)
}
fn remove_item(&mut self, id: u64) -> io::Result<u64> {
if let Some(pos) = self.items.iter().position(|i| i.id == id) {
self.items.remove(pos);
self.persist_items()?;
Ok(id)
} else {
Err(io::Error::new(
io::ErrorKind::NotFound,
"ID to item is invalid",
))
}
}
}
#[cfg(test)]
mod test {
use super::{FileStorage, Item, Store};
use std::env;
use std::fs::{self, File};
use std::io::{self, Read, Write};
use std::path::PathBuf;
use tempfile::TempDir;
type TestResult = Result<(), Box<dyn std::error::Error>>;
#[test]
fn test_config_path() -> TestResult {
let storage = FileStorage::new();
let tmp_dir = set_config_dir()?;
let actual = storage.config_dir_path()?;
let expected = tmp_dir
.path()
.join(".pickup")
.to_owned()
.into_os_string()
.into_string()
.unwrap();
assert_eq!(actual, expected);
env::remove_var("HOME");
Ok(())
}
#[test]
fn test_json_file_path() -> TestResult {
let storage = FileStorage::new();
let tmp_dir = set_config_dir()?;
let actual = storage.json_file_path()?;
let expected = format!(
"{}/{}/{}",
tmp_dir.path().display(),
".pickup",
"items.json"
);
assert_eq!(actual, expected);
env::remove_var("HOME");
Ok(())
}
#[test]
fn test_load_items() -> TestResult {
let (_tmp, mut storage) = storage_with_item()?;
storage.load_items()?;
assert_eq!(storage.items.len(), 1);
env::remove_var("HOME");
Ok(())
}
#[test]
fn test_add_item() -> TestResult {
let (_tmp, mut storage) = storage_with_item()?;
storage.load_items()?;
let item = storage.add_item(String::from("sup"))?;
assert!(item.id > 0);
assert_eq!(item.name, "sup");
assert_eq!(storage.items.len(), 2);
env::remove_var("HOME");
Ok(())
}
#[test]
fn test_get_item() -> TestResult {
let (_tmp, mut storage) = storage_with_item()?;
storage.load_items()?;
let item = storage.get_item(1)?;
assert_eq!(item.name, "foo item");
assert_eq!(item.obtained, false);
env::remove_var("HOME");
Ok(())
}
#[test]
fn test_remove_item() -> TestResult {
let (_tmp, mut storage) = storage_with_item()?;
storage.load_items()?;
let id = storage.remove_item(1)?;
assert_eq!(storage.items.len(), 0);
assert_eq!(id, 1);
env::remove_var("HOME");
Ok(())
}
fn storage_with_item() -> io::Result<(TempDir, FileStorage)> {
let tmp_dir = set_config_dir()?;
let storage = FileStorage::new();
let json_storage_buf = b"[{
\"id\": 1,
\"name\": \"foo item\",
\"obtained\": false,
\"created_at\": \"2021-04-30T05:23:39.821Z\"
}]";
let json_path = storage.json_file_path()?;
let mut f = fs::OpenOptions::new()
.read(true)
.write(true)
.create_new(true)
.open(json_path)?;
f.write_all(json_storage_buf)?;
Ok((tmp_dir, storage))
}
fn set_config_dir() -> io::Result<TempDir> {
let tmp_dir = TempDir::new()?;
env::set_var("HOME", tmp_dir.path());
let config_dir = format!("{}/{}", tmp_dir.path().display(), ".pickup");
fs::create_dir_all(config_dir)?;
Ok(tmp_dir)
}
}