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
106
107
108
109
use std::env;
use std::ffi::OsString;
use std::io;
use std::path::{Path, PathBuf};

use serde_json::{Map, Value};
use thiserror::Error;

mod from_env;
mod from_file;
mod merge_sources;

use merge_sources::merge_sources;

pub type Json = Map<String, Value>;
pub type Result<T> = std::result::Result<T, ConfigError>;

pub struct Config {
    pub prefix: String,
    pub dir: PathBuf,
    pub secrets_file: PathBuf,
    pub service_env: OsString,
    _use_default_default: (),
}

impl Default for Config {
    fn default() -> Self {
        Config {
            prefix: "CONF_".to_string(),
            dir: PathBuf::new(),
            secrets_file: PathBuf::from(
                env::var_os("CONFIG_SECRET_FILE")
                    .unwrap_or_else(|| OsString::from("config.secret.json")),
            ),
            service_env: env::var_os("SERVICE_ENV").unwrap_or_else(|| OsString::from("local")),
            _use_default_default: (),
        }
    }
}

#[derive(Debug, Error)]
pub enum ConfigError {
    #[error("invalid utf-8 in {key:?}, got roughly {value:?}")]
    InvalidEnvEncoding { key: String, value: String },

    #[error("locating file failed: {path:?} (in {cwd:?})")]
    ResolvePath {
        source: io::Error,
        path: PathBuf,
        cwd: io::Result<PathBuf>,
    },

    #[error("open {path:?} failed")]
    FileOpenFailed { source: io::Error, path: PathBuf },

    #[error("invalid json in  {path:?}")]
    InvalidJson {
        source: serde_json::Error,
        path: PathBuf,
    },
}

impl Config {
    pub fn for_prefix<S: ToString>(prefix: S) -> Result<Json> {
        Config {
            prefix: prefix.to_string(),
            ..Default::default()
        }
        .load()
    }

    pub fn for_dir<P: AsRef<Path>>(dir: P) -> Result<Json> {
        Config {
            dir: dir.as_ref().to_path_buf(),
            secrets_file: join(
                dir.as_ref().to_path_buf(),
                &OsString::from("config.secret.json"),
            ),
            ..Default::default()
        }
        .load()
    }

    pub fn load(self) -> Result<Json> {
        let default = from_file::load(join(
            self.dir.to_path_buf(),
            &OsString::from("config.default.json"),
        ))?;
        let service_env = from_file::load(join(self.dir, &env_file(&self.service_env)))?;
        let secret = from_file::load(self.secrets_file)?;
        let from_env = from_env::from_env(&self.prefix)?;
        Ok(merge_sources(
            default,
            &[service_env, secret, Value::Object(from_env)],
        ))
    }
}

fn join(mut root: PathBuf, extra: &OsString) -> PathBuf {
    root.push(extra);
    root
}

fn env_file(env: &OsString) -> OsString {
    let mut file = OsString::from("config.");
    file.push(env);
    file.push(".json");
    file
}