1use std::{
6 borrow::{Borrow, Cow},
7 cell::Cell,
8 fmt::Display,
9 io::stderr,
10 ops::Deref,
11 path::{Path, PathBuf},
12 sync::Arc,
13 time::SystemTime,
14};
15
16use anyhow::{Result, anyhow, bail};
17use chj_unix_util::daemon::warrants_restart::WarrantsRestart;
18use cj_path_util::{path_util::AppendToPath, unix::polyfill::add_extension};
19use serde::{Serialize, de::DeserializeOwned};
20
21use crate::{
22 ctx, debug,
23 io_utils::tempfile_utils::TempfileOptions,
24 serde_types::proper_filename::ProperFilename,
25 serde_util::json5_from_str::{Json5FromStrError, json5_from_str},
26 utillib::{
27 arc::CloneArc,
28 home::{HomeError, home_dir},
29 slice_or_box::SliceOrBox,
30 },
31};
32
33pub fn ron_to_string_pretty<V: serde::Serialize>(value: &V) -> Result<String, ron::Error> {
34 ron::Options::default()
35 .to_string_pretty(value, ron::ser::PrettyConfig::default().struct_names(true))
36}
37
38pub fn ron_to_file_pretty<V: serde::Serialize>(
40 value: &V,
41 path: impl AsRef<Path>,
42 migrate_access: bool,
43 _initial_mode: Option<u32>,
44) -> Result<()> {
45 let s = ron_to_string_pretty(value)?;
46 let path = path.as_ref();
47 let tmpfile = TempfileOptions {
48 target_path: path.into(),
49 retain_tempfile: false,
50 migrate_access,
51 }
52 .tempfile()?;
53 let temp_path = &tmpfile.temp_path;
54 std::fs::write(temp_path, s).map_err(ctx!("writing file to {temp_path:?}"))?;
56 tmpfile.finish()?;
57 Ok(())
58}
59
60pub fn parse_ron<V: DeserializeOwned>(s: &str) -> Result<V> {
61 ron::from_str(s)
62 .map_err(|error| {
63 let ron::error::SpannedError {
64 code,
65 position: ron::error::Position { line, col },
66 } = error;
67 anyhow!("{code} at line:column {line}:{col}")
68 })
69 .map_err(ctx!("decoding RON"))
70}
71
72pub fn load_ron_file<V: DeserializeOwned>(path: impl AsRef<Path>) -> Result<V> {
73 let path = path.as_ref();
74 let s = std::fs::read_to_string(path).map_err(ctx!("loading file from {path:?}"))?;
75 parse_ron(&s).map_err(ctx!("reading file from {path:?}"))
76}
77
78#[derive(Debug, Clone, Copy)]
79pub enum ConfigBackend {
80 Ron,
81 Json5,
82 Yaml,
83 Hcl,
84}
85
86impl Display for ConfigBackend {
87 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88 f.write_str(self.format_name())
89 }
90}
91
92impl ConfigBackend {
93 pub fn format_name(self) -> &'static str {
94 match self {
95 ConfigBackend::Ron => "RON",
96 ConfigBackend::Json5 => "JSON5",
97 ConfigBackend::Yaml => "YAML",
98 ConfigBackend::Hcl => "HCL",
99 }
100 }
101
102 pub fn load_config_file<T: DeserializeOwned>(self, path: &Path) -> Result<T> {
103 let s = std::fs::read_to_string(path).map_err(ctx!("loading config from file {path:?}"))?;
104 match self {
105 ConfigBackend::Ron => parse_ron(&s).map_err(ctx!("reading config from file {path:?}")),
106 ConfigBackend::Json5 => {
107 if false {
111 let mut d = serde_json5::Deserializer::from_str(&s)
115 .map_err(Json5FromStrError)
116 .map_err(ctx!(
117 "decoding JSON5 from config file {path:?} -- step 1, too early?"
118 ))?;
119 serde_path_to_error::deserialize(&mut d)
121 .map_err(ctx!("decoding JSON5 from config file {path:?}"))
122 } else {
123 json5_from_str(&s).map_err(ctx!("decoding JSON5 from config file {path:?}"))
124 }
125 }
126 ConfigBackend::Yaml => {
127 serde_yml::from_str(&s).map_err(ctx!("decoding YAML from config file {path:?}"))
128 }
129 ConfigBackend::Hcl => {
130 hcl::from_str(&s).map_err(ctx!("decoding HCL from config file {path:?}"))
131 }
132 }
133 }
134
135 pub fn save_config_file<T: Serialize>(self, path: &Path, value: &T) -> Result<()> {
136 let s = match self {
137 ConfigBackend::Ron => ron_to_string_pretty(value)?,
138 ConfigBackend::Json5 => {
139 serde_json::to_string_pretty(value).map_err(ctx!("encoding config as JSON5"))?
140 }
141 ConfigBackend::Yaml => {
142 serde_yml::to_string(value).map_err(ctx!("encoding config as YAML"))?
143 }
144 ConfigBackend::Hcl => hcl::to_string(value).map_err(ctx!("encoding config as HCL"))?,
145 };
146 std::fs::write(path, s).map_err(ctx!("writing config file to {path:?}"))
147 }
148}
149
150pub const FILE_EXTENSIONS: &[(&str, ConfigBackend)] = &[
151 ("ron", ConfigBackend::Ron),
152 ("json5", ConfigBackend::Json5),
153 ("json", ConfigBackend::Json5),
154 ("yml", ConfigBackend::Yaml),
155 ("yaml", ConfigBackend::Yaml),
156 ("hcl", ConfigBackend::Hcl),
157];
158
159pub fn supported_formats() -> impl Iterator<Item = String> {
160 FILE_EXTENSIONS
161 .iter()
162 .map(|(ext, backend)| format!(".{ext} ({backend})"))
163}
164
165pub fn backend_from_path(path: &Path) -> Result<ConfigBackend> {
166 if let Some(ext) = path.extension() {
167 if let Some(ext) = ext.to_str() {
168 if let Some((_, backend)) = FILE_EXTENSIONS.iter().find(|(e, _b)| *e == ext) {
169 Ok(*backend)
170 } else {
171 bail!("given file path does have an unknown extension {ext:?}: {path:?}")
172 }
173 } else {
174 bail!("given file path does have an extension that is not unicode: {path:?}")
175 }
176 } else {
177 bail!(
178 "given file path does not have an extension \
179 for determining the file format: {path:?}"
180 )
181 }
182}
183
184pub fn save_config_file<T: Serialize>(path: &Path, value: &T) -> Result<()> {
185 let backend = backend_from_path(path)?;
186 backend.save_config_file(path, value)
187}
188
189pub enum ConfigDir {
190 Etc,
191 Home,
192 Path(Cow<'static, Path>),
193}
194
195impl ConfigDir {
196 pub fn to_path(&self) -> Result<Cow<'_, Path>, &'static HomeError> {
197 match self {
198 ConfigDir::Etc => Ok(AsRef::<Path>::as_ref("/etc").into()),
199 ConfigDir::Home => home_dir().map(Into::into),
200 ConfigDir::Path(cow) => Ok(Cow::Borrowed(cow.borrow())),
201 }
202 }
203
204 pub fn append_file_name(
205 &self,
206 file_name: &ProperFilename,
207 ) -> Result<PathBuf, &'static HomeError> {
208 match self {
209 ConfigDir::Etc => Ok(self.to_path()?.append(file_name.as_ref())),
210 ConfigDir::Home => {
211 let dotted_file_name = format!(".{}", file_name.as_str());
212 Ok(self.to_path()?.append(dotted_file_name))
213 }
214 ConfigDir::Path(cow) => Ok(cow.as_ref().append(file_name.as_ref())),
215 }
216 }
217}
218
219pub trait DefaultConfigPath: DeserializeOwned {
220 fn default_config_file_name_without_suffix() -> Result<Option<ProperFilename>>;
227
228 fn default_config_dirs() -> SliceOrBox<'static, ConfigDir> {
229 const V: &[ConfigDir] = &[ConfigDir::Home, ConfigDir::Etc];
230 V.into()
231 }
232}
233
234struct PathAndTrack {
235 path: Arc<Path>,
236 mtime: Cell<SystemTime>,
239}
240
241pub struct ConfigFile<T> {
245 config: T,
246 path_and_track: Option<PathAndTrack>,
249}
250
251impl<T> Deref for ConfigFile<T> {
252 type Target = T;
253
254 fn deref(&self) -> &Self::Target {
255 &self.config
256 }
257}
258
259impl<T: DeserializeOwned + DefaultConfigPath> ConfigFile<T> {
260 pub fn path(&self) -> Option<&Arc<Path>> {
263 self.path_and_track.as_ref().map(|pt| &pt.path)
264 }
265
266 pub fn perhaps_reload_config(&self) -> Result<Option<Self>> {
276 if let Some(PathAndTrack { path, mtime }) = self.path_and_track.as_ref() {
277 match std::fs::metadata(path) {
278 Ok(s) => match s.modified() {
279 Ok(m) => {
280 if m == mtime.get() {
281 Ok(None)
282 } else {
283 mtime.set(m);
284 let val = Self::load_config(Some(path.clone_arc()), |_| {
285 bail!("config {path:?} missing")
286 })?;
287 Ok(Some(val))
288 }
289 }
290 Err(_) => Ok(None),
291 },
292 Err(_) => Ok(None),
293 }
294 } else {
295 Ok(None)
296 }
297 }
298
299 pub fn load_config(
307 path: Option<Arc<Path>>,
308 or_else: impl FnOnce(&str) -> Result<T>,
309 ) -> Result<Self> {
310 let load_config = |path: Arc<Path>, backend: ConfigBackend| {
311 let config = backend.load_config_file(&path)?;
312 let mtime = Cell::new(std::fs::metadata(&path)?.modified()?);
313 Ok(Self {
314 config,
315 path_and_track: Some(PathAndTrack { path, mtime }),
316 })
317 };
318
319 if let Some(path) = path {
320 let backend = backend_from_path(&path)?;
321 load_config(path, backend)
322 } else {
323 if let Some(file_name) = T::default_config_file_name_without_suffix()? {
324 let mut default_paths_tried = Vec::new();
325 for config_dir in T::default_config_dirs().iter() {
326 let path = config_dir.append_file_name(&file_name)?;
327 let path_and_backends: Vec<(Arc<Path>, &ConfigBackend)> = FILE_EXTENSIONS
328 .iter()
329 .map(|(extension, backend)| {
330 let path = add_extension(&path, extension)
331 .ok_or_else(|| anyhow!("path is missing a file name: {path:?}"))?;
332 if path.exists() {
333 Ok(Some((path.into(), backend)))
334 } else {
335 default_paths_tried.push(path);
336 Ok(None)
337 }
338 })
339 .filter_map(|x| x.transpose())
340 .collect::<Result<_>>()?;
341 match path_and_backends.len() {
342 0 => (),
343 1 => {
344 let (path, backend) = &path_and_backends[0];
345 debug!("found config at {path:?}");
346 return load_config(path.clone_arc(), **backend);
347 }
348 _ => {
349 let paths = path_and_backends
350 .iter()
351 .map(|(path, _)| path)
352 .collect::<Vec<_>>();
353 bail!(
354 "multiple config file paths found, leading to ambiguity: {paths:?}"
355 )
356 }
357 }
358 }
359 let config = or_else(&format!("tried the default paths: {default_paths_tried:?}"))?;
360 Ok(Self {
361 config,
362 path_and_track: None,
363 })
364 } else {
365 let config = or_else(
366 "no path was given and there is no default \
367 config location for this type",
368 )?;
369 Ok(Self {
370 config,
371 path_and_track: None,
372 })
373 }
374 }
375 }
376}
377
378impl<T: DeserializeOwned + DefaultConfigPath> WarrantsRestart for ConfigFile<T> {
379 fn warrants_restart(&self) -> bool {
380 match self.perhaps_reload_config() {
381 Ok(Some(_)) => true,
382 Ok(None) => false,
383 Err(e) => {
384 use std::io::Write;
385 _ = writeln!(&mut stderr(), "could not reload config file: {e:#}");
386 false
387 }
388 }
389 }
390}