tomlenv/env/
environments.rs

1// Copyright (c) 2018 deadmock developers
2//
3// Licensed under the Apache License, Version 2.0
4// <LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0> or the MIT
5// license <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
6// option. All files in the project carrying such notice may not be copied,
7// modified, or distributed except according to those terms.
8
9//! `tomlenv` environments configuration
10use 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/// Hold environment specific data as a map from your environment hierarchy key to data struct
23/// containg the config for that particular environment.
24///
25/// # Example
26///
27/// ```
28/// # use tomlenv::{Environment, Environments, Error, Result};
29/// # use std::env;
30/// # use std::io::Cursor;
31/// # use serde::{Deserialize, Serialize};
32/// # use getset::Getters;
33/// #
34/// # fn foo() -> Result<()> {
35/// // Your environment specific data struct
36/// // *NOTE*: This must implement `Deserialize` and `Serialize`
37/// #[derive(Debug, Deserialize, Getters, Serialize)]
38/// struct RuntimeEnv {
39///   #[get]
40///   name: String,
41///   #[get]
42///   key: Option<String>,
43/// }
44///
45/// // Your environment specific configuration
46/// let toml = r#"[envs.prod]
47/// name = "Production"
48/// key = "abcd-123-efg-45"
49///
50/// [envs.stage]
51/// name = "Stage"
52///
53/// [envs.test]
54/// name = "Test"
55///
56/// [envs.dev]
57/// name = "Development"
58///
59/// [envs.local]
60/// name = "Local"
61/// "#;
62///
63/// // Deserialize the TOML config into your environment structs
64/// let mut cursor = Cursor::new(toml);
65/// let envs: Environments<Environment, RuntimeEnv> = Environments::from_reader(&mut cursor)?;
66///
67/// // Test that all the environments are present
68/// env::set_var("env", "prod");
69/// let mut current = envs.current()?;
70/// assert_eq!(current.name(), "Production");
71/// assert_eq!(current.key(), &Some("abcd-123-efg-45".to_string()));
72///
73/// env::set_var("env", "stage");
74/// current = envs.current()?;
75/// assert_eq!(current.name(), "Stage");
76/// assert_eq!(current.key(), &None);
77///
78/// env::set_var("env", "test");
79/// current = envs.current()?;
80/// assert_eq!(current.name(), "Test");
81/// assert_eq!(current.key(), &None);
82///
83/// env::set_var("env", "dev");
84/// current = envs.current()?;
85/// assert_eq!(current.name(), "Development");
86/// assert_eq!(current.key(), &None);
87///
88/// env::set_var("env", "local");
89/// current = envs.current()?;
90/// assert_eq!(current.name(), "Local");
91/// assert_eq!(current.key(), &None);
92/// #   Ok(())
93/// # }
94/// ```
95#[derive(Clone, Debug, Deserialize, Ser)]
96pub struct Environments<S, T>
97where
98    S: Ord,
99{
100    /// A map of `Environment` to struct
101    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    /// Load the environments from a path.
110    ///
111    /// # Errors
112    ///
113    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    /// Load the environments from a reader.
128    ///
129    /// # Errors
130    ///
131    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    /// Get the current environment
141    ///
142    /// # Errors
143    ///
144    pub fn current(&self) -> Result<&T> {
145        self.current_from("env")
146    }
147
148    /// Get the current environment from the given variable
149    ///
150    /// # Errors
151    ///
152    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}