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
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
use std::collections::HashSet;
use std::ffi::OsStr;
use std::path::{Path, PathBuf};

use figment::error::Kind;
use figment::providers::{Format, Json, Toml, Yaml};
use figment::Figment;
use wordexp::{wordexp, Wordexp};

use crate::cli::Cli;
use crate::config::AppConfig;
use crate::error::Result;

fn expand_include_path(s: impl AsRef<str>, cfg_dir: impl AsRef<Path>) -> Result<Vec<PathBuf>> {
    let cfg_dir = cfg_dir.as_ref();
    // perform expansion, see: man 3 wordexp
    Ok(wordexp(s.as_ref(), Wordexp::new(0), 0)?
        .map(|path| -> Result<_> {
            // convert expansion to path
            let path = PathBuf::from(path);
            if path.is_absolute() {
                // if it's already absolute, keep it
                Ok(path)
            } else {
                // if it's not absolute, assume relative to `cfg_dir` and attempt to resolve
                let joined = cfg_dir.join(path);
                match joined.canonicalize() {
                    Ok(p) => Ok(p),
                    Err(e) => bail!("failed to resolve {}: {}", joined.display(), e),
                }
            }
        })
        .collect::<Result<_>>()?)
}

pub fn parse(args: &Cli) -> Result<AppConfig> {
    let cfg_file = args
        .config
        .as_ref()
        .map(|p| p.to_owned())
        .or_else(|| dirs::config_dir().map(|d| d.join("i3stat/config")))
        .ok_or_else(|| "failed to find config file")?;

    let cfg_dir = cfg_file
        .parent()
        .ok_or_else(|| "failed to find config dir")?;

    // main configuration file
    let mut figment = Figment::new()
        .merge(Toml::file(cfg_file.with_extension("toml")))
        .merge(Json::file(cfg_file.with_extension("json")))
        .merge(Yaml::file(cfg_file.with_extension("yaml")))
        .merge(Yaml::file(cfg_file.with_extension("yml")));

    // parse any additional config files
    let figment = {
        let mut seen = HashSet::new();
        seen.insert(cfg_file.clone());

        // as long as we get more include paths, keep parsing
        loop {
            let include_paths = match figment.extract_inner::<Vec<String>>("include") {
                // we got some include paths
                Ok(user_paths) => {
                    let mut paths = vec![];
                    for unexpanded in user_paths {
                        match expand_include_path(&unexpanded, &cfg_dir) {
                            Ok(path) => paths.extend(path),
                            Err(e) => {
                                log::warn!("failed to include config '{}': {}", unexpanded, e);
                                continue;
                            }
                        }
                    }

                    paths
                }
                // ignore if "include" wasn't specified at all
                Err(e) if matches!(e.kind, Kind::MissingField(_)) => vec![],
                // some other error occurred
                Err(e) => bail!(e),
            };

            if include_paths.iter().all(|p| seen.contains(p)) {
                break figment;
            }

            for include in include_paths {
                match include.extension().and_then(OsStr::to_str) {
                    Some("toml") => figment = figment.admerge(Toml::file(&include)),
                    Some("json") => figment = figment.admerge(Json::file(&include)),
                    Some("yaml") | Some("yml") => figment = figment.admerge(Yaml::file(&include)),
                    Some(e) => bail!("Unsupported file extension: {}", e),
                    None => bail!("No file extension, cannot infer file format"),
                }

                log::trace!("read config file: {}", include.display());
                seen.insert(include);
            }
        }
    };

    let app_config = figment.extract::<AppConfig>()?;
    Ok(app_config)
}