diff --git a/Cargo.lock b/Cargo.lock index 52a6ae7..f5301a3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -22,14 +22,14 @@ dependencies = [ [[package]] name = "rust-decouple" -version = "0.2.0" +version = "0.3.0" dependencies = [ "rust_decouple_derive", ] [[package]] name = "rust_decouple_derive" -version = "0.1.0" +version = "0.2.0" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 636acc1..6758940 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,13 +10,13 @@ license = "MIT" keywords = ["config"] authors = ["joyanedel "] repository = "https://github.com/joyanedel/rust-decouple" -version = "0.2.0" +version = "0.3.0" edition = "2021" rust-version = "1.80.0" [dependencies] -rust_decouple_derive = { path = "./rust_decouple_derive", optional = true, version = "0.1.0" } +rust_decouple_derive = { path = "./rust_decouple_derive", optional = true, version = "0.2.0" } [features] -default = [] +default = ["derive"] derive = ["dep:rust_decouple_derive"] diff --git a/README.md b/README.md index 89c1673..99961ad 100644 --- a/README.md +++ b/README.md @@ -10,77 +10,104 @@ The benefits of the rust version is that the cast is automatically done by the l ### Basic usage -The most basic usage of the library is to get a variable from the environment, if it is not found, it will return an error. +The most basic usage of the library is to define a struct with the variables you want to decouple and then call the `parse` method on it. The library will automatically try to parse the environment variables and return a struct with the values. ```rs -use rust_decouple::macros::config; +use rust_decouple::Decouple; -let my_string: String = config!("VAR_NAME"); -``` +#[derive(Decouple)] +struct EnvVars { + api_key: String, +} -You can also specify a default value if the variable is not found +fn main() { + let constants = match EnvVars::parse() { + Ok(v) => v, + Err(e) => panic!("Error at parsing environment variables. Error: {e}"), + }; -```rs -use rust_decouple::macros::config; + println!("My secret API KEY value is: {}", constants.api_key) +} +``` -let my_string = config!("VAR_NAME", "default_value"); +If you have not set the environment variable `API_KEY`, this example program will panic with the message +```sh +Error at parsing environment variables. Error: Couldn't find variable `API_KEY` +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace ``` -In this case, the variable type will be inferred from the default value. -If the default value is ambiguous, you can specify the type like this: +Once you set the environment variable, the program will print the value of the variable. For instance, if you set the variable `API_KEY` to `123456`, the output will be: +```sh +My secret API KEY value is 123456 +``` -```rs -use rust_decouple::macros::config; +**Note** that this crate does not provide a way to set environment variables, you should set them before running your program, and as you might have noted, the library will look for the environment variable with the same name as the struct field in uppercase. -// The type is annotated by the user -let my_string: u8 = config!("VAR_NAME", 8); +## Advanced usage -// The type is inferred from the default value -let my_u8 = config!("VAR_NAME", 8u8); -``` +### Simple default values -Notice that this usage of default doesn't cover the case where the variable is found but is empty or invalid. +The library also provides a way to set default values for the variables but not using procedural macros in the current version. -#### Vectorized environment variables +```rs +use rust_decouple::core::Environment; -You can also get a vector of values from the environment, the values should be separated by a comma without spaces in between. +fn main() { + let api_key = Environment::from("API_KEY", Some("sample_api_key".to_string())); -```rs -use rust_decouple::macros::config_vec; + println!("My secret API KEY value is: {:?}", api_key) +} +``` -let my_vec: Vec = config_vec!("VAR_NAME"); -let my_vec = config_vec!("VAR_NAME", vec!["1", "2"]); -let my_vec: Vec = config_vec!("VAR_NAME", vec![1, 2]); +In this example, the library will look for the environment variable `API_KEY` and if it is not set, it will use the default +value `sample_api_key`. + +One possible output of this program will be: +```sh +My secret API KEY value is: Ok("sample_api_key") ``` -### Derived trait +The derive macro is based in this implementation, so anything you can do with the Decouple derive macro, you can do with the Environment struct. + +### Vector environment variables -You can also derive the `Decouple` trait for your structs, this will allow you to get the values from the environment in a more structured way as the example below: +The library also provides a way to parse environment variables as vectors. The library will look for the environment variable with the same name as the struct field in uppercase and will split the value by commas. ```rs use rust_decouple::Decouple; #[derive(Decouple)] -struct Test { - var_1: u8, - var_2: Vec, - var_3: Vec, +struct EnvVars { + api_keys: Vec, } fn main() { - let env_vars = Test::parse(); - println!("{}", env_vars.var_1); - println!("{:?}", env_vars.var_2); - println!("{:?}", env_vars.var_3); + let constants = match EnvVars::parse() { + Ok(v) => v, + Err(e) => panic!("Error at parsing environment variables. Error: {e}"), + }; + + println!("My secret API KEYS values are: {:?}", constants.api_keys); } ``` -The `Decouple` trait will automatically implement the `parse` method for your struct, this method will return a new instance of your struct with the values from the environment. -The environment variables will be searched in uppercase and snake case, so the variable `var_1` will be searched as `VAR_1` and `var_2` as `VAR_2`. +If you set the environment variable `API_KEYS` to `123456,7891011`, the output will be: +``` +My secret API KEYS values are: ["123456", "7891011"] +``` -To use it, you need to enable the feature `derive` in your `Cargo.toml` file: +And you can do the same with the VecEnvironment struct: + +```rs +use rust_decouple::core::VecEnvironment; + +fn main() { + let api_keys = VecEnvironment::from("API_KEYS", Some(vec!["sample_api_key".to_string()])); + println!("My secret API KEYS values are: {:?}", api_keys); +} +``` -```toml -[dependencies] -rust_decouple = { version = "0.2", features = ["derive"] } +And the output will be: +```sh +My secret API KEYS values are: Ok(["sample_api_key"]) ``` diff --git a/rust_decouple_derive/Cargo.toml b/rust_decouple_derive/Cargo.toml index 65f52e3..2bb635b 100644 --- a/rust_decouple_derive/Cargo.toml +++ b/rust_decouple_derive/Cargo.toml @@ -3,7 +3,7 @@ name = "rust_decouple_derive" authors = ["joyanedel proc_macro::TokenStream { let input = parse_macro_input!(input as DeriveInput); - let name = &input.ident; + let struct_name = &input.ident; let expanded = match input.data { syn::Data::Struct(ref data_struct) => { @@ -28,12 +28,13 @@ pub fn derive_env_var_parser(input: proc_macro::TokenStream) -> proc_macro::Toke let vec_fields = gen_fields(vec_fields); quote! { - impl Decouple for #name { - fn parse() -> Self { - Self { + impl Decouple for #struct_name { + type Error = rust_decouple::core::FromEnvironmentError; + fn parse() -> Result where Self: Sized { + Ok(Self { #non_vec_fields #vec_fields - } + }) } } } @@ -64,12 +65,40 @@ fn gen_fields(fields: Vec<&(Ident, Type, bool)>) -> proc_macro2::TokenStream { let is_vec = fields.iter().next().unwrap().2; if is_vec { + let extracted_field_types: Vec<_> = field_types + .iter() + .map(|ty| extract_inner_type_from_vec(ty).unwrap()) + .collect(); + quote! { - #(#field_names: rust_decouple::core::VecEnvironment::from(#field_names_uppercase, None) as #field_types,)* + #(#field_names: (rust_decouple::core::VecEnvironment::from::<#extracted_field_types>(#field_names_uppercase, None))?,)* } } else { quote! { - #(#field_names: rust_decouple::core::Environment::from::<#field_types>(#field_names_uppercase, None),)* + #(#field_names: (rust_decouple::core::Environment::from::<#field_types>(#field_names_uppercase, None))?,)* + } + } +} + +// This function checks if a type is a Vec and returns T if it is. +fn extract_inner_type_from_vec(ty: &Type) -> Option<&Type> { + // Check if the type is a Path (which represents types like Vec, Option, etc.) + if let Type::Path(type_path) = ty { + // Check if the path segments are not empty and match "Vec" + if let Some(path_segment) = type_path.path.segments.last() { + if path_segment.ident == "Vec" { + // Now we check the generic arguments (which would hold the inner type T) + if let PathArguments::AngleBracketed(angle_bracketed_args) = &path_segment.arguments + { + // Check if there is exactly one generic argument (Vec has one) + if let Some(GenericArgument::Type(inner_type)) = + angle_bracketed_args.args.first() + { + return Some(inner_type); // Return the inner type T + } + } + } } } + None } diff --git a/src/core.rs b/src/core.rs index 4bd5a81..4e9024a 100644 --- a/src/core.rs +++ b/src/core.rs @@ -1,45 +1,64 @@ -use std::{env, str::FromStr}; +use std::{env, fmt::Display, str::FromStr}; pub struct Environment; pub struct VecEnvironment; +#[derive(Debug, PartialEq)] +pub enum FromEnvironmentError { + VariableNotFoundError(String), + ParseVariableError(String, String), +} + +impl Display for FromEnvironmentError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::VariableNotFoundError(v) => write!(f, "Couldn't find variable `{}`", v), + Self::ParseVariableError(variable, value) => { + write!(f, "Couldn't parse `{}` from value: '{}'", variable, value) + } + } + } +} + impl Environment { /// Retrieve the environment variable parsed as `T` /// Panic if variable is not found and default value is not provided - pub fn from(var_name: &str, default: Option) -> T { + pub fn from(var_name: &str, default: Option) -> Result { let value = env::var(var_name); if value.is_err() && default.is_none() { - panic!("Couldn't find environment variable `{var_name}`"); + return Err(FromEnvironmentError::VariableNotFoundError( + var_name.to_string(), + )); } else if let Some(default_value) = default { - return default_value; + return Ok(default_value); } let value = value.unwrap(); - match T::from_str(&value) { - Ok(t) => t, - Err(_) => panic!("Couldn't parse `{var_name}` = {value}"), - } + T::from_str(value.as_ref()) + .map_err(|_| FromEnvironmentError::ParseVariableError(var_name.to_string(), value)) } } impl VecEnvironment { - pub fn from(var_name: &str, default: Option>) -> Vec { + pub fn from( + var_name: &str, + default: Option>, + ) -> Result, FromEnvironmentError> { let value = env::var(var_name); if value.is_err() && default.is_none() { - panic!("Couldn't find `{var_name}`"); + return Err(FromEnvironmentError::VariableNotFoundError( + var_name.to_string(), + )); } else if let Some(default_value) = default { - return default_value; + return Ok(default_value); } let value = value.unwrap(); - match value + value .split(",") .map(T::from_str) .collect::, _>>() - { - Ok(t) => t, - Err(_) => panic!("Couldn't parse `{var_name}` = {value}"), - } + .map_err(|_| FromEnvironmentError::ParseVariableError(var_name.to_string(), value)) } } @@ -47,61 +66,67 @@ impl VecEnvironment { mod tests { use std::env; - use crate::core::{Environment, VecEnvironment}; + use crate::core::{Environment, FromEnvironmentError, VecEnvironment}; #[test] fn parse_env_var_u8_correctly() { env::set_var("RIGHT_VALUE", "123"); - let result: u8 = Environment::from("RIGHT_VALUE", None); - assert_eq!(result, 123); + let result: Result = Environment::from("RIGHT_VALUE", None); + assert_eq!(result.unwrap(), 123); } #[test] fn parse_env_var_f64_correctly() { env::set_var("RIGHT_F64_VALUE", "12.345"); - let result: f64 = Environment::from("RIGHT_F64_VALUE", None); - assert_eq!(result, 12.345) + let result: Result = Environment::from("RIGHT_F64_VALUE", None); + assert_eq!(result.unwrap(), 12.345) } #[test] fn parse_not_set_env_var_with_default_value_correctly() { env::remove_var("NOT_SET_VALUE"); - let result: u8 = Environment::from("NOT_SET_VALUE", Some(42)); - assert_eq!(result, 42); + let result: Result = Environment::from("NOT_SET_VALUE", Some(42)); + assert_eq!(result.unwrap(), 42); } #[test] - #[should_panic(expected = "Couldn't find environment variable `NOT_SET_VALUE_2`")] fn parse_not_set_env_var_with_no_default_value_panics() { env::remove_var("NOT_SET_VALUE_2"); - Environment::from::("NOT_SET_VALUE_2", None); + let result = Environment::from::("NOT_SET_VALUE_2", None); + assert!(result.is_err_and( + |e| e == FromEnvironmentError::VariableNotFoundError("NOT_SET_VALUE_2".to_string()) + )) } #[test] - #[should_panic(expected = "Couldn't parse `WRONG_TYPED_VALUE` = 12r3")] fn parse_wrong_typed_value_panics() { env::set_var("WRONG_TYPED_VALUE", "12r3"); - Environment::from::("WRONG_TYPED_VALUE", None); + let result = Environment::from::("WRONG_TYPED_VALUE", None); + assert!(result.is_err_and(|e| e + == FromEnvironmentError::ParseVariableError( + "WRONG_TYPED_VALUE".to_string(), + "12r3".to_string() + ))) } #[test] fn parse_vec_of_string_values_correctly() { env::set_var("VEC_STRING_VAL", "hello,world"); - let result: Vec = VecEnvironment::from("VEC_STRING_VAL", None); + let result: Vec = VecEnvironment::from("VEC_STRING_VAL", None).unwrap(); assert_eq!(result, vec!["hello", "world"]); } #[test] fn parse_vec_of_usize_correctly() { env::set_var("VEC_USIZE_VAR", "0,1,2,3,4,5"); - let result: Vec = VecEnvironment::from("VEC_USIZE_VAR", None); + let result: Vec = VecEnvironment::from("VEC_USIZE_VAR", None).unwrap(); assert_eq!(result, vec![0, 1, 2, 3, 4, 5]); } #[test] fn parse_not_set_vec_of_u8_with_default_value_correctly() { env::remove_var("VEC_U8_WITH_DEFAULT"); - let result = VecEnvironment::from("VEC_U8_WITH_DEFAULT", Some(vec![5u8, 42])); + let result = VecEnvironment::from("VEC_U8_WITH_DEFAULT", Some(vec![5u8, 42])).unwrap(); assert_eq!(result, vec![5, 42]); } } diff --git a/src/lib.rs b/src/lib.rs index 01df3ed..06af466 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,4 @@ pub mod core; -pub mod macros; mod traits; #[cfg(feature = "derive")] diff --git a/src/macros.rs b/src/macros.rs deleted file mode 100644 index 4e0b0fc..0000000 --- a/src/macros.rs +++ /dev/null @@ -1,35 +0,0 @@ -/// Macro for Environment parser -/// ## Usage -/// ```rs -/// let variable: u8 = config!("U8_VAR"); -/// let variable_with_default: i32 = config!("NOT_DEFINED_I32_VAR", 0); -/// // Automatically inferred that `variable_with_default_and_type_inferred` is u8 due to the default value -/// let variable_with_default_and_inferred_type = config!("VAR", 0u8); -/// ``` -#[macro_export] -macro_rules! config { - ($var_name:expr, $default_value:expr) => { - rust_decouple::core::Environment::from($var_name, Some($default_value)) - }; - ($var_name:expr) => { - rust_decouple::core::Environment::from($var_name, None) - }; -} - -/// Macro for Vector environment parser -/// ## Usage -/// ```rs -/// let variable: Vec = config!("VEC_U8_VAR"); -/// let variable_with_default: Vec = config!("NOT_DEFINED_I32_VAR", vec![]); -/// // Automatically inferred that `variable_with_default_and_type_inferred` is u8 due to the default value -/// let variable_with_default_and_inferred_type = config!("VAR", vec![0u8]); -/// ``` -#[macro_export] -macro_rules! config_vec { - ($var_name:expr, $default_value:expr) => { - rust_decouple::core::VecEnvironment::from($var_name, Some($default_value)) - }; - ($var_name:expr) => { - rust_decouple::core::VecEnvironment::from($var_name, None); - }; -} diff --git a/src/traits.rs b/src/traits.rs index 9d0d446..a498293 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -1,3 +1,6 @@ pub trait Decouple { - fn parse() -> Self; + type Error; + fn parse() -> Result + where + Self: Sized; } diff --git a/tests/derive.rs b/tests/derive.rs new file mode 100644 index 0000000..32ae74b --- /dev/null +++ b/tests/derive.rs @@ -0,0 +1,41 @@ +use std::env; + +use rust_decouple::Decouple; + +#[test] +fn test_simple_struct_derive() { + #[derive(Decouple)] + struct Test { + test_simple_struct_env: u8, + } + + env::set_var("TEST_SIMPLE_STRUCT_ENV", "8"); + let result = Test::parse(); + + assert!(result.is_ok_and(|v| v.test_simple_struct_env == 8)) +} + +#[test] +fn test_simple_struct_derive_fails() { + #[derive(Decouple)] + struct Test { + _test_non_existing_env_var: u8, + } + + env::remove_var("_TEST_NON_EXISTING_ENV_VAR"); + let result = Test::parse(); + + assert!(result.is_err()) +} + +#[test] +fn test_vector_env_var_in_struct() { + #[derive(Decouple)] + struct Test { + vec_string_env: Vec, + } + env::set_var("VEC_STRING_ENV", "test_1,test_2"); + let result = Test::parse(); + + assert!(result.is_ok_and(|r| r.vec_string_env == vec!["test_1".to_owned(), "test_2".to_owned()])) +}