dotenv_enum/lib.rs
1pub mod env_errors;
2
3use std::env;
4use std::fmt::Debug;
5use std::str::FromStr;
6use strum::IntoEnumIterator;
7use env_errors::EnvEnumResult;
8
9/// # EnvironmentVariable
10/// This trait is a link between the dotenv and your enums.
11/// The macro env_enum simplifies significantly its creation and its safety.
12/// The macro creates a suite of test verifying the enum value exist withing the dotenv.
13///
14/// ## Macro use
15/// ```
16/// use dotenv_enum::{env_enum, EnvironmentVariable};
17/// use strum::IntoEnumIterator;
18///
19/// env_enum!(TheEnumNameEnv, enum_test_module, [ValueOne, ValueTwo]);
20/// ```
21///
22/// ## Self made
23/// ```
24/// use dotenv_enum::EnvironmentVariable;
25/// use strum::IntoEnumIterator;
26///
27/// #[derive(Copy, Clone, strum_macros::EnumIter, PartialEq, Debug)]
28/// enum TheEnumNameEnv {
29/// ValueOne,
30/// ValueTwo,
31/// // ...
32/// }
33///
34/// impl EnvironmentVariable for TheEnumNameEnv {
35/// fn get_key(&self) -> String {
36/// match self {
37/// TheEnumNameEnv::ValueOne => "THE_ENUM_NAME_VALUE_ONE".to_string(),
38/// TheEnumNameEnv::ValueTwo => "THE_ENUM_NAME_VALUE_TWO".to_string(),
39/// // ...
40/// }
41/// }
42/// }
43///
44/// mod enum_test_module {
45/// extern crate self as my_crate;
46/// use std::assert_ne;
47/// use strum::IntoEnumIterator;
48/// use dotenv_enum::EnvironmentVariable;
49///
50/// #[allow(non_snake_case)]
51/// mod when_using_an_element_it_should_be_in_dotenv {
52/// extern crate self as my_crate;
53/// use dotenv_enum::EnvironmentVariable;
54///
55/// #[test]
56/// fn ValueOne() {
57/// dotenv::dotenv().ok();
58/// assert!(!my_crate::$enum_name::$var_name.unwrap_value().is_empty());
59/// }
60///
61/// #[test]
62/// fn ValueTwo() {
63/// dotenv::dotenv().ok();
64/// assert!(!my_crate::$enum_name::$var_name.unwrap_value().is_empty());
65/// }
66/// //...
67/// }
68///
69/// #[test]
70/// fn when_comparing_elements_they_are_all_different() {
71/// my_crate::TheEnumNameEnv::iter().enumerate().for_each(|(index, env_var)| {
72/// my_crate::TheEnumNameEnv::iter().enumerate()
73/// .filter(|(index2, _)| index != *index2)
74/// .for_each(|(_, env_var2)| assert_ne!(env_var.get_key(), env_var2.get_key()));
75/// });
76/// }
77/// }
78/// ```
79pub trait EnvironmentVariable
80 where Self: IntoEnumIterator
81{
82 /// Get the key string of this enum value
83 /// ```
84 /// use dotenv_enum::{env_enum, EnvironmentVariable};
85 /// use strum::IntoEnumIterator;
86 ///
87 /// env_enum!(TheEnumNameEnv, enum_test_module, [ValueOne, ValueTwo]);
88 /// assert_eq!(TheEnumNameEnv::ValueOne.get_key(), "THE_ENUM_NAME_VALUE_ONE".to_string());
89 /// ```
90 fn get_key(&self) -> String;
91
92 /// Verify the key exist within the enum
93 /// ```
94 /// use dotenv_enum::{env_enum, EnvironmentVariable};
95 /// use strum::IntoEnumIterator;
96 ///
97 /// env_enum!(TheEnumNameEnv, enum_test_module, [ValueOne, ValueTwo]);
98 /// assert_eq!(TheEnumNameEnv::does_key_exist("THE_ENUM_NAME_VALUE_ONE"), true);
99 /// assert_eq!(TheEnumNameEnv::does_key_exist("THE_ENUM_NAME_VALUE_THREE"), false);
100 /// ```
101 fn does_key_exist(key: &str) -> bool where Self: IntoEnumIterator {
102 Self::get_enum_value_from_key(key).is_some()
103 }
104
105 /// Get the enum value having the key provided or None
106 /// ```
107 /// use dotenv_enum::{env_enum, EnvironmentVariable};
108 /// use strum::IntoEnumIterator;
109 ///
110 /// env_enum!(TheEnumNameEnv, enum_test_module, [ValueOne, ValueTwo]);
111 /// assert_eq!(TheEnumNameEnv::get_enum_value_from_key("THE_ENUM_NAME_VALUE_ONE"), Some(TheEnumNameEnv::ValueOne));
112 /// assert_eq!(TheEnumNameEnv::get_enum_value_from_key("THE_ENUM_NAME_VALUE_THREE"), None);
113 /// ```
114 fn get_enum_value_from_key(key: &str) -> Option<Self> {
115 <Self as IntoEnumIterator>::iter()
116 .find(|enum_value| enum_value.get_key() == key)
117 }
118
119 /// Get the value from the .env related to the enum value
120 /// ```
121 /// use dotenv_enum::{env_enum, EnvironmentVariable};
122 /// use strum::IntoEnumIterator;
123 /// use dotenv_enum::env_errors::EnvEnumResult;
124 ///
125 /// dotenv::dotenv().ok();
126 /// env_enum!(TheEnumNameEnv, enum_test_module, [ValueOne, ValueTwo]);
127 ///
128 /// // Assuming the line:
129 /// // THE_ENUM_NAME_VALUE_ONE = "val"
130 /// // exist in the .env
131 /// assert_eq!(TheEnumNameEnv::ValueOne.get_value(), EnvEnumResult::Ok("val".to_string()));
132 ///
133 /// // Assuming the key THE_ENUM_NAME_VALUE_TWO does not exist in .env
134 /// assert_eq!(TheEnumNameEnv::ValueTwo.get_value(), EnvEnumResult::Absent("No THE_ENUM_NAME_VALUE_TWO in .env file".to_string()));
135 /// ```
136 fn get_value(&self) -> EnvEnumResult<String> {
137 match env::var(self.get_key()) {
138 Ok(var) => EnvEnumResult::Ok(var),
139 Err(_) => EnvEnumResult::Absent(format!("No {} in .env file", self.get_key()))
140 }
141 }
142
143 /// Get the value from the .env related to the enum value and unwrap it
144 /// This function will panic instead of sending an Err
145 /// ```
146 /// use dotenv_enum::{env_enum, EnvironmentVariable};
147 /// use strum::IntoEnumIterator;
148 ///
149 /// dotenv::dotenv().ok();
150 /// env_enum!(TheEnumNameEnv, enum_test_module, [ValueOne, ValueTwo]);
151 ///
152 /// // Assuming the line:
153 /// // THE_ENUM_NAME_VALUE_ONE = "val"
154 /// // exist in the .env
155 /// assert_eq!(TheEnumNameEnv::ValueOne.unwrap_value(), "val".to_string());
156 /// ```
157 fn unwrap_value(&self) -> String {
158 self.get_value().panic_if_absent()
159 }
160
161 /// Get the value from the .env related to the enum value and casted it into the type T
162 /// ```
163 /// use dotenv_enum::{env_enum, EnvironmentVariable};
164 /// use strum::IntoEnumIterator;
165 /// use dotenv_enum::env_errors::EnvEnumResult;
166 ///
167 /// dotenv::dotenv().ok();
168 /// env_enum!(TheEnumNameEnv, enum_test_module, [ValueOne, ValueTwo]);
169 ///
170 /// // Assuming the line:
171 /// // THE_ENUM_NAME_VALUE_ONE = "val"
172 /// // exist in the .env
173 /// assert_eq!(TheEnumNameEnv::ValueOne.get_casted_value::<String>(), EnvEnumResult::Ok("val".to_string()));
174 /// assert_eq!(TheEnumNameEnv::ValueOne.get_casted_value::<u32>(), EnvEnumResult::IncorrectCast("Cannot cast THE_ENUM_NAME_VALUE_ONE into u32".to_string()));
175 ///
176 /// // Assuming the key THE_ENUM_NAME_VALUE_TWO does not exist in .env
177 /// assert_eq!(TheEnumNameEnv::ValueTwo.get_value(), EnvEnumResult::Absent("No THE_ENUM_NAME_VALUE_TWO in .env file".to_string()));
178 /// ```
179 fn get_casted_value<T: FromStr + std::clone::Clone>(&self) -> EnvEnumResult<T>
180 where <T as FromStr>::Err: Debug {
181 match self.get_value() {
182 EnvEnumResult::Ok(val) => match val.parse::<T>() {
183 Ok(var) => EnvEnumResult::Ok(var),
184 Err(_) => EnvEnumResult::IncorrectCast(format!(
185 "Cannot cast {} into {}",
186 self.get_key(),
187 std::any::type_name::<T>()
188 )),
189 },
190 EnvEnumResult::Absent(err) => EnvEnumResult::Absent(err),
191 EnvEnumResult::IncorrectCast(err) => EnvEnumResult::IncorrectCast(err)
192 }
193 }
194
195 /// Get the value from the .env related to the enum value and casted it into the type T
196 /// This function will panic instead of sending an Err
197 /// ```
198 /// use dotenv_enum::{env_enum, EnvironmentVariable};
199 /// use strum::IntoEnumIterator;
200 ///
201 /// dotenv::dotenv().ok();
202 /// env_enum!(TheEnumNameEnv, enum_test_module, [ValueOne, ValueTwo]);
203 ///
204 /// // Assuming the line:
205 /// // THE_ENUM_NAME_VALUE_ONE = "val"
206 /// // exist in the .env
207 /// assert_eq!(TheEnumNameEnv::ValueOne.unwrap_casted_value::<String>(), "val".to_string());
208 /// ```
209 fn unwrap_casted_value<T: FromStr + std::clone::Clone>(&self) -> T
210 where <T as FromStr>::Err: Debug {
211 self.get_casted_value::<T>().panic_if_absent()
212 }
213
214 /// Create a full capitalize, seperated by underscored, without suffix Env, and merge name_value
215 /// ```
216 /// use dotenv_enum::{env_enum, EnvironmentVariable};
217 /// use strum::IntoEnumIterator;
218 ///
219 /// env_enum!(TheEnumNameEnv, enum_test_module, [Value]);
220 ///
221 /// assert_eq!(TheEnumNameEnv::create_env_string("LolEnv", "AValue"), "LOL_A_VALUE".to_string())
222 /// ```
223 fn create_env_string(enum_name: &str, enum_value: &str) -> String {
224 let values = Self::get_env_strings(enum_value);
225 let mut name = Self::get_env_strings(enum_name);
226 let name_size = name.len() - 1;
227 if name[name_size].eq("Env") {
228 name.remove(name_size);
229 }
230 format!("{}_{}", name.join("_").to_uppercase(), values.join("_").to_uppercase())
231 }
232
233 /// Create a vector of all the words seperated by either underscores or capital letters
234 /// ```
235 /// use dotenv_enum::{env_enum, EnvironmentVariable};
236 /// use strum::IntoEnumIterator;
237 ///
238 /// env_enum!(TheEnumNameEnv, enum_test_module, [Value]);
239 ///
240 /// assert_eq!(TheEnumNameEnv::get_env_strings("LolEnv_Ok"), vec!["Lol", "Env", "Ok"])
241 /// ```
242 fn get_env_strings(enum_value: &str) -> Vec<String> {
243 Self::split_string_on_capitalize(enum_value.trim()
244 .to_string()
245 .chars()
246 .filter(|character: &char| !character.eq(&'_'))
247 .collect())
248 }
249
250 /// Split a string on capital letter and keep it
251 /// ```
252 /// use dotenv_enum::{env_enum, EnvironmentVariable};
253 /// use strum::IntoEnumIterator;
254 ///
255 /// env_enum!(TheEnumNameEnv, enum_test_module, [Value]);
256 ///
257 /// assert_eq!(TheEnumNameEnv::split_string_on_capitalize("LolEnv_Ok".to_string()), vec!["Lol", "Env_", "Ok"])
258 /// ```
259 fn split_string_on_capitalize(value: String) -> Vec<String> {
260 let mut enum_values: Vec<String> = vec![];
261 let mut size: usize;
262 for character in value.chars() {
263 size = enum_values.len();
264 if character.is_uppercase() || size == 0 {
265 enum_values.push(String::from(character));
266 } else {
267 enum_values[size - 1].push(character);
268 }
269 }
270 enum_values
271 }
272}
273
274/// # Environment Enum
275/// This macro creates an enum and implements the trait [EnvironmentVariable].
276/// It also creates tests to verify that the keys from the enums exists within the macro
277/// ```
278/// use dotenv_enum::{env_enum, EnvironmentVariable};
279/// use strum::IntoEnumIterator;
280///
281/// env_enum!(TheEnumNameEnv, enum_test_module, [Value]);
282/// ```
283/// The first value is the enum name (the suffix Env will be removed for the key value)
284/// The second value is the name of the test module
285/// The third value is a list of enum values
286#[macro_export]
287macro_rules! env_enum {
288 ($enum_name: ident, $env_test_name: ident, [$($var_name: ident), *]) => {
289 #[derive(Copy, Clone, strum_macros::EnumIter, PartialEq, Debug)]
290 pub enum $enum_name {
291 $($var_name,)*
292 }
293
294 impl EnvironmentVariable for $enum_name {
295 fn get_key(&self) -> String {
296 match self {
297 $($enum_name::$var_name => Self::create_env_string(stringify!($enum_name), stringify!($var_name)),)*
298 }
299 }
300 }
301
302 #[cfg(test)]
303 mod $env_test_name {
304 extern crate self as my_crate;
305 use strum::IntoEnumIterator;
306 use dotenv_enum::EnvironmentVariable;
307
308 #[allow(non_snake_case)]
309 mod when_using_an_element_it_should_be_in_dotenv {
310 extern crate self as my_crate;
311 use dotenv_enum::EnvironmentVariable;
312
313 $(#[test]
314 fn $var_name() {
315 dotenv::dotenv().ok();
316 assert!(!my_crate::$enum_name::$var_name.unwrap_value().is_empty());
317 })*
318 }
319
320 #[test]
321 fn when_comparing_elements_they_are_all_different() {
322 my_crate::$enum_name::iter().enumerate().for_each(|(index, env_var)| {
323 my_crate::$enum_name::iter().enumerate()
324 .filter(|(index2, _)| index != *index2)
325 .for_each(|(_, env_var2)| assert_ne!(env_var.get_key(), env_var2.get_key()));
326 });
327 }
328 }
329 };
330}
331
332#[cfg(test)]
333extern crate self as dotenv_enum;
334#[cfg(test)] env_enum!(AnEnv, an_test, [Lol, TeamJaws, Mdr]);
335#[cfg(test)] env_enum!(En, en_test, [Kappa, Pog, Mdr]);
336
337#[cfg(test)]
338mod tests {
339 use super::*;
340 use test_case::test_case;
341
342 #[test_case(AnEnv::Lol, ("AN_LOL", EnvEnumResult::Ok("waw".to_string())); "an lol")]
343 #[test_case(AnEnv::TeamJaws, ("AN_TEAM_JAWS", EnvEnumResult::Ok("jason".to_string())); "an team jaws")]
344 #[test_case(En::Pog, ("EN_POG", EnvEnumResult::Ok("champ".to_string())); "en pog")]
345 #[test_case(AnEnv::Mdr, ("AN_MDR", EnvEnumResult::Ok("11".to_string())); "an mdr")]
346 #[test_case(En::Mdr, ("EN_MDR", EnvEnumResult::Ok("54".to_string())); "en mdr")]
347 #[test_case(En::Kappa, ("EN_KAPPA", EnvEnumResult::Ok("12".to_string())); "en kappa")]
348 fn when_using_the_getter_on_str_env_it_should_return_the_right_value(env: impl EnvironmentVariable + Copy, expected: (&str, EnvEnumResult<String>)) {
349 test_env::<String>(env, (expected.0, expected.1));
350 }
351
352 #[test_case(AnEnv::Mdr, ("AN_MDR", EnvEnumResult::Ok(11)); "an mdr")]
353 #[test_case(En::Mdr, ("EN_MDR", EnvEnumResult::Ok(54)); "en mdr")]
354 #[test_case(En::Kappa, ("EN_KAPPA", EnvEnumResult::Ok(12)); "en kappa")]
355 #[test_case(AnEnv::Lol, ("AN_LOL", EnvEnumResult::IncorrectCast("Cannot cast AN_LOL into u32".to_string())); "an lol")]
356 #[test_case(AnEnv::TeamJaws, ("AN_TEAM_JAWS", EnvEnumResult::IncorrectCast("Cannot cast AN_TEAM_JAWS into u32".to_string())); "an team jaws")]
357 #[test_case(En::Pog, ("EN_POG", EnvEnumResult::IncorrectCast("Cannot cast EN_POG into u32".to_string())); "en pog")]
358 fn when_using_the_getter_on_number_it_should_return_the_right_value(env: impl EnvironmentVariable + Copy, expected: (&str, EnvEnumResult<u32>)) {
359 test_env::<u32>(env, expected);
360 }
361
362 fn test_env<T: FromStr + PartialEq + Debug + Clone>(env: impl EnvironmentVariable + Copy + Sized, expected: (&str, EnvEnumResult<T>)) where <T as std::str::FromStr>::Err: Debug {
363 dotenv::dotenv().ok();
364 assert_eq!(env.get_key(), expected.0);
365 assert_eq!(env.get_casted_value::<T>(), expected.1);
366 }
367
368 #[test_case("AN_MDR", Some(AnEnv::Mdr); "is present")]
369 #[test_case("LOLLL", None; "is not present")]
370 fn when_looking_from_string_you_can_get_the_enum_value(key: &str, expected: Option<AnEnv>) {
371 assert_eq!(AnEnv::get_enum_value_from_key(key), expected);
372 assert_eq!(AnEnv::does_key_exist(key), expected.is_some())
373 }
374}