Skip to content

Commit

Permalink
Replace current implementation for try from (#5)
Browse files Browse the repository at this point in the history
* Return parsed environment variables as a result

* Remove macro support

* Refactor derive_env_var_parser to return a Result

* Fix Vec environment variables procedural macro issue

* Update rust-decouple to version 0.3.0
  • Loading branch information
joyanedel authored Sep 22, 2024
1 parent 4a8a145 commit c639bea
Show file tree
Hide file tree
Showing 10 changed files with 211 additions and 122 deletions.
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ license = "MIT"
keywords = ["config"]
authors = ["joyanedel <[email protected]>"]
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"]
109 changes: 68 additions & 41 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> = config_vec!("VAR_NAME");
let my_vec = config_vec!("VAR_NAME", vec!["1", "2"]);
let my_vec: Vec<u8> = 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<i32>,
var_3: Vec<String>,
struct EnvVars {
api_keys: Vec<String>,
}
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"])
```
2 changes: 1 addition & 1 deletion rust_decouple_derive/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name = "rust_decouple_derive"
authors = ["joyanedel <[email protected]"]
description = "A simple library to ease the process of parsing environment variables subcrate"
license = "MIT"
version = "0.1.0"
version = "0.2.0"
edition = "2021"

[dependencies]
Expand Down
45 changes: 37 additions & 8 deletions rust_decouple_derive/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
use quote::quote;
use syn::{parse_macro_input, DeriveInput, Ident, Type};
use syn::{parse_macro_input, DeriveInput, GenericArgument, Ident, PathArguments, Type};

#[proc_macro_derive(Decouple)]
pub fn derive_env_var_parser(input: proc_macro::TokenStream) -> 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) => {
Expand All @@ -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<Self, Self::Error> where Self: Sized {
Ok(Self {
#non_vec_fields
#vec_fields
}
})
}
}
}
Expand Down Expand Up @@ -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<T> 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<T> has one)
if let Some(GenericArgument::Type(inner_type)) =
angle_bracketed_args.args.first()
{
return Some(inner_type); // Return the inner type T
}
}
}
}
}
None
}
Loading

0 comments on commit c639bea

Please sign in to comment.