1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
//! Load a config file by trying out default config file locations:
//!
//! - `{NAME_UPPERCASE}_CONFIG` envitonment variable
//! - `~/.config/{name}/config.toml`
//! - `/etc/{name}/config.toml`
//! - `/usr/local/etc/{name}/config.toml`
//! - `~/Library/Preferences/{name}/config.toml`
//! - `/usr/local/etc/{name}/config.toml`
//!
//! ```no_run
//! use serde::{Deserialize, Serialize};
//!
//! #[derive(Debug, Deserialize, Serialize)]
//! struct Config {}
//!
//! let config: Config = config_dirs::load("my-app", toml::from_str).expect("Failed to load config");
//! ```

use std::{
    env, fs, io,
    path::{Path, PathBuf},
};

use thiserror::Error;
use tracing::{error, info};

#[derive(Debug, Error)]
pub enum Error<ParseError: std::error::Error> {
    #[error("Failed to read config file: {0:#}")]
    Io(#[from] io::Error),
    #[error("Failed to parse config: {0:#}")]
    Parse(ParseError),
    #[error("Failed to load config from paths")]
    NoPath,
}

pub fn load<Config, E: std::error::Error>(
    name: &str,
    parse: impl Fn(&str) -> Result<Config, E> + Copy,
) -> Result<Config, Error<E>> {
    if let Ok(path) = env::var(format!("{}_CONFIG", name.to_uppercase())) {
        if let Some(path) = path_with_home_dir(&path) {
            match load_from_path(path, parse) {
                Err(Error::Io(e)) if matches!(e.kind(), io::ErrorKind::NotFound) => {}
                v => return v,
            }
        }
    }
    let paths = [
        format!("~/.config/{name}/config.toml"),
        format!("/etc/{name}/config.toml"),
        format!("/usr/local/etc/{name}/config.toml"),
        format!("~/Library/Preferences/{name}/config.toml"),
        format!("/Library/Preferences/{name}/config.toml"),
    ];
    for path in paths {
        if let Some(path) = path_with_home_dir(&path) {
            match load_from_path(path, parse) {
                Err(Error::Io(e)) if matches!(e.kind(), io::ErrorKind::NotFound) => {}
                v => return v,
            }
        }
    }
    error!("Failed to load config");
    Err(Error::NoPath)
}

fn load_from_path<Config, E: std::error::Error>(
    path: impl AsRef<Path>,
    parse: impl Fn(&str) -> Result<Config, E>,
) -> Result<Config, Error<E>> {
    info!("Loading config from {}", path.as_ref().to_string_lossy());
    parse(&fs::read_to_string(path)?).map_err(Error::Parse)
}

fn path_with_home_dir(path: &str) -> Option<PathBuf> {
    if path.starts_with("~/") {
        dirs::home_dir().map(|v| v.join(&path[2..]))
    } else {
        Some(PathBuf::from(path))
    }
}