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}