1use std::{
20 env, fs, io,
21 path::{Path, PathBuf},
22};
23
24use convert_case::{Case, Casing};
25use genawaiter::{stack::let_gen, yield_};
26use thiserror::Error;
27use tracing::{error, info};
28
29#[derive(Debug, Error)]
30pub enum Error<ParseError: std::error::Error> {
31 #[error("Failed to read config file: {0:#}")]
32 Io(#[from] io::Error),
33 #[error("Failed to parse config: {0:#}")]
34 Parse(ParseError),
35 #[error("Failed to load config from paths")]
36 NoPath,
37}
38
39pub fn load<Config, E: std::error::Error>(
40 name: &str,
41 parse: impl Fn(&str) -> Result<Config, E> + Copy,
42) -> Result<Config, Error<E>> {
43 if let Ok(path) = env::var(format!("{}_CONFIG", name.to_case(Case::ScreamingSnake))) {
44 if let Some(path) = path_with_home_dir(&path) {
45 match load_from_path(path, parse) {
46 Err(Error::Io(e)) if matches!(e.kind(), io::ErrorKind::NotFound) => {}
47 v => return v,
48 }
49 }
50 }
51 let_gen!(paths, {
52 yield_!(format!("~/.config/{name}/config.toml"));
53 yield_!(format!("/etc/{name}/config.toml"));
54 yield_!(format!("/usr/local/etc/{name}/config.toml"));
55 yield_!(format!("~/Library/Preferences/{name}/config.toml"));
56 yield_!(format!("/Library/Preferences/{name}/config.toml"));
57 });
58 for path in paths {
59 if let Some(path) = path_with_home_dir(&path) {
60 match load_from_path(path, parse) {
61 Err(Error::Io(e)) if matches!(e.kind(), io::ErrorKind::NotFound) => {}
62 v => return v,
63 }
64 }
65 }
66 error!("Failed to load config");
67 Err(Error::NoPath)
68}
69
70pub fn load_from_path<Config, E: std::error::Error>(
71 path: impl AsRef<Path>,
72 parse: impl Fn(&str) -> Result<Config, E>,
73) -> Result<Config, Error<E>> {
74 info!("Loading config from {}", path.as_ref().to_string_lossy());
75 parse(&fs::read_to_string(path)?).map_err(Error::Parse)
76}
77
78fn path_with_home_dir(path: &str) -> Option<PathBuf> {
79 if path.starts_with("~/") {
80 dirs::home_dir().map(|v| v.join(&path[2..]))
81 } else {
82 Some(PathBuf::from(path))
83 }
84}