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

use failure::{Error, Fail};
use serde_json::{Map, Value};

mod from_env;
mod from_file;
mod merge_sources;

use merge_sources::merge_sources;

pub type Json = Map<String, Value>;

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, Fail)]
pub enum ConfigError {
    #[fail(display = "invalid utf-8 in {:?}", key)]
    InvalidEnvEncoding { key: String },

    #[fail(display = "locating file failed: {:?} (in {:?})", path, cwd)]
    ResolvePath {
        path: PathBuf,
        cwd: io::Result<PathBuf>,
    },

    #[fail(display = "loading {:?}", path)]
    LoadingFile { path: PathBuf },

    #[fail(display = "open failed")]
    FileOpenFailed,

    #[fail(display = "invalid json")]
    InvalidJson,
}

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

    pub fn for_dir<P: AsRef<Path>>(dir: P) -> Result<Json, Error> {
        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, Error> {
        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
}