1use anyhow::{bail, Context, Result};
3use log::{debug, info};
4use serde::{de::DeserializeOwned, Serialize};
5use std::default::Default;
6use std::fs;
7use std::io::{self, prelude::*};
8use std::path::{Path, PathBuf};
9use std::time::{SystemTime, UNIX_EPOCH};
10
11fn read_json<P: AsRef<Path>>(path: P) -> Result<(String, String)> {
12 let mut f = fs::OpenOptions::new().read(true).open(path)?;
13 let mut buf = String::new();
14 f.read_to_string(&mut buf)?;
15
16 let mut preamble = String::new();
17 let mut body = String::new();
18 let mut seen_body = false;
19
20 for line in buf.lines() {
21 let trimmed = line.trim();
22 if trimmed.starts_with("//") || trimmed.starts_with("#") {
23 if !seen_body {
24 preamble = preamble + line + "\n";
25 }
26 body = body + "\n";
27 } else {
28 seen_body = true;
29 body = body + line + "\n"
30 }
31 }
32 Ok((preamble, body))
33}
34
35pub trait JsonLoad
36where
37 Self: DeserializeOwned,
38{
39 fn loaded(&mut self, _prev: Option<&mut Self>) -> Result<()> {
40 Ok(())
41 }
42
43 fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
44 let (_, body) = read_json(path)?;
45 Ok(serde_json::from_str::<Self>(&body)?)
46 }
47}
48
49pub trait JsonSave
50where
51 Self: Default + Serialize,
52{
53 fn preamble() -> Option<String> {
54 None
55 }
56
57 fn maybe_create_dfl<P: AsRef<Path>>(path_in: P) -> Result<bool> {
58 let path = path_in.as_ref();
59
60 if let Some(parent) = path.parent() {
61 fs::create_dir_all(&parent)?;
62 }
63
64 match fs::OpenOptions::new()
65 .write(true)
66 .create_new(true)
67 .open(&path)
68 {
69 Ok(mut f) => {
70 let data: Self = Default::default();
71 f.write_all(data.as_json()?.as_ref())?;
72 Ok(true)
73 }
74 Err(e) => match e.kind() {
75 io::ErrorKind::AlreadyExists => Ok(false),
76 _ => Err(e.into()),
77 },
78 }
79 }
80
81 fn as_json(&self) -> Result<String> {
82 let mut serialized = serde_json::to_string_pretty(&self)?;
83 if !serialized.ends_with("\n") {
84 serialized += "\n";
85 }
86 match Self::preamble() {
87 Some(pre) => Ok(pre + &serialized),
88 None => Ok(serialized),
89 }
90 }
91
92 fn save<P: AsRef<Path>>(&self, path: P) -> Result<()> {
93 let path: &Path = path.as_ref();
94 let fname = match path.file_name() {
95 Some(v) => v,
96 None => bail!("can't save to null path"),
97 };
98
99 let mut tmp_path = PathBuf::from(path);
100 tmp_path.pop();
101 tmp_path.push(format!(".{}.json-save-staging", &fname.to_string_lossy()));
102
103 let mut f = fs::OpenOptions::new()
104 .write(true)
105 .create(true)
106 .truncate(true)
107 .open(&tmp_path)
108 .with_context(|| format!("opening staging file {:?}", &tmp_path))?;
109 f.write_all(self.as_json()?.as_ref())
110 .with_context(|| format!("writing staging file {:?}", &tmp_path))?;
111 fs::rename(&tmp_path, path)
112 .with_context(|| format!("moving {:?} to {:?}", &tmp_path, path))?;
113 Ok(())
114 }
115}
116
117#[derive(Clone, Debug)]
118pub struct JsonConfigFile<T: JsonLoad + JsonSave> {
119 pub path: Option<PathBuf>,
120 pub loaded_mod: SystemTime,
121 pub data: T,
122}
123
124impl<T: JsonLoad + JsonSave + Default> Default for JsonConfigFile<T> {
125 fn default() -> Self {
126 Self {
127 path: None,
128 loaded_mod: UNIX_EPOCH,
129 data: Default::default(),
130 }
131 }
132}
133
134impl<T: JsonLoad + JsonSave> JsonConfigFile<T> {
135 pub fn load<P: AsRef<Path>>(path_in: P) -> Result<Self> {
136 let path = AsRef::<Path>::as_ref(&path_in);
137
138 let modified = path.metadata()?.modified()?;
139 let mut data = T::load(&path)?;
140 data.loaded(None)?;
141
142 Ok(Self {
143 path: Some(PathBuf::from(path)),
144 loaded_mod: modified,
145 data,
146 })
147 }
148
149 pub fn load_or_create<P: AsRef<Path>>(path_opt: Option<P>) -> Result<Self> {
150 match path_opt {
151 Some(path_in) => {
152 let path = AsRef::<Path>::as_ref(&path_in);
153
154 if T::maybe_create_dfl(&path)? {
155 info!("cfg: Created {:?}", &path);
156 }
157
158 Self::load(path)
159 }
160 None => {
161 let mut data: T = Default::default();
162 data.loaded(None)?;
163
164 Ok(Self {
165 path: None,
166 loaded_mod: UNIX_EPOCH,
167 data,
168 })
169 }
170 }
171 }
172
173 pub fn save(&self) -> Result<()> {
174 if let Some(path) = self.path.as_deref() {
175 self.data.save(&path)
176 } else {
177 Ok(())
178 }
179 }
180
181 pub fn maybe_reload(&mut self) -> Result<bool> {
182 let path = match self.path.as_ref() {
183 Some(p) => p,
184 None => return Ok(false),
185 };
186
187 let modified = fs::metadata(&path)?.modified()?;
188 match SystemTime::now().duration_since(modified) {
190 Ok(dur) if dur.as_millis() < 10 => return Ok(false),
191 _ => {}
192 }
193
194 if self.loaded_mod == modified {
196 return Ok(false);
197 }
198
199 self.loaded_mod = modified;
200 let mut data = T::load(&path)?;
201 data.loaded(Some(&mut self.data))?;
202 self.data = data;
203 Ok(true)
204 }
205}
206
207pub trait JsonArgs
208where
209 Self: JsonLoad + JsonSave,
210{
211 fn match_cmdline() -> clap::ArgMatches<'static>;
212 fn verbosity(matches: &clap::ArgMatches) -> u32;
213 fn log_file(matches: &clap::ArgMatches) -> String;
214 fn system_configuration_overrides(
215 _matches: &clap::ArgMatches,
216 ) -> (Option<usize>, Option<usize>, Option<usize>) {
217 (None, None, None)
218 }
219 fn process_cmdline(&mut self, matches: &clap::ArgMatches) -> bool;
220}
221
222pub trait JsonArgsHelper
223where
224 Self: JsonArgs,
225{
226 fn init_args_and_logging_nosave() -> Result<(JsonConfigFile<Self>, bool)>;
227 fn save_args(args_file: &JsonConfigFile<Self>) -> Result<()>;
228 fn init_args_and_logging() -> Result<JsonConfigFile<Self>>;
229}
230
231impl<T> JsonArgsHelper for T
232where
233 T: JsonArgs,
234{
235 fn init_args_and_logging_nosave() -> Result<(JsonConfigFile<T>, bool)> {
236 let matches = T::match_cmdline();
237 super::init_logging(T::verbosity(&matches), T::log_file(&matches));
238 let overrides = T::system_configuration_overrides(&matches);
239 super::override_system_configuration(overrides.0, overrides.1, overrides.2);
240
241 let mut args_file = JsonConfigFile::<T>::load_or_create(matches.value_of("args").as_ref())?;
242 let updated = args_file.data.process_cmdline(&matches);
243
244 Ok((args_file, updated))
245 }
246
247 fn save_args(args_file: &JsonConfigFile<T>) -> Result<()> {
248 if args_file.path.is_some() {
249 debug!(
250 "Updating command line arguments file {:?}",
251 &args_file.path.as_deref().unwrap()
252 );
253 args_file.save()?;
254 }
255 Ok(())
256 }
257
258 fn init_args_and_logging() -> Result<JsonConfigFile<T>> {
259 let (args_file, updated) = Self::init_args_and_logging_nosave()?;
260 if updated {
261 Self::save_args(&args_file)?;
262 }
263 Ok(args_file)
264 }
265}
266
267#[derive(Debug)]
268pub struct JsonReportFile<T: JsonSave> {
269 pub path: Option<PathBuf>,
270 pub staging: PathBuf,
271 pub data: T,
272}
273
274impl<T: JsonSave> JsonReportFile<T> {
275 pub fn new<P: AsRef<Path>>(path_opt: Option<P>) -> Self {
276 let (path, staging) = match path_opt {
277 Some(p) => {
278 let pb = PathBuf::from(p.as_ref());
279 let mut st = pb.clone().into_os_string();
280 st.push(".staging");
281 (Some(pb), PathBuf::from(st))
282 }
283 None => (None, PathBuf::new()),
284 };
285
286 Self {
287 path,
288 staging,
289 data: Default::default(),
290 }
291 }
292
293 pub fn commit(&self) -> Result<()> {
294 let path = match self.path.as_ref() {
295 Some(v) => v,
296 None => return Ok(()),
297 };
298
299 self.data.save(&self.staging)?;
300 fs::rename(&self.staging, &path)?;
301 Ok(())
302 }
303}
304
305pub struct JsonRawFile {
306 pub path: PathBuf,
307 pub preamble: String,
308 pub value: serde_json::Value,
309}
310
311impl JsonRawFile {
312 pub fn load<P: AsRef<Path>>(path_in: P) -> Result<Self> {
313 let path = PathBuf::from(path_in.as_ref());
314 let (preamble, body) = read_json(&path)?;
315
316 Ok(Self {
317 path,
318 preamble,
319 value: serde_json::from_str(&body)?,
320 })
321 }
322
323 pub fn save(&self) -> Result<()> {
324 let output = self.preamble.clone() + &serde_json::ser::to_string_pretty(&self.value)?;
325 let mut f = fs::OpenOptions::new()
326 .write(true)
327 .create(true)
328 .truncate(true)
329 .open(&self.path)?;
330 f.write_all(output.as_ref())?;
331 Ok(())
332 }
333}