1use std::env;
2use std::ffi::OsString;
3use std::io;
4use std::path::{Path, PathBuf};
5
6use serde_json::{Map, Value};
7use thiserror::Error;
8
9mod from_env;
10mod from_file;
11mod merge_sources;
12
13use merge_sources::merge_sources;
14
15pub type Json = Map<String, Value>;
16pub type Result<T> = std::result::Result<T, ConfigError>;
17
18pub struct Config {
19 pub prefix: String,
20 pub dir: PathBuf,
21 pub secrets_file: PathBuf,
22 pub service_env: OsString,
23 _use_default_default: (),
24}
25
26impl Default for Config {
27 fn default() -> Self {
28 Config {
29 prefix: "CONF_".to_string(),
30 dir: PathBuf::new(),
31 secrets_file: PathBuf::from(
32 env::var_os("CONFIG_SECRET_FILE")
33 .unwrap_or_else(|| OsString::from("config.secret.json")),
34 ),
35 service_env: env::var_os("SERVICE_ENV").unwrap_or_else(|| OsString::from("local")),
36 _use_default_default: (),
37 }
38 }
39}
40
41#[derive(Debug, Error)]
42pub enum ConfigError {
43 #[error("invalid utf-8 in {key:?}, got roughly {value:?}")]
44 InvalidEnvEncoding { key: String, value: String },
45
46 #[error("locating file failed: {path:?} (in {cwd:?})")]
47 ResolvePath {
48 source: io::Error,
49 path: PathBuf,
50 cwd: io::Result<PathBuf>,
51 },
52
53 #[error("open {path:?} failed")]
54 FileOpenFailed { source: io::Error, path: PathBuf },
55
56 #[error("invalid json in {path:?}")]
57 InvalidJson {
58 source: serde_json::Error,
59 path: PathBuf,
60 },
61}
62
63impl Config {
64 pub fn for_prefix<S: ToString>(prefix: S) -> Result<Json> {
65 Config {
66 prefix: prefix.to_string(),
67 ..Default::default()
68 }
69 .load()
70 }
71
72 pub fn for_dir<P: AsRef<Path>>(dir: P) -> Result<Json> {
73 Config {
74 dir: dir.as_ref().to_path_buf(),
75 secrets_file: join(
76 dir.as_ref().to_path_buf(),
77 &OsString::from("config.secret.json"),
78 ),
79 ..Default::default()
80 }
81 .load()
82 }
83
84 pub fn load(self) -> Result<Json> {
85 let default = from_file::load(join(
86 self.dir.to_path_buf(),
87 &OsString::from("config.default.json"),
88 ))?;
89 let service_env = from_file::load(join(self.dir, &env_file(&self.service_env)))?;
90 let secret = from_file::load(self.secrets_file)?;
91 let from_env = from_env::from_env(&self.prefix)?;
92 Ok(merge_sources(
93 default,
94 &[service_env, secret, Value::Object(from_env)],
95 ))
96 }
97}
98
99fn join(mut root: PathBuf, extra: &OsString) -> PathBuf {
100 root.push(extra);
101 root
102}
103
104fn env_file(env: &OsString) -> OsString {
105 let mut file = OsString::from("config.");
106 file.push(env);
107 file.push(".json");
108 file
109}