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}