diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b81c037..7839d64 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,12 +16,12 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 10 steps: - - uses: actions/checkout@v3.6.0 + - uses: actions/checkout@v4.0.0 with: ref: ${{ github.ref_name }} - name: Cache Rust - uses: actions/cache@v3.3.1 + uses: actions/cache@v3.3.2 with: path: | ~/.cargo @@ -37,11 +37,6 @@ jobs: - name: Install cargo-release run: cargo install cargo-release - - name: Setup git - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions@users.noreply.github.com" - - name: Cargo login run: cargo login ${{ secrets.CRATES_API_TOKEN }} @@ -59,7 +54,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 5 steps: - - uses: actions/checkout@v3.6.0 + - uses: actions/checkout@v4.0.0 - name: Create Github release uses: softprops/action-gh-release@v1 @@ -77,10 +72,10 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 10 steps: - - uses: actions/checkout@v3.6.0 + - uses: actions/checkout@v4.0.0 - name: Cache Rust - uses: actions/cache@v3.3.1 + uses: actions/cache@v3.3.2 with: path: | ~/.cargo @@ -115,7 +110,7 @@ jobs: needs: release runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3.6.0 + - uses: actions/checkout@v4.0.0 - name: Merge ${{ github.base_ref }} -> develop uses: devmasx/merge-branch@v1.4.0 @@ -125,7 +120,7 @@ jobs: github_token: ${{ github.token }} type: now - - uses: actions/checkout@v3.6.0 + - uses: actions/checkout@v4.0.0 with: ref: develop diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 601f006..12dbf6b 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -27,10 +27,10 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v3.6.0 + - uses: actions/checkout@v4.0.0 - name: Cache Rust - uses: actions/cache@v3.3.1 + uses: actions/cache@v3.3.2 with: path: | ~/.cargo @@ -54,10 +54,10 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v3.6.0 + - uses: actions/checkout@v4.0.0 - name: Cache Rust - uses: actions/cache@v3.3.1 + uses: actions/cache@v3.3.2 with: path: | ~/.cargo @@ -89,7 +89,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 10 steps: - - uses: actions/checkout@v3.6.0 + - uses: actions/checkout@v4.0.0 - name: Check formatting run: cargo fmt --check @@ -103,10 +103,10 @@ jobs: os: [ubuntu-latest] timeout-minutes: 15 steps: - - uses: actions/checkout@v3.6.0 + - uses: actions/checkout@v4.0.0 - name: Cache Rust - uses: actions/cache@v3.3.1 + uses: actions/cache@v3.3.2 with: path: | ~/.cargo diff --git a/Cargo.toml b/Cargo.toml index 605fcbd..ce9b595 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ members = [ "env-settings-utils", "test-env-settings", ] +resolver = "2" [workspace.package] authors = ["Dario Curreri "] diff --git a/README.md b/README.md index dd6bd55..75095bd 100644 --- a/README.md +++ b/README.md @@ -77,27 +77,34 @@ struct MyStruct { #[env_settings(variable = "MY_BIRTH_DATE")] birth_date: String, - birth_place: Option + birth_place: Option, + + #[env_settings(skip)] + friends: Vec, } fn main() { - let my_struct = MyStruct::from_env().unwrap(); + let friends = vec!["luca".to_string()]; + let my_struct = MyStruct::from_env(friends.clone()).unwrap(); assert_eq!(my_struct.name, "paolo".to_string()); assert_eq!(my_struct.favourite_number, 42); assert_eq!(my_struct.birth_date, "01/01/1970"); assert_eq!(my_struct.birth_place, None); + assert_eq!(my_struct.friends, friends); let name = "luca"; let my_struct = MyStruct::new( Some(name.to_string()), None, None, - Some("london".to_string()) + Some("london".to_string()), + friends.clone(), ).unwrap(); assert_eq!(my_struct.name, name); assert_eq!(my_struct.favourite_number, 42); assert_eq!(my_struct.birth_date, "01/01/1970"); assert_eq!(my_struct.birth_place, Some("london".to_string())); + assert_eq!(my_struct.friends, friends); } ``` @@ -117,6 +124,7 @@ The current supported parameters for the structs are: The current supported parameters for the fields are: - `default`: the default value to use if the environment variable is not found. By default, it is not set +- `skip`: whether to skip the parsing of the environment variable - `variable`: the environment variable to use for the lookup. By default, the name of the field ### Variables resolution hierarchy diff --git a/env-settings-derive/src/lib.rs b/env-settings-derive/src/lib.rs index 91c68de..6bbc0a7 100644 --- a/env-settings-derive/src/lib.rs +++ b/env-settings-derive/src/lib.rs @@ -11,7 +11,6 @@ use proc_macro::TokenStream; use quote::quote; use std::collections::HashMap; use syn::parse; -use utils::EnvSettingsField; mod utils; @@ -26,12 +25,13 @@ pub fn env_settings_derive(input: TokenStream) -> TokenStream { } /// Implement the logic of the derive macro -fn implement(input: &utils::EnvSettingsInput) -> TokenStream { +fn implement(input: &utils::input::EnvSettingsInput) -> TokenStream { let struct_name = &input.name; let mut new_args = Vec::new(); let mut new_impls = Vec::new(); let mut from_env_impls = Vec::new(); + let mut from_env_args = Vec::new(); let mut env_variables_impls = quote! {}; let mut file_path_impls = quote! {}; @@ -59,99 +59,108 @@ fn implement(input: &utils::EnvSettingsInput) -> TokenStream { let prefix = input.params.prefix.clone().unwrap_or(String::default()); - for EnvSettingsField { - name, - type_, - default, - is_optional, - variable, - } in &input.fields - { - let mut env_variable = variable.to_owned().unwrap_or(format!("{}{}", prefix, name)); - if case_insensitive { - env_variable = env_variable.to_lowercase(); - } - let name_string = name.to_string(); - let type_string = type_ - .iter() - .map(|segment| segment.ident.to_string()) - .collect::>() - .join("::"); - - // the variable involded must be named `value` - let optional_value_impl = if *is_optional { - quote! { Some(value) } - } else { - quote! { value } - }; + for field in &input.fields { + match field { + utils::field::EnvSettingsField::NonParsable { name, type_ } => { + let argument = quote! { #name: #type_ }; + new_args.push(argument.clone()); + from_env_args.push(argument); + let value = quote! {#name}; + new_impls.push(value.clone()); + from_env_impls.push(value); + } + utils::field::EnvSettingsField::Parsable { + name, + name_label, + type_, + type_label, + default, + optional_type, + variable, + } => { + let mut env_variable = variable.to_owned().unwrap_or(format!("{}{}", prefix, name)); + if case_insensitive { + env_variable = env_variable.to_lowercase(); + } - // the variable involded must be named `value_to_parse` - let convert_err_impl = quote! { - return Err(env_settings_utils::EnvSettingsError::Convert( - #name_string, - value_to_parse.to_owned(), - #type_string, - )) - }; + // the variable involved must be named `value` + let (optional_value_impl, default_value_impl, new_arg_impl, parse_type) = + match optional_type { + Some(optional_type) => ( + quote! { Some(value) }, + quote! { None }, + quote! { #name: #type_ }, + optional_type, + ), + None => ( + quote! { value }, + quote! { return Err(env_settings_utils::EnvSettingsError::NotExists(#env_variable)) }, + quote! { #name: Option<#type_> }, + type_, + ), + }; + + // the variable involved must be named `value_to_parse` + let convert_err_impl = quote! { + return Err(env_settings_utils::EnvSettingsError::Convert( + #name_label, + value_to_parse.to_owned(), + #type_label, + )) + }; + let default_impl = match default { + Some(value_to_parse) => { + quote! { + match #value_to_parse.parse::<#parse_type>() { + Ok(value) => #optional_value_impl, + Err(_) => { + let value_to_parse = #value_to_parse.to_owned(); + #convert_err_impl + } + } + } + } + None => default_value_impl, + }; - let default_impl = match default { - Some(value_to_parse) => { - quote! { - match #value_to_parse.parse::<#type_>() { + // the variable involved must be named `value_to_parse` + let parse_impl = quote! { + match value_to_parse.parse::<#parse_type>() { Ok(value) => #optional_value_impl, - Err(_) => { - let value_to_parse = #value_to_parse.to_owned(); - #convert_err_impl + Err(_) => #convert_err_impl + } + }; + + // the variable involved must be named `env_variables` + let env_value_impl = if input.params.delay { + quote! { + match env_variables.get(#env_variable) { + Some(value_to_parse) => #parse_impl, + None => #default_impl, } } - } - } - None => { - if *is_optional { - quote! { None } } else { - quote! { return Err(env_settings_utils::EnvSettingsError::NotExists(#env_variable)) } - } - } - }; - - // the variable involded must be named `value_to_parse` - let parse_impl = quote! { - match value_to_parse.parse::<#type_>() { - Ok(value) => #optional_value_impl, - Err(_) => #convert_err_impl - } - }; - - // the variable involded must be named `env_variables` - let env_value_impl = if input.params.delay { - quote! { - match env_variables.get(#env_variable) { - Some(value_to_parse) => #parse_impl, - None => #default_impl, - } - } - } else { - match env_variables.get(&env_variable) { - Some(value_to_parse) => quote! { - { - let value_to_parse = #value_to_parse.to_owned(); - #parse_impl + match env_variables.get(&env_variable) { + Some(value_to_parse) => quote! { + { + let value_to_parse = #value_to_parse.to_owned(); + #parse_impl + } + }, + None => default_impl, } - }, - - None => quote! { #default_impl }, - } - }; + }; - new_impls.push(quote! { - #name: match #name { - Some(value) => #optional_value_impl, - None => #env_value_impl + new_impls.push(quote! { + #name: match #name { + Some(value) => #optional_value_impl, + None => #env_value_impl + } + }); + new_args.push(new_arg_impl); + from_env_impls.push(quote! { #name: #env_value_impl }); } - }); - new_args.push(quote! { #name: Option<#type_> }); - from_env_impls.push(quote! { #name: #env_value_impl }); + } } let pre_impls = quote! { @@ -171,7 +180,7 @@ fn implement(input: &utils::EnvSettingsInput) -> TokenStream { Ok(instance) } - fn from_env() -> env_settings_utils::EnvSettingsResult { + fn from_env(#(#from_env_args),*) -> env_settings_utils::EnvSettingsResult { #pre_impls let instance = Self { #(#from_env_impls),* diff --git a/env-settings-derive/src/utils.rs b/env-settings-derive/src/utils.rs index 442c2e4..b3dfe16 100644 --- a/env-settings-derive/src/utils.rs +++ b/env-settings-derive/src/utils.rs @@ -1,220 +1,3 @@ -use proc_macro2::TokenTree; -use std::collections::HashMap; -use syn::parse::{Parse, ParseStream}; -use syn::punctuated::Punctuated; -use syn::token::PathSep; -use syn::{ - Attribute, Data, DeriveInput, Error, Fields, Ident, Meta, MetaList, PathSegment, Result, Type, -}; - -/// The field info needed to the `EnvSettings` derive -#[derive(Debug)] -pub(crate) struct EnvSettingsField { - /// The name of the field - pub(crate) name: Ident, - - /// The type of the field - pub(crate) type_: Punctuated, - - /// The default value of the field - pub(crate) default: Option, - - /// Whether the field is optional or not - pub(crate) is_optional: bool, - - /// The environment variable name - pub(crate) variable: Option, -} - -/// The outer parameters of `EnvSettings` derive -#[derive(Debug, Default)] -pub(crate) struct EnvSettingsOuterParams { - /// Whether the environment variables matching should be case insensitive - pub(crate) case_insensitive: bool, - - /// Whether to delay the lookup for environment variables from compilation time to run time - pub(crate) delay: bool, - - /// The path of the file to load - pub(crate) file_path: Option, - - /// The prefix to add the name of the struct fields to match the environment variables - pub(crate) prefix: Option, -} - -/// The inner parameters of `EnvSettings` derive -#[derive(Debug, Default)] -pub(crate) struct EnvSettingsInnerParams { - /// The default value to use if the environment variable is not set - pub(crate) default: Option, - - /// The environment variable name - pub(crate) variable: Option, -} - -/// The `EnvSettings` macro input -#[derive(Debug)] -pub(crate) struct EnvSettingsInput { - /// The parameters of `EnvSettings` derive - pub(crate) params: EnvSettingsOuterParams, - - /// The identifier of the struct - pub(crate) name: Ident, - - /// The fields of the struct - pub(crate) fields: Vec, -} - -impl EnvSettingsInput { - /// Parse the attributes of the input - fn parse_attributes(attributes: &[Attribute]) -> Result>> { - let mut params = HashMap::new(); - for attribute in attributes { - if attribute.meta.path().is_ident("env_settings") { - if let Meta::List(MetaList { tokens, .. }) = &attribute.meta { - let mut tokens_iterator = tokens.clone().into_iter(); - while let Some(token) = tokens_iterator.next() { - match token { - TokenTree::Ident(ident) => { - if let Some(TokenTree::Punct(punct)) = tokens_iterator.next() { - match punct.as_char() { - '=' => { - if let Some(TokenTree::Literal(literal)) = - tokens_iterator.next() - { - let value = literal.to_string().replace('\"', ""); - params.insert(ident.to_string(), Some(value)); - } else { - return Err(Error::new( - punct.span(), - "literal value expected", - )); - } - } - ',' => { - params.insert(ident.to_string(), None); - } - _ => { - let error_message = - format!("punct value `{}` unexpected", punct); - return Err(Error::new(punct.span(), error_message)); - } - } - } else { - params.insert(ident.to_string(), None); - } - } - TokenTree::Punct(punct) => { - if punct.as_char() != ',' { - let error_message = - format!("punct value `{}` unexpected", punct); - return Err(Error::new(punct.span(), error_message)); - } - } - _ => { - let error_message = format!("token value `{}` unexpected", token); - return Err(Error::new(token.span(), error_message)); - } - }; - } - } - } - } - Ok(params) - } - - fn parse_inner_attributes(attributes: &[Attribute]) -> Result { - let params = Self::parse_attributes(attributes)?; - let mut env_settings_inner_params = EnvSettingsInnerParams::default(); - if let Some(default) = params.get("default") { - env_settings_inner_params.default = default.to_owned(); - } - if let Some(variable) = params.get("variable") { - env_settings_inner_params.variable = variable.to_owned(); - } - Ok(env_settings_inner_params) - } - - fn parse_outer_attributes(attributes: &[Attribute]) -> Result { - let params = Self::parse_attributes(attributes)?; - let mut env_settings_outer_params = EnvSettingsOuterParams::default(); - if params.contains_key("case_insensitive") { - env_settings_outer_params.case_insensitive = true; - } - if params.contains_key("delay") { - env_settings_outer_params.delay = true; - } - if let Some(file_path) = params.get("file_path") { - env_settings_outer_params.file_path = file_path.to_owned(); - } - if let Some(prefix) = params.get("prefix") { - env_settings_outer_params.prefix = prefix.to_owned(); - }; - Ok(env_settings_outer_params) - } - - /// Parse the data of the input - fn parse_data(data: &Data) -> Result> { - match data { - Data::Struct(_struct) => { - if let Fields::Named(names_fields) = &_struct.fields { - let mut fields = Vec::new(); - for field in &names_fields.named { - let params = Self::parse_inner_attributes(&field.attrs)?; - if let (Some(field_name), Type::Path(field_type)) = - (&field.ident, &field.ty) - { - let mut segments = field_type.path.segments.to_owned(); - let is_optional = if segments[0].ident == "Option" { - if let syn::PathArguments::AngleBracketed(arguments) = - &segments[0].arguments - { - if let syn::GenericArgument::Type(Type::Path(field_type)) = - &arguments.args[0] - { - segments = field_type.path.segments.to_owned() - } - } - true - } else { - false - }; - let field = EnvSettingsField { - name: field_name.to_owned(), - type_: segments, - default: params.default, - is_optional, - variable: params.variable, - }; - fields.push(field); - } - } - Ok(fields) - } else { - Err(Error::new( - _struct.struct_token.span, - "struct fields must be named", - )) - } - } - Data::Enum(_enum) => Err(Error::new(_enum.enum_token.span, "enum not supported")), - Data::Union(_union) => Err(Error::new(_union.union_token.span, "union not supported")), - } - } -} - -/// Implement the parse method for `EnvSettingsInput` -impl Parse for EnvSettingsInput { - fn parse(input: ParseStream) -> Result { - let ast = DeriveInput::parse(input)?; - let params = EnvSettingsInput::parse_outer_attributes(&ast.attrs)?; - let name = ast.ident; - let fields = EnvSettingsInput::parse_data(&ast.data)?; - let env_settings_input = EnvSettingsInput { - params, - name, - fields, - }; - Ok(env_settings_input) - } -} +mod attributes; +pub(crate) mod field; +pub(crate) mod input; diff --git a/env-settings-derive/src/utils/attributes.rs b/env-settings-derive/src/utils/attributes.rs new file mode 100644 index 0000000..fa596f7 --- /dev/null +++ b/env-settings-derive/src/utils/attributes.rs @@ -0,0 +1,2 @@ +pub(crate) mod inner; +pub(crate) mod outer; diff --git a/env-settings-derive/src/utils/attributes/inner.rs b/env-settings-derive/src/utils/attributes/inner.rs new file mode 100644 index 0000000..3b25776 --- /dev/null +++ b/env-settings-derive/src/utils/attributes/inner.rs @@ -0,0 +1,33 @@ +use crate::utils::input::EnvSettingsInput; + +use syn::{Attribute, Result}; + +/// The inner parameters of `EnvSettings` derive +#[derive(Debug, Default)] +pub(crate) struct EnvSettingsInnerParams { + /// The default value to use if the environment variable is not set + pub(crate) default: Option, + + /// The environment variable name + pub(crate) variable: Option, + + /// Whether to skip the parsing + pub(crate) skip: bool, +} + +impl EnvSettingsInnerParams { + pub(crate) fn parse_attributes(attributes: &[Attribute]) -> Result { + let params = EnvSettingsInput::parse_attributes(attributes)?; + let mut env_settings_inner_params = EnvSettingsInnerParams::default(); + if let Some(default) = params.get("default") { + env_settings_inner_params.default = default.to_owned(); + } + if let Some(variable) = params.get("variable") { + env_settings_inner_params.variable = variable.to_owned(); + } + if params.contains_key("skip") { + env_settings_inner_params.skip = true; + } + Ok(env_settings_inner_params) + } +} diff --git a/env-settings-derive/src/utils/attributes/outer.rs b/env-settings-derive/src/utils/attributes/outer.rs new file mode 100644 index 0000000..680803e --- /dev/null +++ b/env-settings-derive/src/utils/attributes/outer.rs @@ -0,0 +1,39 @@ +use crate::utils::input::EnvSettingsInput; + +use syn::{Attribute, Result}; + +/// The outer parameters of `EnvSettings` derive +#[derive(Debug, Default)] +pub(crate) struct EnvSettingsOuterParams { + /// Whether the environment variables matching should be case insensitive + pub(crate) case_insensitive: bool, + + /// Whether to delay the lookup for environment variables from compilation time to run time + pub(crate) delay: bool, + + /// The path of the file to load + pub(crate) file_path: Option, + + /// The prefix to add the name of the struct fields to match the environment variables + pub(crate) prefix: Option, +} + +impl EnvSettingsOuterParams { + pub(crate) fn parse_attributes(attributes: &[Attribute]) -> Result { + let params = EnvSettingsInput::parse_attributes(attributes)?; + let mut env_settings_outer_params = EnvSettingsOuterParams::default(); + if params.contains_key("case_insensitive") { + env_settings_outer_params.case_insensitive = true; + } + if params.contains_key("delay") { + env_settings_outer_params.delay = true; + } + if let Some(file_path) = params.get("file_path") { + env_settings_outer_params.file_path = file_path.to_owned(); + } + if let Some(prefix) = params.get("prefix") { + env_settings_outer_params.prefix = prefix.to_owned(); + }; + Ok(env_settings_outer_params) + } +} diff --git a/env-settings-derive/src/utils/field.rs b/env-settings-derive/src/utils/field.rs new file mode 100644 index 0000000..518d1bb --- /dev/null +++ b/env-settings-derive/src/utils/field.rs @@ -0,0 +1,133 @@ +use crate::utils::attributes::inner::EnvSettingsInnerParams; + +use syn::{ + punctuated, token, Attribute, Data, Error, Fields, GenericArgument, Ident, PathArguments, + PathSegment, Result, Type, TypePath, +}; + +/// The field info needed to the `EnvSettings` derive +pub(crate) enum EnvSettingsField { + NonParsable { + /// The name of the field + name: Ident, + + /// The type of the field + type_: Type, + }, + + Parsable { + /// The name of the field + name: Ident, + + /// The name label of the field + name_label: String, + + /// The type of the field + type_: Type, + + /// The type label of the field + type_label: String, + + /// The default value of the field + default: Option, + + /// The type specified in the option + optional_type: Option, + + /// The environment variable name + variable: Option, + }, +} + +impl EnvSettingsField { + fn get_field(type_: &Type, name: &Ident, attrs: &[Attribute]) -> Result { + let params = EnvSettingsInnerParams::parse_attributes(attrs)?; + let non_parsable_field = EnvSettingsField::NonParsable { + name: name.to_owned(), + type_: type_.to_owned(), + }; + let field = if params.skip { + non_parsable_field + } else { + match &type_ { + Type::Path(type_path) => { + Self::get_field_from_type_path(type_, type_path, name, params)? + } + _ => non_parsable_field, + } + }; + Ok(field) + } + + fn get_field_from_type_path( + type_: &Type, + type_path: &TypePath, + name: &Ident, + params: EnvSettingsInnerParams, + ) -> Result { + let mut segments = type_path.path.segments.to_owned(); + let optional_type = Self::get_optional_type(&segments); + if let Some(Type::Path(optional_type_path)) = &optional_type { + segments = optional_type_path.path.segments.to_owned(); + } + let type_label = segments + .into_iter() + .map(|segment| segment.ident.to_string()) + .collect::>() + .join("::"); + + let parsable_field = EnvSettingsField::Parsable { + name: name.to_owned(), + name_label: name.to_string(), + type_: type_.to_owned(), + type_label, + default: params.default, + optional_type, + variable: params.variable, + }; + Ok(parsable_field) + } + + fn get_optional_type( + segments: &punctuated::Punctuated, + ) -> Option { + if segments[0].ident == "Option" { + if let PathArguments::AngleBracketed(arguments) = &segments[0].arguments { + if let GenericArgument::Type(optional_type) = &arguments.args[0] { + Some(optional_type.to_owned()) + } else { + None + } + } else { + None + } + } else { + None + } + } + + /// Parse the fields of the input + pub(crate) fn parse_fields(data: &Data) -> Result> { + match data { + Data::Struct(_struct) => { + if let Fields::Named(names_fields) = &_struct.fields { + let mut fields = Vec::new(); + for field in &names_fields.named { + if let Some(field_name) = &field.ident { + let field = Self::get_field(&field.ty, field_name, &field.attrs)?; + fields.push(field); + } + } + Ok(fields) + } else { + Err(Error::new( + _struct.struct_token.span, + "struct fields must be named", + )) + } + } + Data::Enum(_enum) => Err(Error::new(_enum.enum_token.span, "enum not supported")), + Data::Union(_union) => Err(Error::new(_union.union_token.span, "union not supported")), + } + } +} diff --git a/env-settings-derive/src/utils/input.rs b/env-settings-derive/src/utils/input.rs new file mode 100644 index 0000000..3a94979 --- /dev/null +++ b/env-settings-derive/src/utils/input.rs @@ -0,0 +1,94 @@ +use crate::utils::{attributes, field}; + +use proc_macro2::TokenTree; +use std::collections::HashMap; +use syn::{parse, Attribute, DeriveInput, Error, Ident, Meta, MetaList, Result}; + +/// The `EnvSettings` macro input +pub(crate) struct EnvSettingsInput { + /// The parameters of `EnvSettings` derive + pub(crate) params: attributes::outer::EnvSettingsOuterParams, + + /// The identifier of the struct + pub(crate) name: Ident, + + /// The fields of the struct + pub(crate) fields: Vec, +} + +impl EnvSettingsInput { + /// Parse the attributes of the input + pub(crate) fn parse_attributes( + attributes: &[Attribute], + ) -> Result>> { + let mut params = HashMap::new(); + for attribute in attributes { + if attribute.meta.path().is_ident("env_settings") { + if let Meta::List(MetaList { tokens, .. }) = &attribute.meta { + let mut tokens_iterator = tokens.clone().into_iter(); + while let Some(token) = tokens_iterator.next() { + match token { + TokenTree::Ident(ident) => { + if let Some(TokenTree::Punct(punct)) = tokens_iterator.next() { + match punct.as_char() { + '=' => { + if let Some(TokenTree::Literal(literal)) = + tokens_iterator.next() + { + let value = literal.to_string().replace('\"', ""); + params.insert(ident.to_string(), Some(value)); + } else { + return Err(Error::new( + punct.span(), + "literal value expected", + )); + } + } + ',' => { + params.insert(ident.to_string(), None); + } + _ => { + let error_message = + format!("punct value `{}` unexpected", punct); + return Err(Error::new(punct.span(), error_message)); + } + } + } else { + params.insert(ident.to_string(), None); + } + } + TokenTree::Punct(punct) => { + if punct.as_char() != ',' { + let error_message = + format!("punct value `{}` unexpected", punct); + return Err(Error::new(punct.span(), error_message)); + } + } + _ => { + let error_message = format!("token value `{}` unexpected", token); + return Err(Error::new(token.span(), error_message)); + } + }; + } + } + } + } + Ok(params) + } +} + +/// Implement the parse method for `EnvSettingsInput` +impl parse::Parse for EnvSettingsInput { + fn parse(input: parse::ParseStream) -> Result { + let ast = DeriveInput::parse(input)?; + let params = attributes::outer::EnvSettingsOuterParams::parse_attributes(&ast.attrs)?; + let name = ast.ident; + let fields = field::EnvSettingsField::parse_fields(&ast.data)?; + let env_settings_input = EnvSettingsInput { + params, + name, + fields, + }; + Ok(env_settings_input) + } +} diff --git a/env-settings/src/lib.rs b/env-settings/src/lib.rs index a83e957..66979f6 100644 --- a/env-settings/src/lib.rs +++ b/env-settings/src/lib.rs @@ -5,7 +5,7 @@ html_favicon_url = "https://raw.githubusercontent.com/dariocurr/env-settings/main/docs/logo.ico" )] -//! # Env settings +//! # Env Settings //! //! **Env Settings** is a Rust library that helps you to initialize structs using environment variables //! @@ -90,26 +90,33 @@ //! #[env_settings(variable = "MY_BIRTH_DATE")] //! birth_date: String, //! -//! birth_place: Option +//! birth_place: Option, +//! +//! #[env_settings(skip)] +//! friends: Vec, //! } //! -//! let my_struct = MyStruct::from_env().unwrap(); +//! let friends = vec!["luca".to_string()]; +//! let my_struct = MyStruct::from_env(friends.clone()).unwrap(); //! assert_eq!(my_struct.name, "paolo".to_string()); //! assert_eq!(my_struct.favourite_number, 42); //! assert_eq!(my_struct.birth_date, "01/01/1970"); //! assert_eq!(my_struct.birth_place, None); +//! assert_eq!(my_struct.friends, friends); //! //! let name = "luca"; //! let my_struct = MyStruct::new( //! Some(name.to_string()), //! None, //! None, -//! Some("london".to_string()) +//! Some("london".to_string()), +//! friends.clone(), //! ).unwrap(); //! assert_eq!(my_struct.name, name); //! assert_eq!(my_struct.favourite_number, 42); //! assert_eq!(my_struct.birth_date, "01/01/1970"); //! assert_eq!(my_struct.birth_place, Some("london".to_string())); +//! assert_eq!(my_struct.friends, friends); //! ``` //! //! ### Parameters @@ -128,6 +135,7 @@ //! The current supported parameters for the fields are: //! //! - `default`: the default value to use if the environment variable is not found. By default, it is not set +//! - `skip`: whether to skip the parsing of the environment variable //! - `variable`: the environment variable to use for the lookup. By default, the name of the field //! //! ### Variables resolution hierarchy diff --git a/test-env-settings/src/lib.rs b/test-env-settings/src/lib.rs index 86c3cf7..dfa4424 100644 --- a/test-env-settings/src/lib.rs +++ b/test-env-settings/src/lib.rs @@ -12,7 +12,9 @@ mod case_insensitive; mod default; mod e2e; mod file_path; +mod option; mod prefix; +mod skip; mod variable; #[cfg(test)] diff --git a/test-env-settings/src/skip.rs b/test-env-settings/src/skip.rs new file mode 100644 index 0000000..a0f09af --- /dev/null +++ b/test-env-settings/src/skip.rs @@ -0,0 +1,102 @@ +#[cfg(test)] +mod tests { + + use crate::tests::with_env_variables; + + use env_settings_derive::EnvSettings; + use env_settings_utils::{EnvSettingsError, EnvSettingsResult}; + use rstest::rstest; + use std::collections::HashMap; + + #[derive(Debug, EnvSettings, PartialEq)] + #[env_settings(delay)] + struct TestEnvSettings { + #[env_settings(skip)] + names: Vec, + + age: u8, + } + + #[rstest] + #[case( + HashMap::from([]), + vec!["lorem".to_string()], + Err(EnvSettingsError::NotExists("age")) + )] + #[case( + HashMap::from([("age", "42")]), + vec!["lorem".to_string()], + Ok(TestEnvSettings { names: vec!["lorem".to_string()], age: 42 }) + )] + #[case( + HashMap::from([("name", "[other]"), ("age", "42")]), + vec!["lorem".to_string()], + Ok(TestEnvSettings { names: vec!["lorem".to_string()], age: 42 }) + )] + #[case( + HashMap::from([("age", "other")]), + vec!["lorem".to_string()], + Err(EnvSettingsError::Convert("age", "other".to_string(), "u8")) + )] + fn test_from_env( + #[case] env_variables: HashMap<&'static str, &'static str>, + #[case] names: Vec, + #[case] expected_result: EnvSettingsResult, + ) { + let _ = with_env_variables( + &env_variables, + || TestEnvSettings::from_env(names.clone()), + &expected_result, + ); + } + + #[rstest] + #[case( + HashMap::from([]), + vec!["lorem".to_string()], + None, + Err(EnvSettingsError::NotExists("age")) + )] + #[case( + HashMap::from([("name", "[other]"), ("age", "42")]), + vec!["lorem".to_string()], + None, + Ok(TestEnvSettings { names: vec!["lorem".to_string()], age: 42 }) + )] + #[case( + HashMap::from([]), + vec!["lorem".to_string()], + Some(42), + Ok(TestEnvSettings { names: vec!["lorem".to_string()], age: 42 }) + )] + #[case( + HashMap::from([("age", "42")]), + vec!["lorem".to_string()], + None, + Ok(TestEnvSettings { names: vec!["lorem".to_string()], age: 42 }) + )] + #[case( + HashMap::from([("age", "24")]), + vec!["lorem".to_string()], + Some(42), + Ok(TestEnvSettings { names: vec!["lorem".to_string()], age: 42 }) + )] + #[case( + HashMap::from([("age", "other")]), + vec!["lorem".to_string()], + None, + Err(EnvSettingsError::Convert("age", "other".to_string(), "u8")) + )] + fn test_new( + #[case] env_variables: HashMap<&'static str, &'static str>, + #[case] names: Vec, + #[case] age: Option, + #[case] expected_result: EnvSettingsResult, + ) { + let _ = with_env_variables( + &env_variables, + || TestEnvSettings::new(names.clone(), age), + &expected_result, + ); + } +}