1use crate::error::{Error, Result};
11use clap::ArgMatches;
12use serde::{de::DeserializeOwned, ser::Serialize, Deserialize, Serialize as Ser};
13use std::{
14 collections::BTreeMap,
15 convert::TryFrom,
16 env,
17 fs::File,
18 io::Read,
19 path::{Path, PathBuf},
20};
21
22#[derive(Clone, Debug, Deserialize, Ser)]
96pub struct Environments<S, T>
97where
98 S: Ord,
99{
100 envs: BTreeMap<S, T>,
102}
103
104impl<S, T> Environments<S, T>
105where
106 T: DeserializeOwned + Serialize,
107 S: DeserializeOwned + Serialize + Ord + PartialOrd + TryFrom<String>,
108{
109 pub fn from_path(path: &Path) -> Result<Self> {
114 match File::open(path) {
115 Ok(mut file) => {
116 let mut buffer = String::new();
117 let _ = file.read_to_string(&mut buffer)?;
118 Ok(toml::from_str(&buffer)?)
119 }
120 Err(e) => {
121 eprintln!("Unable to read '{}'", path.display());
122 Err(e.into())
123 }
124 }
125 }
126
127 pub fn from_reader<R>(reader: &mut R) -> Result<Self>
132 where
133 R: Read,
134 {
135 let mut buffer = String::new();
136 let _ = reader.read_to_string(&mut buffer)?;
137 Ok(toml::from_str(&buffer)?)
138 }
139
140 pub fn current(&self) -> Result<&T> {
145 self.current_from("env")
146 }
147
148 pub fn current_from(&self, var: &'static str) -> Result<&T> {
153 let environment = TryFrom::try_from(env::var(var)?)
154 .map_err(|_e| Error::invalid_current_environment(var))?;
155 self.envs
156 .get(&environment)
157 .ok_or_else(|| Error::invalid_current_environment(var))
158 }
159}
160
161impl<'a, S, T> TryFrom<&'a ArgMatches<'a>> for Environments<S, T>
162where
163 T: DeserializeOwned + Serialize,
164 S: DeserializeOwned + Serialize + Ord + PartialOrd + TryFrom<String>,
165{
166 type Error = Error;
167
168 fn try_from(matches: &'a ArgMatches<'a>) -> Result<Self> {
169 let env_path = if let Some(env_path) = matches.value_of("env_path") {
170 PathBuf::from(env_path).join("env.toml")
171 } else {
172 PathBuf::from("env.toml")
173 };
174
175 Environments::from_path(env_path.as_path())
176 }
177}
178
179#[cfg(test)]
180mod test {
181 use super::Environments;
182 use crate::{env::Environment, error::Result};
183 use clap::{App, Arg};
184 use dirs;
185 use getset::Getters;
186 use serde::{Deserialize, Serialize};
187 use std::{
188 collections::BTreeMap,
189 convert::TryFrom,
190 env,
191 fs::{remove_file, OpenOptions},
192 io::{BufWriter, Cursor, Write},
193 };
194 use toml;
195
196 const TOMLENV: &str = "TOMLENV";
197 const EXPECTED_TOML_STR: &str = r#"[envs.prod]
198name = "Production"
199key = "abcd-123-efg-45"
200
201[envs.stage]
202name = "Stage"
203
204[envs.test]
205name = "Test"
206
207[envs.dev]
208name = "Development"
209
210[envs.local]
211name = "Local"
212"#;
213
214 #[derive(Debug, Deserialize, Getters, Serialize)]
215 struct RuntimeEnv {
216 #[get]
217 name: String,
218 #[get]
219 key: Option<String>,
220 }
221
222 fn try_decode(toml: &str) -> Result<Environments<Environment, RuntimeEnv>> {
223 let mut cursor = Cursor::new(toml);
224 Ok(Environments::from_reader(&mut cursor)?)
225 }
226
227 fn try_encode(environments: &Environments<Environment, RuntimeEnv>) -> Result<String> {
228 Ok(toml::to_string(environments)?)
229 }
230
231 fn try_current(envs: &Environments<Environment, RuntimeEnv>, expected: &str) -> Result<()> {
232 let current = envs.current()?;
233 assert_eq!(current.name(), expected);
234 Ok(())
235 }
236
237 fn try_current_from(
238 var: &'static str,
239 envs: &Environments<Environment, RuntimeEnv>,
240 expected: &str,
241 ) -> Result<()> {
242 let current = envs.current_from(var)?;
243 assert_eq!(current.name(), expected);
244 Ok(())
245 }
246
247 fn test_cli() -> App<'static, 'static> {
248 App::new("env-from-app-matches")
249 .version("1")
250 .author("Yoda")
251 .about("command line for proxy config testing")
252 .arg(
253 Arg::with_name("env_path")
254 .short("e")
255 .long("envpath")
256 .takes_value(true)
257 .value_name("ENV_PATH"),
258 )
259 }
260
261 #[test]
262 fn decode() {
263 match try_decode(EXPECTED_TOML_STR) {
264 Ok(_) => assert!(true, "Successfully decode TOML to Environments"),
265 Err(_) => assert!(false, "Unable to decode TOML to Environments!"),
266 }
267 }
268
269 #[test]
270 fn encode() {
271 let mut envs = BTreeMap::new();
272 let prod = RuntimeEnv {
273 name: "Production".to_string(),
274 key: Some("abcd-123-efg-45".to_string()),
275 };
276 let stage = RuntimeEnv {
277 name: "Stage".to_string(),
278 key: None,
279 };
280 let test = RuntimeEnv {
281 name: "Test".to_string(),
282 key: None,
283 };
284 let dev = RuntimeEnv {
285 name: "Development".to_string(),
286 key: None,
287 };
288 let local = RuntimeEnv {
289 name: "Local".to_string(),
290 key: None,
291 };
292 let _b = envs.insert(Environment::Prod, prod);
293 let _b = envs.insert(Environment::Stage, stage);
294 let _b = envs.insert(Environment::Test, test);
295 let _b = envs.insert(Environment::Dev, dev);
296 let _b = envs.insert(Environment::Local, local);
297
298 let environments = Environments { envs };
299
300 match try_encode(&environments) {
301 Ok(toml) => assert_eq!(toml, EXPECTED_TOML_STR, "TOML strings match"),
302 Err(_) => assert!(false, "Unable to encode Environments to TOML"),
303 }
304 }
305
306 #[test]
307 fn current() {
308 match try_decode(EXPECTED_TOML_STR) {
309 Ok(ref envs) => {
310 env::set_var("env", "prod");
311 match try_current(envs, "Production") {
312 Ok(_) => assert!(true, "Found Production Env"),
313 Err(_) => assert!(false, "Current is not Production!"),
314 }
315 env::set_var("env", "stage");
316 match try_current(envs, "Stage") {
317 Ok(_) => assert!(true, "Found Stage Env"),
318 Err(_) => assert!(false, "Current is not Stage!"),
319 }
320 env::set_var("env", "test");
321 match try_current(envs, "Test") {
322 Ok(_) => assert!(true, "Found Test Env"),
323 Err(_) => assert!(false, "Current is not Test!"),
324 }
325 env::set_var("env", "dev");
326 match try_current(envs, "Development") {
327 Ok(_) => assert!(true, "Found Development Env"),
328 Err(_) => assert!(false, "Current is not Development!"),
329 }
330 env::set_var("env", "local");
331 match try_current(envs, "Local") {
332 Ok(_) => assert!(true, "Found Local Env"),
333 Err(_) => assert!(false, "Current is not Local!"),
334 }
335 }
336 Err(_) => assert!(false, "Unable to decode TOML to Environments!"),
337 }
338 }
339
340 #[test]
341 fn current_from() {
342 match try_decode(EXPECTED_TOML_STR) {
343 Ok(ref envs) => {
344 env::set_var(TOMLENV, "prod");
345 match try_current_from(TOMLENV, envs, "Production") {
346 Ok(_) => assert!(true, "Found Production Env"),
347 Err(_) => assert!(false, "Current is not Production!"),
348 }
349 env::set_var(TOMLENV, "stage");
350 match try_current_from(TOMLENV, envs, "Stage") {
351 Ok(_) => assert!(true, "Found Stage Env"),
352 Err(_) => assert!(false, "Current is not Stage!"),
353 }
354 env::set_var(TOMLENV, "test");
355 match try_current_from(TOMLENV, envs, "Test") {
356 Ok(_) => assert!(true, "Found Test Env"),
357 Err(_) => assert!(false, "Current is not Test!"),
358 }
359 env::set_var(TOMLENV, "dev");
360 match try_current_from(TOMLENV, envs, "Development") {
361 Ok(_) => assert!(true, "Found Development Env"),
362 Err(_) => assert!(false, "Current is not Development!"),
363 }
364 env::set_var(TOMLENV, "local");
365 match try_current_from(TOMLENV, envs, "Local") {
366 Ok(_) => assert!(true, "Found Local Env"),
367 Err(_) => assert!(false, "Current is not Local!"),
368 }
369 }
370 Err(_) => assert!(false, "Unable to decode TOML to Environments!"),
371 }
372 }
373
374 #[test]
375 fn try_from() {
376 if let Some(data_local_dir) = dirs::data_local_dir() {
377 let env_toml = data_local_dir.join("env.toml");
378 if let Ok(tmpfile) = OpenOptions::new()
379 .create(true)
380 .read(true)
381 .write(true)
382 .open(&env_toml)
383 {
384 let mut writer = BufWriter::new(tmpfile);
385 writer
386 .write_all(EXPECTED_TOML_STR.as_bytes())
387 .expect("Unable to write tmpfile");
388 }
389
390 let blah = format!("{}", data_local_dir.display());
391 let arg_vec: Vec<&str> = vec!["env-from-app-matches", "--envpath", &blah];
392 let matches = test_cli().get_matches_from(arg_vec);
393 match Environments::try_from(&matches) {
394 Ok(e) => {
395 let _b: Environments<Environment, RuntimeEnv> = e;
396 assert!(true);
397 }
398 Err(_) => assert!(false, "Unable to deserialize environments"),
399 }
400
401 remove_file(env_toml).expect("Unable to remove tmp 'env.toml'");
402 }
403 }
404}