From c460f11dcea835c3b8c8b3014066ddfea84dd44c Mon Sep 17 00:00:00 2001 From: Dmitriy Pleshevskiy Date: Sun, 22 Dec 2019 13:13:29 +0300 Subject: [PATCH] Initial commit --- .env | 4 + .gitignore | 7 + Cargo.toml | 17 +++ LICENSE | 21 +++ README.md | 27 ++++ src/lib.rs | 308 ++++++++++++++++++++++++++++++++++++++++++ tests/config_macro.rs | 111 +++++++++++++++ 7 files changed, 495 insertions(+) create mode 100644 .env create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 src/lib.rs create mode 100644 tests/config_macro.rs diff --git a/.env b/.env new file mode 100644 index 0000000..3dfb2dc --- /dev/null +++ b/.env @@ -0,0 +1,4 @@ +DEBUG=1 +TESTING=0 +SECRET_KEY='hello:)' +DATABASE_URL='postgres:/' \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..060e7fb --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.idea/ + +/target +Cargo.lock + +src/main.rs +.env* diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..9cb5d91 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "itconfig" +version = "0.1.1" +authors = ["Dmitriy Pleshevskiy "] +description = "Easy build a configs from environment variables and use it in globally." +categories = ["config", "web-programming"] +keywords = ["config", "env", "macro", "configuration", "environment"] +edition = "2018" +license = "MIT" +repository = "https://github.com/icetemple/itconfig-rs" +homepage = "https://github.com/icetemple/itconfig-rs" +documentation = "https://docs.rs/itconfig" +readme = "README.md" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ae03a7f --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 IceTemple + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..a85ca02 --- /dev/null +++ b/README.md @@ -0,0 +1,27 @@ +# itconfig + +Simple configuration with macro for rust application. + +We recommend you start with the [documentation]. + + +## Example usage + +```rust +#[macro_use] extern crate itconfig; +use dotenv::dotenv; + +config! { + DATABASE_URL: bool, + HOST: String => "127.0.0.1", +} + +fn main () { + dotenv().ok(); + cfg::init(); + assert_eq(cfg::HOST(), String::from("127.0.0.1"); +} +``` + + +[documentation]: https://docs.rs/itconfig diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..6f96ebb --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,308 @@ +//! # itconfig +//! +//! Simple configuration with macro for rust application. +//! +//! +//! +//! ## Example usage +//! +//! ``` +//! #[macro_use] extern crate itconfig; +//! use dotenv::dotenv; +//! +//! config! { +//! DATABASE_URL: bool, +//! HOST: String => "127.0.0.1", +//! } +//! +//! fn main () { +//! dotenv().ok(); +//! cfg::init(); +//! assert_eq(cfg::HOST(), String::from("127.0.0.1"); +//! } + +#[doc(hidden)] +macro_rules! __impl_from_for_numbers { + ( + $($ty:ty),+ + ) => { + $( + impl From for $ty { + fn from(env: EnvValue) -> Self { + env.0.parse::().unwrap() + } + } + )* + } +} + + +#[derive(Debug)] +#[doc(hidden)] +pub struct EnvValue(String); + +impl EnvValue { + pub fn new(string: String) -> Self { + Self(string) + } +} + +__impl_from_for_numbers![ + i8, i16, i32, i64, i128, isize, + u8, u16, u32, u64, u128, usize, + f32, f64 +]; + +impl From for bool { + fn from(env: EnvValue) -> Self { + match env.0.to_lowercase().as_str() { + "true" | "1" | "t" | "on" => true, + _ => false, + } + } +} + +impl From for EnvValue { + fn from(val: String) -> Self { + Self(val) + } +} + +impl From for String { + fn from(env: EnvValue) -> Self { + env.0 + } +} + + +/// Creates new public 'cfg' mod with function fo get each environment variable of mapping. +/// +/// All variables are required and program will panic if some variables haven't value, but you +/// can add default value for specific variable. +/// +/// Example usage +/// ------------- +/// +/// ```rust +/// # #[macro_use] extern crate itconfig; +/// config! { +/// DATABASE_URL: bool, +/// } +/// +/// # fn main () { +/// # use std::env; +/// # env::set_var("DATABASE_URL", "sqlite://"); +/// # +/// # cfg::init(); +/// # assert_eq(cfg::DATABASE_URL(), true); +/// # } +/// ``` +/// +/// Config with default value +/// +/// ```rust +/// # #[macro_use] extern crate itconfig; +/// config! { +/// DATABASE_URL: bool, +/// HOST: String => "127.0.0.1", +/// } +/// +/// # fn main () { +/// # cfg::init(); +/// # assert_eq(cfg::HOST(), String::from("127.0.0.1"); +/// # } +/// ``` +/// +/// This module will also contain helper method: +/// +/// `init` +/// ------ +/// +/// If you miss some required variables your application will panic at startup. +/// Run this at the main function for check all required variables without default value. +/// +/// ```rust +/// #[macro_use] extern crate itconfig; +/// +/// config! { +/// DATABASE_URL: bool, +/// HOST: String => "127.0.0.1", +/// } +/// +/// fn main () { +/// cfg::init(); +/// assert_eq(cfg::HOST(), String::from("127.0.0.1"); +/// } +/// ``` +/// +/// Also dotenv module is supported +/// +/// ```rust +/// #[macro_use] extern crate itconfig; +/// use dotenv::dotenv; +/// +/// config! { +/// DATABASE_URL: bool, +/// HOST: String => "127.0.0.1", +/// } +/// +/// fn main () { +/// dotenv().ok(); +/// cfg::init(); +/// assert_eq(cfg::HOST(), String::from("127.0.0.1"); +/// } +/// ``` +/// +#[macro_export] +macro_rules! config { + ($($tokens:tt)*) => { + __config_parse_variables!( + tokens = [$($tokens)*], + variables = [], + ); + } +} + +#[macro_export] +#[doc(hidden)] +macro_rules! __invalid_config_syntax { + () => { + compile_error!( + "Invalid `config!` syntax. Please see the `config!` macro docs for more info." + ); + }; +} + + +#[macro_export] +#[doc(hidden)] +macro_rules! __config_parse_variables { + // Find general config of variable + ( + tokens = [ + $name:ident : $ty:ty => $default:expr, + $($rest:tt)* + ], + $($args:tt)* + ) => { + __config_parse_variables!( + current_variable = { + name = $name, + ty = $ty, + env_name = stringify!($name), + default = $default, + }, + tokens = [$($rest)*], + $($args)* + ); + }; + + ( + tokens = [ + $name:ident : $ty:ty, + $($rest:tt)* + ], + $($args:tt)* + ) => { + __config_parse_variables!( + current_variable = { + name = $name, + ty = $ty, + env_name = stringify!($name), + }, + tokens = [$($rest)*], + $($args)* + ); + }; + + // Done parsing variable + ( + current_variable = { + $($current_variable:tt)* + }, + tokens = $tokens:tt, + variables = [$($variables:tt,)*], + $($args:tt)* + ) => { + __config_parse_variables!( + tokens = $tokens, + variables = [$($variables,)* { $($current_variable)* },], + ); + }; + + // Done parsing all variables + ( + tokens = [], + $($args:tt)* + ) => { + __config_impl!($($args)*); + }; + + // Invalid syntax + ($($tokens:tt)*) => { + __invalid_config_syntax!(); + }; +} + + +#[macro_export] +#[doc(hidden)] +macro_rules! __config_impl { + ( + variables = [$({ + name = $name:ident, + $($variable:tt)* + },)+], + ) => { + pub mod cfg { + #![allow(non_snake_case)] + use std::env; + use $crate::EnvValue; + + pub fn init() { + $($name();)+ + } + + $(__config_variable! { + name = $name, + $($variable)* + })+ + } + }; +} + + +#[macro_export] +#[doc(hidden)] +macro_rules! __config_variable { + // Add method with default value + ( + name = $name:ident, + ty = $ty:ty, + env_name = $env_name:expr, + default = $default:expr, + ) => { + pub fn $name() -> $ty { + env::var($env_name) + .map(|val| EnvValue::from(val).into()) + .unwrap_or_else(|_| $default) + } + }; + + // Add method without default value + ( + name = $name:ident, + ty = $ty:ty, + env_name = $env_name:expr, + ) => { + pub fn $name() -> $ty { + env::var($env_name) + .map(|val| EnvValue::from(val).into()) + .unwrap_or_else(|_| { + panic!(format!(r#"Cannot read "{}" environment variable"#, $env_name)) + }) + + } + }; +} + diff --git a/tests/config_macro.rs b/tests/config_macro.rs new file mode 100644 index 0000000..bb93eed --- /dev/null +++ b/tests/config_macro.rs @@ -0,0 +1,111 @@ +use std::env; +#[macro_use] +extern crate it_config; + +#[test] +fn one_variable() { + config! { + DEBUG: bool => true, + } + + assert_eq!(cfg::DEBUG(), true); +} + +#[test] +fn few_variables() { + config! { + FOO: bool => true, + BAR: bool => false, + } + + assert_eq!(cfg::FOO(), true); + assert_eq!(cfg::BAR(), false); +} + +#[test] +fn different_types() { + config! { + NUMBER: i32 => 30, + BOOL: bool => true, + STRING: String => "string".to_string(), + } + + assert_eq!(cfg::NUMBER(), 30); + assert_eq!(cfg::BOOL(), true); + assert_eq!(cfg::STRING(), "string"); +} + +#[test] +fn convert_bool_type_from_env() { + env::set_var("T_BOOL", "t"); + env::set_var("TRUE_BOOL", "true"); + env::set_var("NUM_BOOL", "1"); + env::set_var("ON_BOOL", "on"); + env::set_var("CAMEL_CASE", "True"); + env::set_var("FALSE_BOOL", "false"); + + config! { + T_BOOL: bool, + TRUE_BOOL: bool, + NUM_BOOL: bool, + ON_BOOL: bool, + CAMEL_CASE: bool, + FALSE_BOOL: bool, + + } + + assert_eq!(cfg::T_BOOL(), true); + assert_eq!(cfg::TRUE_BOOL(), true); + assert_eq!(cfg::NUM_BOOL(), true); + assert_eq!(cfg::ON_BOOL(), true); + assert_eq!(cfg::CAMEL_CASE(), true); + assert_eq!(cfg::FALSE_BOOL(), false); +} + +#[test] +fn convert_number_value_from_env() { + env::set_var("I8", "10"); + env::set_var("I16", "10"); + env::set_var("I32", "10"); + env::set_var("I64", "10"); + env::set_var("I128", "10"); + env::set_var("ISIZE","10"); + env::set_var("U8", "10"); + env::set_var("U16", "10"); + env::set_var("U32", "10"); + env::set_var("U64", "10"); + env::set_var("U128", "10"); + env::set_var("USIZE","10"); + env::set_var("F32", "10"); + env::set_var("F64","10"); + + config! { + I8: i8, + I16: i16, + I32: i32, + I64: i64, + I128: i128, + ISIZE: isize, + U8: u8, + U16: u16, + U32: u32, + U64: u64, + U128: u128, + USIZE: usize, + F32: f32, + F64: f64, + } + + assert_eq!(cfg::I8(), 10); + assert_eq!(cfg::I16(), 10); + assert_eq!(cfg::I32(), 10); + assert_eq!(cfg::I64(), 10); + assert_eq!(cfg::ISIZE(), 10); + assert_eq!(cfg::U8(), 10); + assert_eq!(cfg::U16(), 10); + assert_eq!(cfg::U32(), 10); + assert_eq!(cfg::U64(), 10); + assert_eq!(cfg::USIZE(), 10); + assert_eq!(cfg::F32(), 10.0); + assert_eq!(cfg::F64(), 10.0); +}