use anyhow::{bail, Context, Result};
use log::{debug, info};
use serde::{de::DeserializeOwned, Serialize};
use std::default::Default;
use std::fs;
use std::io::{self, prelude::*};
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
fn read_json<P: AsRef<Path>>(path: P) -> Result<(String, String)> {
let mut f = fs::OpenOptions::new().read(true).open(path)?;
let mut buf = String::new();
f.read_to_string(&mut buf)?;
let mut preamble = String::new();
let mut body = String::new();
let mut seen_body = false;
for line in buf.lines() {
let trimmed = line.trim();
if trimmed.starts_with("//") || trimmed.starts_with("#") {
if !seen_body {
preamble = preamble + line + "\n";
}
body = body + "\n";
} else {
seen_body = true;
body = body + line + "\n"
}
}
Ok((preamble, body))
}
pub trait JsonLoad
where
Self: DeserializeOwned,
{
fn loaded(&mut self, _prev: Option<&mut Self>) -> Result<()> {
Ok(())
}
fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
let (_, body) = read_json(path)?;
Ok(serde_json::from_str::<Self>(&body)?)
}
}
pub trait JsonSave
where
Self: Default + Serialize,
{
fn preamble() -> Option<String> {
None
}
fn maybe_create_dfl<P: AsRef<Path>>(path_in: P) -> Result<bool> {
let path = path_in.as_ref();
if let Some(parent) = path.parent() {
fs::create_dir_all(&parent)?;
}
match fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&path)
{
Ok(mut f) => {
let data: Self = Default::default();
f.write_all(data.as_json()?.as_ref())?;
Ok(true)
}
Err(e) => match e.kind() {
io::ErrorKind::AlreadyExists => Ok(false),
_ => Err(e.into()),
},
}
}
fn as_json(&self) -> Result<String> {
let mut serialized = serde_json::to_string_pretty(&self)?;
if !serialized.ends_with("\n") {
serialized += "\n";
}
match Self::preamble() {
Some(pre) => Ok(pre + &serialized),
None => Ok(serialized),
}
}
fn save<P: AsRef<Path>>(&self, path: P) -> Result<()> {
let path: &Path = path.as_ref();
let fname = match path.file_name() {
Some(v) => v,
None => bail!("can't save to null path"),
};
let mut tmp_path = PathBuf::from(path);
tmp_path.pop();
tmp_path.push(format!(".{}.json-save-staging", &fname.to_string_lossy()));
let mut f = fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(&tmp_path)
.with_context(|| format!("opening staging file {:?}", &tmp_path))?;
f.write_all(self.as_json()?.as_ref())
.with_context(|| format!("writing staging file {:?}", &tmp_path))?;
fs::rename(&tmp_path, path)
.with_context(|| format!("moving {:?} to {:?}", &tmp_path, path))?;
Ok(())
}
}
#[derive(Clone, Debug)]
pub struct JsonConfigFile<T: JsonLoad + JsonSave> {
pub path: Option<PathBuf>,
pub loaded_mod: SystemTime,
pub data: T,
}
impl<T: JsonLoad + JsonSave + Default> Default for JsonConfigFile<T> {
fn default() -> Self {
Self {
path: None,
loaded_mod: UNIX_EPOCH,
data: Default::default(),
}
}
}
impl<T: JsonLoad + JsonSave> JsonConfigFile<T> {
pub fn load<P: AsRef<Path>>(path_in: P) -> Result<Self> {
let path = AsRef::<Path>::as_ref(&path_in);
let modified = path.metadata()?.modified()?;
let mut data = T::load(&path)?;
data.loaded(None)?;
Ok(Self {
path: Some(PathBuf::from(path)),
loaded_mod: modified,
data,
})
}
pub fn load_or_create<P: AsRef<Path>>(path_opt: Option<P>) -> Result<Self> {
match path_opt {
Some(path_in) => {
let path = AsRef::<Path>::as_ref(&path_in);
if T::maybe_create_dfl(&path)? {
info!("cfg: Created {:?}", &path);
}
Self::load(path)
}
None => {
let mut data: T = Default::default();
data.loaded(None)?;
Ok(Self {
path: None,
loaded_mod: UNIX_EPOCH,
data,
})
}
}
}
pub fn save(&self) -> Result<()> {
if let Some(path) = self.path.as_deref() {
self.data.save(&path)
} else {
Ok(())
}
}
pub fn maybe_reload(&mut self) -> Result<bool> {
let path = match self.path.as_ref() {
Some(p) => p,
None => return Ok(false),
};
let modified = fs::metadata(&path)?.modified()?;
match SystemTime::now().duration_since(modified) {
Ok(dur) if dur.as_millis() < 10 => return Ok(false),
_ => {}
}
if self.loaded_mod == modified {
return Ok(false);
}
self.loaded_mod = modified;
let mut data = T::load(&path)?;
data.loaded(Some(&mut self.data))?;
self.data = data;
Ok(true)
}
}
pub trait JsonArgs
where
Self: JsonLoad + JsonSave,
{
fn match_cmdline() -> clap::ArgMatches<'static>;
fn verbosity(matches: &clap::ArgMatches) -> u32;
fn log_file(matches: &clap::ArgMatches) -> String;
fn system_configuration_overrides(
_matches: &clap::ArgMatches,
) -> (Option<usize>, Option<usize>, Option<usize>) {
(None, None, None)
}
fn process_cmdline(&mut self, matches: &clap::ArgMatches) -> bool;
}
pub trait JsonArgsHelper
where
Self: JsonArgs,
{
fn init_args_and_logging_nosave() -> Result<(JsonConfigFile<Self>, bool)>;
fn save_args(args_file: &JsonConfigFile<Self>) -> Result<()>;
fn init_args_and_logging() -> Result<JsonConfigFile<Self>>;
}
impl<T> JsonArgsHelper for T
where
T: JsonArgs,
{
fn init_args_and_logging_nosave() -> Result<(JsonConfigFile<T>, bool)> {
let matches = T::match_cmdline();
super::init_logging(T::verbosity(&matches), T::log_file(&matches));
let overrides = T::system_configuration_overrides(&matches);
super::override_system_configuration(overrides.0, overrides.1, overrides.2);
let mut args_file = JsonConfigFile::<T>::load_or_create(matches.value_of("args").as_ref())?;
let updated = args_file.data.process_cmdline(&matches);
Ok((args_file, updated))
}
fn save_args(args_file: &JsonConfigFile<T>) -> Result<()> {
if args_file.path.is_some() {
debug!(
"Updating command line arguments file {:?}",
&args_file.path.as_deref().unwrap()
);
args_file.save()?;
}
Ok(())
}
fn init_args_and_logging() -> Result<JsonConfigFile<T>> {
let (args_file, updated) = Self::init_args_and_logging_nosave()?;
if updated {
Self::save_args(&args_file)?;
}
Ok(args_file)
}
}
#[derive(Debug)]
pub struct JsonReportFile<T: JsonSave> {
pub path: Option<PathBuf>,
pub staging: PathBuf,
pub data: T,
}
impl<T: JsonSave> JsonReportFile<T> {
pub fn new<P: AsRef<Path>>(path_opt: Option<P>) -> Self {
let (path, staging) = match path_opt {
Some(p) => {
let pb = PathBuf::from(p.as_ref());
let mut st = pb.clone().into_os_string();
st.push(".staging");
(Some(pb), PathBuf::from(st))
}
None => (None, PathBuf::new()),
};
Self {
path,
staging,
data: Default::default(),
}
}
pub fn commit(&self) -> Result<()> {
let path = match self.path.as_ref() {
Some(v) => v,
None => return Ok(()),
};
self.data.save(&self.staging)?;
fs::rename(&self.staging, &path)?;
Ok(())
}
}
pub struct JsonRawFile {
pub path: PathBuf,
pub preamble: String,
pub value: serde_json::Value,
}
impl JsonRawFile {
pub fn load<P: AsRef<Path>>(path_in: P) -> Result<Self> {
let path = PathBuf::from(path_in.as_ref());
let (preamble, body) = read_json(&path)?;
Ok(Self {
path,
preamble,
value: serde_json::from_str(&body)?,
})
}
pub fn save(&self) -> Result<()> {
let output = self.preamble.clone() + &serde_json::ser::to_string_pretty(&self.value)?;
let mut f = fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(&self.path)?;
f.write_all(output.as_ref())?;
Ok(())
}
}