fhttp_core/profiles/
mod.rs

1use std::collections::HashMap;
2use std::env::{self, VarError};
3use std::iter::Iterator;
4use std::path::{Path, PathBuf};
5use std::str::FromStr;
6
7use anyhow::{anyhow, Context, Result};
8use promptly::prompt;
9use serde::{Deserialize, Serialize};
10
11pub use profile_variable::ProfileVariable;
12
13use crate::path_utils::RelativePath;
14use crate::{Config, ResponseStore};
15
16mod profile_variable;
17
18pub struct Profiles;
19
20impl Profiles {
21    pub fn parse<P: AsRef<Path>>(path: P) -> Result<HashMap<String, Profile>> {
22        let path = path.as_ref();
23        let content = std::fs::read_to_string(path)
24            .with_context(|| format!("Error opening file {}", path.to_str().unwrap()))?;
25        let profiles = serde_json::from_str::<HashMap<String, _Profile>>(&content)
26            .with_context(|| format!("error reading profile from {}", path.to_str().unwrap()))?;
27        let ret = profiles
28            .into_iter()
29            .map(|(key, value)| {
30                let profile = Profile::new(path, value.variables);
31                (key, profile)
32            })
33            .collect::<HashMap<String, Profile>>();
34
35        Ok(ret)
36    }
37}
38
39#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
40struct _Profile {
41    pub variables: HashMap<String, ProfileVariable>,
42}
43
44#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
45pub struct Profile {
46    source_path: PathBuf,
47    variables: HashMap<String, ProfileVariable>,
48}
49
50impl Profile {
51    pub fn empty<T: Into<PathBuf>>(source_path: T) -> Self {
52        Profile {
53            source_path: source_path.into(),
54            variables: HashMap::new(),
55        }
56    }
57
58    pub fn new<T: Into<PathBuf>>(
59        source_path: T,
60        variables: HashMap<String, ProfileVariable>,
61    ) -> Self {
62        Profile {
63            source_path: source_path.into(),
64            variables,
65        }
66    }
67
68    pub fn defined_through_request<K: Into<String>>(&self, key: K) -> Option<PathBuf> {
69        let key = key.into();
70
71        match self.variables.contains_key(&key) {
72            true => match self.variables.get(&key) {
73                Some(ProfileVariable::Request { request }) => {
74                    Some(PathBuf::from_str(request).unwrap())
75                }
76                _ => None,
77            },
78            false => None,
79        }
80    }
81
82    pub fn get<'a, K: Into<&'a str>>(
83        &self,
84        key: K,
85        config: &Config,
86        response_store: &ResponseStore,
87        default: Option<&'a str>,
88        for_dependency: bool,
89    ) -> Result<String> {
90        let key = key.into();
91
92        match self.variables.get(key) {
93            Some(ProfileVariable::Request { request }) => {
94                Ok(response_store.get(&self.get_dependency_path(request)?))
95            }
96            Some(var) => var.get(config, for_dependency),
97            None => get_from_environment(key, config, default),
98        }
99    }
100
101    pub fn source_path(&self) -> &Path {
102        &self.source_path
103    }
104
105    pub fn variables(&self) -> Vec<&ProfileVariable> {
106        self.variables.values().collect()
107    }
108
109    pub fn override_with(&mut self, other: Profile) {
110        for (key, value) in other.variables {
111            self.variables.insert(key, value);
112        }
113    }
114}
115
116impl AsRef<Path> for Profile {
117    fn as_ref(&self) -> &Path {
118        &self.source_path
119    }
120}
121
122fn get_from_environment(key: &str, config: &Config, default: Option<&str>) -> Result<String> {
123    match env::var(key) {
124        Ok(value) => Ok(value),
125        Err(VarError::NotUnicode(_)) => {
126            Err(anyhow!("environment variable {} is not unicode!", key))
127        }
128        Err(VarError::NotPresent) => match default {
129            Some(default) => Ok(default.to_owned()),
130            None => match config.prompt_missing_env_vars() {
131                true => {
132                    let value = prompt::<String, _>(&key).unwrap();
133                    env::set_var(key, &value);
134                    Ok(value)
135                }
136                false => Err(anyhow!(format!("missing environment variable {}", key))),
137            },
138        },
139    }
140}
141
142#[cfg(test)]
143mod test {
144    use std::env;
145
146    use maplit::hashmap;
147
148    use crate::profiles::ProfileVariable;
149    use crate::test_utils::root;
150
151    use super::*;
152
153    #[test]
154    fn should_load_profiles() -> Result<()> {
155        let path = root().join("resources/test/profiles/profile1.json");
156        let profiles = Profiles::parse(path)?;
157        assert_eq!(
158            profiles,
159            hashmap! {
160                "development".into() => Profile {
161                    source_path: root().join("resources/test/profiles/profile1.json").path_buf(),
162                    variables: hashmap!{},
163                },
164                "testing".into() => Profile {
165                    source_path: root().join("resources/test/profiles/profile1.json").path_buf(),
166                    variables: hashmap!{
167                        "var1".into() => ProfileVariable::StringValue("value1".into())
168                    },
169                }
170            }
171        );
172
173        Ok(())
174    }
175
176    #[test]
177    fn get_should_get_variables() -> Result<()> {
178        let profile = Profile {
179            source_path: env::current_dir().unwrap(),
180            variables: hashmap! {
181                "a".into() => ProfileVariable::StringValue("b".into())
182            },
183        };
184
185        assert_eq!(
186            profile.get("a", &Config::default(), &ResponseStore::new(), None, true)?,
187            String::from("b")
188        );
189
190        Ok(())
191    }
192
193    #[test]
194    fn get_should_default_to_env_vars() -> Result<()> {
195        env::set_var("a", "A");
196
197        let profile = Profile {
198            source_path: env::current_dir().unwrap(),
199            variables: HashMap::new(),
200        };
201
202        assert_eq!(
203            profile.get("a", &Config::default(), &ResponseStore::new(), None, true)?,
204            String::from("A")
205        );
206
207        Ok(())
208    }
209
210    #[test]
211    fn override_with_should_merge() -> Result<()> {
212        let config = Config::default();
213        let response_store = ResponseStore::new();
214
215        let mut default = Profile::new(
216            env::current_dir().unwrap(),
217            hashmap! {
218                String::from("a") => ProfileVariable::StringValue(String::from("A")),
219                String::from("b") => ProfileVariable::StringValue(String::from("B"))
220            },
221        );
222        let local = Profile::new(
223            env::current_dir().unwrap(),
224            hashmap! {
225                String::from("b") => ProfileVariable::StringValue(String::from("BBB")),
226                String::from("c") => ProfileVariable::StringValue(String::from("CCC")),
227            },
228        );
229
230        default.override_with(local);
231        assert_eq!(default.get("a", &config, &response_store, None, true)?, "A");
232        assert_eq!(
233            default.get("b", &config, &response_store, None, true)?,
234            "BBB"
235        );
236        assert_eq!(
237            default.get("c", &config, &response_store, None, true)?,
238            "CCC"
239        );
240
241        Ok(())
242    }
243}