use super::{anyhow, bail, env, serde, serde_json, tempfile, Result};
use serde::Deserialize;
use serde::Serialize;
use serde_json::{from_value, to_value, Value};
use std::collections::HashMap;
use std::fs::File;
use std::io::{BufReader, BufWriter};
use std::path::{Path, PathBuf};
#[derive(Debug)]
pub struct Data {
inner: HashMap<String, Value>,
file_name: PathBuf,
}
impl Data {
pub fn load<P: AsRef<Path>>(p: P) -> Result<Self> {
if p.as_ref().as_os_str().is_empty() {
bail!("File name to load data from cannot be empty");
}
let filename = p
.as_ref()
.file_name()
.ok_or_else(|| anyhow!("invalid file name"))?;
let wf_data_path = env::workflow_data().ok_or_else(|| {
anyhow!("missing env variable for data dir. forgot to set workflow bundle id?")
})?;
let wf_data_fn = wf_data_path.join(filename);
let inner = Self::read_data_from_disk(&wf_data_fn)
.or_else(|_| -> Result<_> { Ok(HashMap::new()) })?;
Ok(Data {
inner,
file_name: wf_data_fn,
})
}
pub fn set<K, V>(&mut self, k: K, v: &V) -> Result<()>
where
K: Into<String>,
V: Serialize,
{
let v = to_value(v)?;
self.inner.insert(k.into(), v);
Self::write_data_to_disk(&self.file_name, &self.inner)
}
pub fn get<K, V>(&self, k: K) -> Option<V>
where
K: AsRef<str>,
V: for<'d> Deserialize<'d>,
{
self.inner
.get(k.as_ref())
.and_then(|v| from_value(v.clone()).ok())
}
pub fn clear(&mut self) {
self.inner.clear();
}
pub fn save_to_file<P, V>(p: P, data: &V) -> Result<()>
where
P: AsRef<Path>,
V: Serialize,
{
let filename = p
.as_ref()
.file_name()
.ok_or_else(|| anyhow!("invalid file name"))?;
let p = env::workflow_cache()
.map(|wfc| wfc.join(filename))
.ok_or_else(|| {
anyhow!("missing env variable for cache dir. forgot to set workflow bundle id?")
})?;
debug!("saving to: {}", p.to_str().expect(""));
Self::write_data_to_disk(p, data)
}
fn write_data_to_disk<P, V>(p: P, data: &V) -> Result<()>
where
P: AsRef<Path> + std::fmt::Debug,
V: Serialize,
{
use std::fs;
use tempfile::Builder;
let wfc = env::workflow_cache().ok_or_else(|| {
anyhow!("missing env variable for cache dir. forgot to set workflow bundle id?")
})?;
let named_tempfile = Builder::new()
.prefix("alfred_rs_temp")
.suffix(".json")
.rand_bytes(5)
.tempfile_in(wfc)?;
let fn_temp = named_tempfile.as_ref();
File::create(&fn_temp).and_then(|fp| {
let buf_writer = BufWriter::with_capacity(0x1000, fp);
serde_json::to_writer(buf_writer, data)?;
Ok(())
})?;
fs::rename(fn_temp, p)?;
Ok(())
}
pub fn load_from_file<P, V>(p: P) -> Option<V>
where
P: AsRef<Path>,
V: for<'d> Deserialize<'d>,
{
let p = env::workflow_cache()
.and_then(|wfc| p.as_ref().file_name().map(|name| wfc.join(name)))?;
debug!("loading from: {}", p.to_str().expect(""));
Self::read_data_from_disk(&p).ok()
}
fn read_data_from_disk<V>(p: &Path) -> Result<V>
where
V: for<'d> Deserialize<'d>,
{
File::open(p).map_err(Into::into).and_then(|fp| {
let buf_reader = BufReader::with_capacity(0x1000, fp);
let d: V = serde_json::from_reader(buf_reader)?;
Ok(d)
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::prelude::*;
use std::env as StdEnv;
use std::ffi::OsStr;
use std::fs::remove_file;
use std::{thread, time};
use tempfile::Builder;
#[test]
fn it_sets_gets_data() {
#[derive(Serialize, Deserialize)]
struct User {
name: String,
age: usize,
}
setup_workflow_env_vars(true);
let user = User {
name: "Hamid".to_string(),
age: 42,
};
{
let mut wf_data: Data = Data::load("settings_test.json").unwrap();
wf_data.set("key1", &8).unwrap();
wf_data.set("key2", &user).unwrap();
wf_data.set("date", &Utc::now()).unwrap();
println!("{:?}", wf_data);
}
{
let wf_data = Data::load("settings_test.json").unwrap();
assert_eq!(3, wf_data.inner.len());
let user: User = wf_data.get("key2").unwrap();
assert_eq!(42, user.age);
let x: i8 = wf_data.get("key1").unwrap();
assert_eq!(8, x);
let _last_log: DateTime<Utc> = wf_data.get("date").expect("log date was not set");
}
}
#[test]
fn it_saves_loads_from_file() {
let wfc = setup_workflow_env_vars(true);
let path = wfc.join("_test_saves_loads_from_file");
let _r = remove_file(&path);
let now = Utc::now();
Data::save_to_file(&path, &now).expect("couldn't write to file");
let what_now: DateTime<Utc> =
Data::load_from_file(path).expect("couldn't get value from test file");
assert_eq!(now, what_now);
}
#[test]
fn it_overwrites_cached_data_file() {
let wfc = setup_workflow_env_vars(true);
let path = wfc.join("_test_it_overwrites_cached_data_file");
let _r = remove_file(&path);
let ten_millis = time::Duration::from_millis(10);
let now1 = Utc::now();
Data::save_to_file(&path, &now1).expect("couldn't write to file");
thread::sleep(ten_millis);
let now2 = Utc::now();
Data::save_to_file(&path, &now2).expect("couldn't write to file");
let what_now: DateTime<Utc> =
Data::load_from_file(path).expect("couldn't get value from test file");
assert_eq!(now2, what_now);
}
pub(super) fn setup_workflow_env_vars(secure_temp_dir: bool) -> PathBuf {
let path = if secure_temp_dir {
Builder::new()
.prefix("alfred_workflow_test")
.rand_bytes(5)
.tempdir()
.unwrap()
.into_path()
} else {
StdEnv::temp_dir()
};
{
let v: &OsStr = path.as_ref();
StdEnv::set_var("alfred_workflow_data", v);
StdEnv::set_var("alfred_workflow_cache", v);
StdEnv::set_var("alfred_workflow_uid", "workflow.B0AC54EC-601C");
StdEnv::set_var(
"alfred_workflow_name",
"YouForgotTo/フ:Name好YouráOwnسWork}flowッ",
);
StdEnv::set_var("alfred_workflow_bundleid", "MY_BUNDLE_ID");
StdEnv::set_var("alfred_workflow_version", "0.10.5");
}
path
}
}