Merge pull request #27 from pleshevskiy/redesign

Design improvements
This commit is contained in:
Dmitriy Pleshevskiy 2022-07-24 12:48:44 +00:00 committed by GitHub
commit 3686486847
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 457 additions and 2382 deletions

3
.vim/coc-settings.json Normal file
View File

@ -0,0 +1,3 @@
{
"rust-analyzer.cargo.features": "all"
}

1371
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,49 @@
[workspace]
members = [
"itconfig",
"itconfig-macro",
"itconfig-tests",
"estring",
"enve_mod",
]
[package]
name = "enve"
version = "1.1.1"
authors = ["Dmitriy Pleshevskiy <dmitriy@ideascup.me>"]
description = "Easy build a configs from environment variables and use it in globally."
categories = ["config", "web-programming"]
keywords = ["config", "env", "configuration", "environment", "macro"]
edition = "2018"
license = "MIT"
repository = "https://github.com/pleshevskiy/enve"
homepage = "https://github.com/pleshevskiy/enve"
documentation = "https://docs.rs/enve"
readme = "../README.md"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
default = []
number = ["estring/number"]
bool = ["estring/bool"]
vec = ["estring/vec"]
macro = ["enve_mod"]
[dependencies]
estring = "0.1"
enve_mod = { version = "1.1", path = "./enve_mod", optional = true }
[dev-dependencies]
lazy_static = "1.4.0"
criterion = "0.3.1"
[badges]
maintenance = { status = "actively-developed" }
# https://docs.rs/about
[package.metadata.docs.rs]
all-features = true
[[example]]
name = "calc"
required-features = ["number", "vec"]

View File

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2019 IceTemple
Copyright (c) 2019-2022 pleshevskiy
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@ -1,10 +1,10 @@
# itconfig
# enve
[![CI](https://github.com/icetemple/itconfig-rs/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/icetemple/itconfig-rs/actions/workflows/ci.yml)
[![CI](https://github.com/pleshevskiy/enve/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/pleshevskiy/enve/actions/workflows/ci.yml)
[![unsafe forbidden](https://img.shields.io/badge/unsafe-forbidden-success.svg)](https://github.com/rust-secure-code/safety-dance/)
[![Documentation](https://docs.rs/itconfig/badge.svg)](https://docs.rs/itconfig)
[![Crates.io](https://img.shields.io/crates/v/itconfig)](https://crates.io/crates/itconfig)
![Crates.io](https://img.shields.io/crates/l/itconfig)
[![Documentation](https://docs.rs/pleshevskiy/badge.svg)](https://docs.rs/enve)
[![Crates.io](https://img.shields.io/crates/v/enve)](https://crates.io/crates/enve)
![Crates.io](https://img.shields.io/crates/l/enve)
Easy build a configs from environment variables and use it in globally.
@ -23,7 +23,7 @@ of it I decided to create my own library.
The MSRV is 1.39.0
Add `itconfig = { version = "1.0", features = ["macro"] }` as a dependency in
Add `enve = { version = "1.0", features = ["mod"] }` as a dependency in
`Cargo.toml`.
`Cargo.toml` example:
@ -35,17 +35,16 @@ version = "0.1.0"
authors = ["Me <user@rust-lang.org>"]
[dependencies]
itconfig = { version = "1.0", features = ["macro"] }
enve = { version = "1.0", features = ["mod"] }
```
## Basic usage
```rust
use itconfig::config;
use std::env;
//use dotenv::dotenv;
config! {
enve::mod! {
DEBUG: bool => false,
#[env_name = "APP_HOST"]
@ -97,7 +96,6 @@ Macro is an optional feature, disabled by default. You can use this library
without macro
```rust
use itconfig::*;
use std::env;
// use dotenv::dotenv;
@ -106,9 +104,9 @@ fn main() {
// or
env::set_var("DATABASE_URL", "postgres://127.0.0.1:5432/test");
let database_url = get_env::<String>("DATABASE_URL").unwrap();
let new_profile: bool = get_env_or_default("FEATURE_NEW_PROFILE", false);
let articles_per_page: u32 = get_env_or_set_default("ARTICLES_PER_PAGE", 10);
let database_url = enve::get::<String>("DATABASE_URL").unwrap();
let new_profile: bool = enve::get("FEATURE_NEW_PROFILE").unwrap_or_default();
let articles_per_page: u32 = enve::get_or_set_default("ARTICLES_PER_PAGE", 10);
}
```
@ -120,39 +118,19 @@ cargo test --all-features
## Available features
- **default** - ["primitives"]
- **macro** - Activates `config!` macros for easy configure web application.
- **primitives** - Group for features: `numbers` and `bool`.
- **numbers** - Group for features: `int`, `uint` and `float`.
- **int** - Group for features: `i8`, `i16`, `i32`, `i64`, `i128` and `isize`.
- **uint** - Group for features: `u8`, `u16`, `u32`, `u64`, `u128` and `usize`.
- **float** - Group for features: `f32` and `f64`
- **i8** - impl EnvString for `i8` type
- **i16** - impl EnvString for `i16` type
- **i32** - impl EnvString for `i32` type
- **i64** - impl EnvString for `i64` type
- **i128** - impl EnvString for `i128` type
- **isize** - impl EnvString for `isize` type
- **u8** - impl EnvString for `u8` type
- **u16** - impl EnvString for `u16` type
- **u32** - impl EnvString for `u32` type
- **u64** - impl EnvString for `u64` type
- **u128** - impl EnvString for `u128` type
- **usize** - impl EnvString for `usize` type
- **f32** - impl EnvString for `f32` type
- **f64** - impl EnvString for `f64` type
- **bool** - impl EnvString for `bool` type
- **json_array** - Add EnvString impl for vector type (uses optional
`serde_json` package). ⚠ **_DEPRECATED_**
- **number** - Group for features: `int`, `uint` and `float`.
- **bool** - impl EnvString for `bool` type `serde_json` package). ⚠
**_DEPRECATED_**
## License
[MIT] © [Ice Temple](https://github.com/icetemple)
[MIT] © [pleshevskiy](https://github.com/pleshevskiy)
## Contributors
[pleshevskiy](https://github.com/pleshevskiy) (Dmitriy Pleshevskiy) creator,
maintainer.
[documentation]: https://docs.rs/itconfig
[MIT]: https://github.com/icetemple/itconfig-rs/blob/master/LICENSE
[documentation]: https://docs.rs/enve
[MIT]: https://github.com/icetemple/enve-rs/blob/master/LICENSE

View File

@ -4,14 +4,14 @@ use std::env;
#[macro_use]
extern crate lazy_static;
#[macro_use]
extern crate itconfig;
extern crate enve;
fn setup_env_var(key: &'static str, initial: String) {
env::set_var(key, initial);
}
fn source_get_env() -> u32 {
itconfig::get_env::<u32>("TEST").unwrap()
enve::get::<u32>("TEST").unwrap()
}
fn lazy_get_env() -> u32 {

View File

@ -1,5 +1,5 @@
[package]
name = "itconfig-macro"
name = "enve_mod"
version = "1.1.1"
authors = ["Dmitriy Pleshevskiy <dmitriy@ideascup.me>"]
description = "Easy build a configs from environment variables and use it in globally."
@ -7,9 +7,9 @@ categories = ["config", "web-programming"]
keywords = ["config", "env", "configuration", "environment", "macro"]
edition = "2018"
license = "MIT"
repository = "https://github.com/icetemple/itconfig-rs"
homepage = "https://github.com/icetemple/itconfig-rs"
documentation = "https://docs.rs/itconfig"
repository = "https://github.com/pleshevskiy/enve"
homepage = "https://github.com/pleshevskiy/enve"
documentation = "https://docs.rs/enve"
readme = "../README.md"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@ -22,9 +22,8 @@ quote = "1.0.9"
proc-macro2 = "1.0.24"
[dev-dependencies]
itconfig = { path = "../itconfig" }
enve = { path = ".." }
lazy_static = "1.4.0"
[badges]
travis-ci = { repository = "icetemple/itconfig-rs" }
maintenance = { status = "passively-maintained" }
maintenance = { status = "actively-developed" }

15
examples/README.md Normal file
View File

@ -0,0 +1,15 @@
# Examples
## Calculator
Fun calculator based on environment variables
```
E=2*2-1-1+5*3-10 cargo run --example calc --all-features
```
Limits:
- Supports `*`, `+`, `-`
- You cannot start from a negative number. `E=-10`. Solution: start from `0`.
`E=0-10`.

36
examples/calc.rs Normal file
View File

@ -0,0 +1,36 @@
use enve::estr::SepVec;
type MinusVec<T> = SepVec<T, '-'>;
type PlusVec<T> = SepVec<T, '+'>;
type MulVec<T> = SepVec<T, '*'>;
const HELP_MESSAGE: &str = "
USAGE:
E=10+10*2+4 cargo run --example calc --all-features
";
fn main() -> Result<(), enve::Error> {
let res: f32 = enve::get::<PlusVec<MinusVec<MulVec<f32>>>>("E")
.map_err(|err| {
match err {
enve::Error::NotPresent => eprintln!("The expression was not found"),
rest => eprintln!("ERROR: {}", rest),
}
eprintln!("{}", HELP_MESSAGE);
std::process::exit(0);
})
.unwrap()
.iter()
.map(|p| {
p.iter()
.map(|m| m.iter().product::<f32>())
.reduce(|acc, v| acc - v)
.unwrap_or_default()
})
.sum::<f32>();
println!("result: {}", res);
Ok(())
}

View File

@ -1,43 +0,0 @@
use std::convert::Infallible;
use hyper::service::{make_service_fn, service_fn};
use hyper::{Body, Request, Response, Server};
itconfig::config! {
hyper {
static HOST < (
ADDR => "127.0.0.1",
":",
PORT => 8000,
),
}
}
async fn hello(_: Request<Body>) -> Result<Response<Body>, Infallible> {
Ok(Response::new(Body::from("Hello World!")))
}
#[tokio::main]
pub async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
config::init();
pretty_env_logger::init();
// For every connection, we must make a `Service` to handle all
// incoming HTTP requests on said connection.
let make_svc = make_service_fn(|_conn| {
// This is the `Service` that will handle the connection.
// `service_fn` is a helper to convert a function that
// returns a Response into a `Service`.
async { Ok::<_, Infallible>(service_fn(hello)) }
});
let addr = config::hyper::HOST().parse()?;
let server = Server::bind(&addr).serve(make_svc);
println!("Listening on http://{}", addr);
server.await?;
Ok(())
}

View File

@ -1,11 +0,0 @@
# Hyper "hello world"
```bash
cargo run --example hyper
```
# Rocket "hello world"
```bash
cargo run --example roket
```

View File

@ -1,22 +0,0 @@
#[macro_use]
extern crate rocket;
itconfig::config! {
rocket {
HOST: String => "localhost",
PORT: u16 => 9000,
BASE_URL => "/",
}
}
#[get("/")]
fn hello() -> &'static str {
"Hello, world!"
}
#[launch]
fn rocket() -> _ {
config::init();
rocket::build().mount(config::rocket::BASE_URL(), routes![hello])
}

View File

@ -1,22 +0,0 @@
[package]
name = "itconfig_tests"
version = "0.1.0"
authors = ["Dmitriy Pleshevskiy <dmitriy@ideascup.me>"]
edition = "2018"
license = "MIT"
publish = false
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
itconfig = { path = '../itconfig', features = ["macro"] }
criterion = "0.3.1"
lazy_static = "1.4.0"
[features]
default = ["meta_namespace"]
meta_namespace = []
[[bench]]
name = "main_benches"
harness = false

View File

@ -1,81 +0,0 @@
[package]
name = "itconfig"
version = "1.1.1"
authors = ["Dmitriy Pleshevskiy <dmitriy@ideascup.me>"]
description = "Easy build a configs from environment variables and use it in globally."
categories = ["config", "web-programming"]
keywords = ["config", "env", "configuration", "environment", "macro"]
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
[features]
default = ["primitives"]
macro = ["itconfig-macro"]
primitives = ["numbers", "bool"]
numbers = ["int", "uint", "float"]
int = ["i8", "i16", "i32", "i64", "i128", "isize"]
uint = ["u8", "u16", "u32", "u64", "u128", "usize"]
float = ["f32", "f64"]
i8 = []
i16 = []
i32 = []
i64 = []
i128 = []
isize = []
u8 = []
u16 = []
u32 = []
u64 = []
u128 = []
usize = []
f32 = []
f64 = []
bool = []
# deprecated since 1.1
json_array = ["serde_json"]
[dependencies]
serde_json = { version = "1", optional = true }
itconfig-macro = { version = "1.1", path = "../itconfig-macro", optional = true }
[dev-dependencies]
lazy_static = "1.4.0"
# required for examples
rocket = "0.5.0-rc.2"
hyper = { version = "0.14.4", features = ["full"] }
serde_json = "1.0.62"
tokio = { version = "1.2.0", features = ["macros", "rt-multi-thread"] }
bytes = "1.0.1"
futures-util = { version = "0.3.13", default-features = false }
pretty_env_logger = "0.4.0"
[badges]
maintenance = { status = "passively-maintained" }
# https://docs.rs/about
[package.metadata.docs.rs]
all-features = true
[[example]]
name = "hyper"
path = "../examples/hyper.rs"
required-features = ["macro"]
[[example]]
name = "rocket"
path = "../examples/rocket.rs"
required-features = ["macro"]

View File

@ -1,176 +0,0 @@
use std::ops::Deref;
/// Wrapper under String type.
///
/// When we read the environment variable, we automatically convert the value
/// to EnvString and then convert it to your expected type.
///
#[derive(Debug, Default, PartialEq, Eq, Clone)]
pub struct EnvString(String);
impl<T> From<T> for EnvString
where
T: ToEnvString,
{
fn from(val: T) -> Self {
val.to_env_string()
}
}
impl Deref for EnvString {
type Target = String;
fn deref(&self) -> &Self::Target {
&self.0
}
}
/// A trait for converting value to EnvString.
///
/// This trait automatically implemented for any type which implements the
/// [`Display`] trait. As such, `ToEnvString` shouldn't be implemented directly:
/// [`Display`] should be implemented instead, and you get the `ToEnvString`
/// implementation for free.
///
/// [`Display`]: std::fmt::Display
pub trait ToEnvString {
/// Converts the giving value to a `EnvString`.
///
/// # Examples
///
/// basic usage
///
/// ```rust
/// # use itconfig::{EnvString, ToEnvString};
/// let i = 5;
/// let five = EnvString::from("5");
/// assert_eq!(five, i.to_env_string());
/// ```
fn to_env_string(&self) -> EnvString;
}
/// Simple and safe type conversions that may fail in a controlled way under
/// some circumstances.
///
/// This trait automatically implemented for all standard primitives. If you
/// want to use your custom type in the library you need to implement
/// `ToEnvString` and `FromEnvString` manually.
pub trait FromEnvString: Sized {
/// The type returned in the event of a conversion error.
type Err;
/// Performs the conversion.
fn from_env_string(s: &EnvString) -> Result<Self, Self::Err>;
}
impl<T> ToEnvString for T
where
T: std::fmt::Display,
{
#[inline]
fn to_env_string(&self) -> EnvString {
EnvString(self.to_string())
}
}
#[doc(hidden)]
macro_rules! from_env_string_numbers_impl {
($($ty:ty => $feature:expr),+) => {
$(
#[cfg(feature = $feature)]
impl FromEnvString for $ty {
type Err = <$ty as std::str::FromStr>::Err;
#[inline]
fn from_env_string(s: &EnvString) -> Result<Self, Self::Err> {
s.0.parse::<Self>()
}
}
)+
};
}
from_env_string_numbers_impl![
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"
];
#[cfg(feature = "bool")]
impl FromEnvString for bool {
type Err = ();
fn from_env_string(s: &EnvString) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"true" | "t" | "yes" | "y" | "on" | "1" => Ok(true),
_ => Ok(false),
}
}
}
impl FromEnvString for String {
type Err = ();
fn from_env_string(s: &EnvString) -> Result<Self, Self::Err> {
Ok(s.0.clone())
}
}
impl FromEnvString for &'static str {
type Err = ();
fn from_env_string(s: &EnvString) -> Result<Self, Self::Err> {
Ok(Box::leak(s.0.clone().into_boxed_str()))
}
}
//===========================================================================//
// DEPRECATED //
//===========================================================================//
/// Error type for json array implementation
#[cfg(feature = "json_array")]
#[derive(Debug)]
#[deprecated(since = "1.1.0")]
pub enum ArrayEnvError {
/// Invalid type.
InvalidType,
/// Failed to parse environment variable
FailedToParse,
}
#[cfg(feature = "json_array")]
#[allow(deprecated)]
impl<T> FromEnvString for Vec<T>
where
T: FromEnvString,
{
type Err = ArrayEnvError;
fn from_env_string(s: &EnvString) -> Result<Self, Self::Err> {
serde_json::from_str::<Vec<isize>>(s.trim())
.map(|vec| vec.iter().map(|v| v.to_string()).collect::<Vec<String>>())
.or_else(|_| serde_json::from_str::<Vec<String>>(s.trim()))
.map_err(|_| ArrayEnvError::InvalidType)
.and_then(|vec| {
vec.iter()
.map(|v| {
FromEnvString::from_env_string(&v.to_env_string())
.map_err(|_| ArrayEnvError::FailedToParse)
})
.collect::<Result<Vec<T>, _>>()
})
}
}

View File

@ -1,27 +0,0 @@
use std::error;
use std::fmt;
/// The error type for operations interacting with environment variables
#[derive(Debug, PartialEq)]
pub enum EnvError {
/// The specified environment variable was not present in the current process's environment.
MissingVariable(String),
/// Failed to parse the specified environment variable.
FailedToParse(String),
}
impl fmt::Display for EnvError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self {
EnvError::MissingVariable(env_name) => {
write!(f, r#"Environment variable "{}" is missing"#, env_name)
}
EnvError::FailedToParse(env_name) => {
write!(f, r#"Failed to parse environment variable "{}""#, env_name)
}
}
}
}
impl error::Error for EnvError {}

View File

@ -1,240 +0,0 @@
use crate::envstr::*;
use crate::error::*;
use crate::utils::*;
use std::env;
/// Same as get_env but returns Option enum instead Result
///
/// Example
/// -------
///
/// ```rust
/// # extern crate itconfig;
/// # use itconfig::maybe_get_env;
/// use std::env;
///
/// fn main () {
/// env::set_var("HOST", "https://example.com");
///
/// let host: Option<&'static str> = maybe_get_env("HOST");
/// let not_existence_host: Option<&'static str> = maybe_get_env("NOT_EXISTENCE_HOST");
///
/// assert_eq!(host, Some("https://example.com"));
/// assert_eq!(not_existence_host, None);
/// }
/// ```
///
pub fn maybe_get_env<T>(env_name: &str) -> Option<T>
where
T: FromEnvString,
{
get_env(env_name).ok()
}
/// This function is similar as `get_env`, but it unwraps result with panic on error.
///
/// Panics
/// ------
/// Application will panic if environment variable is missing or cannot parse variable to
/// expected type
///
pub fn get_env_or_panic<T>(env_name: &str) -> T
where
T: FromEnvString,
{
get_env(env_name).unwrap_or_else(make_panic)
}
/// Try to read environment variable and parse to expected type. You may to put to argument
/// any type with `FromEnvString` trait.
///
/// Example
/// -------
///
/// ```rust
/// # extern crate itconfig;
/// # use itconfig::get_env;
/// use std::env;
///
/// fn main () {
/// env::set_var("DEBUG", "true");
///
/// let result: bool = get_env("DEBUG").unwrap();
///
/// assert_eq!(result, true);
/// }
/// ```
///
pub fn get_env<T>(env_name: &str) -> Result<T, EnvError>
where
T: FromEnvString,
{
get_env_or(env_name, |_| {
Err(EnvError::MissingVariable(env_name.to_string()))
})
}
/// This function is similar as `get_env_or_panic`, but you can pass default value for
/// environment variable with `ToEnvString` trait.
///
/// Panics
/// ------
/// Application will panic if cannot parse variable to expected type
///
/// Example
/// -------
///
/// ```rust
/// # extern crate itconfig;
/// # use itconfig::get_env_or_default;
/// use std::env;
///
/// fn main () {
/// let result: bool = get_env_or_default("TESTING", "true");
/// assert_eq!(result, true);
/// }
/// ```
///
pub fn get_env_or_default<T, D>(env_name: &str, default: D) -> T
where
T: FromEnvString,
D: ToEnvString,
{
get_env_or(env_name, |_| Ok(default.to_env_string())).unwrap_or_else(make_panic)
}
/// This function is similar as `get_env_or_default`, but the default value will be set to environment
/// variable, if env variable is missed.
///
/// Panics
/// ------
/// Application will panic if cannot parse variable to expected type
///
/// Example
/// -------
///
/// ```rust
/// # extern crate itconfig;
/// # use itconfig::get_env_or_set_default;
/// use std::env;
///
/// fn main () {
/// let result: bool = get_env_or_set_default("TESTING", "true");
/// assert_eq!(result, true);
///
/// let var = env::var("TESTING").unwrap();
/// assert_eq!(var, "true");
/// }
/// ```
///
pub fn get_env_or_set_default<T, D>(env_name: &str, default: D) -> T
where
T: FromEnvString,
D: ToEnvString,
{
get_env_or(env_name, |_| {
let val = default.to_env_string();
env::set_var(env_name, val.as_str());
Ok(val)
})
.unwrap_or_else(make_panic)
}
/// This function returns env variable as `EnvString` structure. You can pass callback for custom
/// default expression. Callback should return `EnvString` value or `EnvError`
pub fn get_env_or<T, F>(env_name: &str, cb: F) -> Result<T, EnvError>
where
T: FromEnvString,
F: FnOnce(env::VarError) -> Result<EnvString, EnvError>,
{
env::var(env_name)
.map(|s| s.to_env_string())
.or_else(cb)
.and_then(|env_str| parse_env_variable(env_name, env_str))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic(expected = "Environment variable \"TEST_CASE_1\" is missing")]
fn get_missing_env() {
get_env_or_panic::<String>("TEST_CASE_1");
}
#[test]
#[should_panic(expected = "Failed to parse environment variable \"TEST_CASE_2\"")]
fn get_env_with_invalid_value() {
let env_name = "TEST_CASE_2";
env::set_var(&env_name, "30r");
get_env_or_panic::<u32>(env_name);
}
#[test]
fn get_result_of_missing_env() {
let env_name = String::from("TEST_CASE_3");
let env_val = get_env::<String>(&env_name);
assert_eq!(env_val, Err(EnvError::MissingVariable(env_name)))
}
#[test]
fn get_result_of_env_with_invalid_value() {
let env_name = String::from("TEST_CASE_4");
env::set_var(&env_name, "30r");
let env_val = get_env::<u32>(&env_name);
assert_eq!(env_val, Err(EnvError::FailedToParse(env_name)))
}
#[test]
fn get_result_of_env_successfully() {
env::set_var("TEST_CASE_5", "30");
let env_var = get_env("TEST_CASE_5");
assert_eq!(env_var, Ok(30));
}
#[test]
fn get_missing_env_with_default_value() {
let flag: bool = get_env_or_default("TEST_CASE_6", "true");
assert!(flag);
}
#[test]
#[should_panic(expected = "Failed to parse environment variable \"TEST_CASE_7\"")]
fn get_invalid_env_with_default_value() {
env::set_var("TEST_CASE_7", "30r");
get_env_or_default::<u32, _>("TEST_CASE_7", 30);
}
#[test]
#[should_panic(expected = "Failed to parse environment variable \"TEST_CASE_8\"")]
fn get_env_with_invalid_default_value() {
get_env_or_default::<u32, _>("TEST_CASE_8", "30r");
}
#[test]
fn get_env_with_default_successfully() {
env::set_var("TEST_CASE_9", "10");
let env_val: u32 = get_env_or_default("TEST_CASE_9", 30);
assert_eq!(env_val, 10)
}
#[test]
fn get_missing_env_with_set_default_value() {
let flag: bool = get_env_or_set_default("TEST_CASE_10", "true");
assert!(flag);
let env_var = env::var("TEST_CASE_10");
assert_eq!(env_var, Ok(String::from("true")))
}
#[test]
fn get_optional_env() {
env::set_var("TEST_CASE_11", "something");
let something: Option<&'static str> = maybe_get_env("TEST_CASE_11");
assert_eq!(something, Some("something"));
let nothing: Option<&'static str> = maybe_get_env("TEST_CASE_11_NONE");
assert_eq!(nothing, None);
}
}

View File

@ -1,304 +0,0 @@
use crate::envstr::*;
use crate::error::*;
use crate::utils::*;
use std::env;
/// Same as get_vec_env but returns Option enum instead Result
///
/// Example
/// -------
///
/// ```rust
/// # extern crate itconfig;
/// # use itconfig::*;
/// use std::env;
///
/// #[derive(Debug, PartialEq, Eq)]
/// enum PaymentPlatform {
/// PayPal,
/// Stripe,
/// SomethingElse,
/// }
///
/// impl FromEnvString for PaymentPlatform {
/// type Err = &'static str;
///
/// fn from_env_string(envstr: &EnvString) -> Result<Self, Self::Err> {
/// match envstr.to_lowercase().as_str() {
/// "paypal" => Ok(Self::PayPal),
/// "stripe" => Ok(Self::Stripe),
/// "smth" => Ok(Self::SomethingElse),
/// _ => Err("Unsupported payment platform"),
/// }
/// }
/// }
///
///
/// fn main () {
/// env::set_var("PAYMENT_PLATFORMS", "paypal,stripe");
///
/// let payment_platforms: Option<Vec<PaymentPlatform>> = maybe_get_vec_env("PAYMENT_PLATFORMS", ",");
/// assert_eq!(
/// payment_platforms,
/// Some(vec![PaymentPlatform::PayPal, PaymentPlatform::Stripe])
/// );
/// }
/// ```
///
pub fn maybe_get_vec_env<T>(env_name: &str, sep: &'static str) -> Option<Vec<T>>
where
T: FromEnvString,
{
get_vec_env(env_name, sep).ok()
}
/// This function is similar as `get_vec_env`, but it unwraps result with panic on error.
///
/// Panics
/// ------
/// Application will panic if environment variable is missing or cannot parse variable to
/// expected type
///
pub fn get_vec_env_or_panic<T>(env_name: &str, sep: &'static str) -> Vec<T>
where
T: FromEnvString,
{
get_vec_env(env_name, sep).unwrap_or_else(make_panic)
}
/// Try to read environment variable, split by separator and parse each item to expected
/// type.
///
/// Example
/// -------
///
/// ```rust
/// # extern crate itconfig;
/// # use itconfig::get_vec_env;
/// use std::env;
///
/// fn main () {
/// env::set_var("DEBUG", "true");
///
/// let result: Vec<bool> = get_vec_env("DEBUG", ",").unwrap();
///
/// assert_eq!(result, vec![true]);
/// }
/// ```
///
pub fn get_vec_env<T>(env_name: &str, sep: &str) -> Result<Vec<T>, EnvError>
where
T: FromEnvString,
{
get_vec_env_or(env_name, sep, |_| {
Err(EnvError::MissingVariable(env_name.to_string()))
})
}
/// This function is similar as `get_vec_env_or_panic`, but you can pass default value for
/// environment variable with `ToEnvString` trait.
///
/// Panics
/// ------
/// Application will panic if cannot parse variable to expected type
///
/// Example
/// -------
///
/// ```rust
/// # extern crate itconfig;
/// # use itconfig::get_vec_env_or_default;
/// use std::env;
///
/// fn main () {
/// let result: Vec<bool> = get_vec_env_or_default("TESTING", ",", vec!["true"]);
/// assert_eq!(result, vec![true]);
/// }
/// ```
///
pub fn get_vec_env_or_default<T, D>(env_name: &str, sep: &str, default: Vec<D>) -> Vec<T>
where
T: FromEnvString,
D: ToEnvString,
{
get_vec_env_or(env_name, sep, |_| Ok(vec_to_env_strings(default))).unwrap_or_else(make_panic)
}
/// This function is similar as `get_vec_env_or_default`, but the default value will be set to environment
/// variable, if env variable is missed.
///
/// Panics
/// ------
/// Application will panic if cannot parse variable to expected type
///
/// Example
/// -------
///
/// ```rust
/// # extern crate itconfig;
/// # use itconfig::get_vec_env_or_set_default;
/// use std::env;
///
/// fn main () {
/// let result: Vec<bool> = get_vec_env_or_set_default("TESTING", ",", vec!["true"]);
/// assert_eq!(result, vec![true]);
///
/// let var = env::var("TESTING").unwrap();
/// assert_eq!(var, "true");
/// }
/// ```
///
pub fn get_vec_env_or_set_default<T, D>(env_name: &str, sep: &str, default: Vec<D>) -> Vec<T>
where
T: FromEnvString,
D: ToEnvString,
{
get_vec_env_or(env_name, sep, |_| {
let default_env_strings = vec_to_env_strings(default);
let env_val = join(&default_env_strings, sep);
env::set_var(env_name, env_val.as_str());
Ok(default_env_strings)
})
.unwrap_or_else(make_panic)
}
/// This function returns env variable as `EnvString` structure. You can pass callback for custom
/// default expression. Callback should return `EnvString` value or `EnvError`
pub fn get_vec_env_or<T, F>(env_name: &str, sep: &str, cb: F) -> Result<Vec<T>, EnvError>
where
T: FromEnvString,
F: FnOnce(env::VarError) -> Result<Vec<EnvString>, EnvError>,
{
env::var(env_name)
.map(|s| {
s.split(sep)
.into_iter()
.map(|item| item.to_env_string())
.collect()
})
.or_else(cb)
.and_then(|items| {
items
.into_iter()
.map(|env_str| parse_env_variable(env_name, env_str))
.collect()
})
}
#[cfg(test)]
mod tests {
use super::*;
const SEP: &str = ",";
#[test]
#[should_panic(expected = "Environment variable \"TEST_CASE_VEC_1\" is missing")]
fn get_missing_vec_env() {
let _: Vec<&'static str> = get_vec_env_or_panic("TEST_CASE_VEC_1", SEP);
}
#[test]
#[should_panic(expected = "Failed to parse environment variable \"TEST_CASE_VEC_2\"")]
fn get_vec_env_with_invalid_value() {
let env_name = "TEST_CASE_VEC_2";
env::set_var(&env_name, "30r");
let _: Vec<u32> = get_vec_env_or_panic(env_name, SEP);
}
#[test]
fn get_result_of_missing_vec_env() {
let env_name = String::from("TEST_CASE_VEC_3");
let env_val = get_vec_env::<String>(&env_name, SEP);
assert_eq!(env_val, Err(EnvError::MissingVariable(env_name)))
}
#[test]
fn get_result_of_vec_env_with_invalid_value() {
let env_name = String::from("TEST_CASE_VEC_4");
env::set_var(&env_name, "30r");
let env_val = get_vec_env::<u32>(&env_name, SEP);
assert_eq!(env_val, Err(EnvError::FailedToParse(env_name)))
}
#[test]
fn get_result_of_vec_env_successfully() {
env::set_var("TEST_CASE_VEC_5", "30");
let env_var = get_vec_env("TEST_CASE_VEC_5", SEP);
assert_eq!(env_var, Ok(vec![30]));
}
#[test]
fn get_missing_vec_env_with_default_value() {
let flag: Vec<bool> = get_vec_env_or_default("TEST_CASE_VEC_6", SEP, vec!["true"]);
assert_eq!(flag, vec![true]);
}
#[test]
#[should_panic(expected = "Failed to parse environment variable \"TEST_CASE_VEC_7\"")]
fn get_invalid_vec_env_with_default_value() {
env::set_var("TEST_CASE_VEC_7", "30r");
get_vec_env_or_default::<u32, _>("TEST_CASE_VEC_7", SEP, vec![30]);
}
#[test]
#[should_panic(expected = "Failed to parse environment variable \"TEST_CASE_VEC_8\"")]
fn get_vec_env_with_invalid_default_value() {
get_vec_env_or_default::<u32, _>("TEST_CASE_VEC_8", SEP, vec!["30r"]);
}
#[test]
fn get_vec_env_with_default_successfully() {
env::set_var("TEST_CASE_VEC_9", "10");
let env_val: Vec<u32> = get_vec_env_or_default("TEST_CASE_VEC_9", SEP, vec![30]);
assert_eq!(env_val, vec![10])
}
#[test]
fn get_missing_vec_env_with_set_default_value() {
let flag: Vec<bool> = get_vec_env_or_set_default("TEST_CASE_VEC_10", SEP, vec!["true"]);
assert_eq!(flag, vec![true]);
let env_var = env::var("TEST_CASE_VEC_10");
assert_eq!(env_var, Ok(String::from("true")))
}
#[test]
fn get_optional_vec_env() {
env::set_var("TEST_CASE_VEC_11", "something");
let something: Option<Vec<&'static str>> = maybe_get_vec_env("TEST_CASE_VEC_11", SEP);
assert_eq!(something, Some(vec!["something"]));
let nothing: Option<Vec<&'static str>> = maybe_get_vec_env("TEST_CASE_VEC_11_NONE", SEP);
assert_eq!(nothing, None);
}
#[test]
fn get_custom_type_from_vec_env() {
#[derive(Debug, PartialEq, Eq)]
enum PaymentPlatform {
PayPal,
Stripe,
SomethingElse,
}
impl FromEnvString for PaymentPlatform {
type Err = &'static str;
fn from_env_string(envstr: &EnvString) -> Result<Self, Self::Err> {
match envstr.to_lowercase().as_str() {
"paypal" => Ok(Self::PayPal),
"stripe" => Ok(Self::Stripe),
"smth" => Ok(Self::SomethingElse),
_ => Err("Unsupported payment platform"),
}
}
}
env::set_var("TEST_CASE_VEC_12", "paypal,stripe");
let something: Option<Vec<PaymentPlatform>> = maybe_get_vec_env("TEST_CASE_VEC_12", SEP);
assert_eq!(
something,
Some(vec![PaymentPlatform::PayPal, PaymentPlatform::Stripe])
);
}
}

View File

@ -1,33 +0,0 @@
use crate::{EnvError, EnvString, FromEnvString, ToEnvString};
pub(crate) fn parse_env_variable<T>(env_name: &str, env_str: EnvString) -> Result<T, EnvError>
where
T: FromEnvString,
{
FromEnvString::from_env_string(&env_str)
.map_err(|_| EnvError::FailedToParse(env_name.to_string()))
}
pub(crate) fn make_panic<T>(e: EnvError) -> T {
panic!("{}", e)
}
pub(crate) fn join(env_strings: &[EnvString], sep: &str) -> String {
env_strings
.iter()
.enumerate()
.fold(String::new(), |mut res, (i, item)| {
if i > 0 {
res.push_str(sep);
}
res.push_str(item);
res
})
}
pub(crate) fn vec_to_env_strings<T>(values: Vec<T>) -> Vec<EnvString>
where
T: ToEnvString,
{
values.into_iter().map(EnvString::from).collect()
}

238
src/core.rs Normal file
View File

@ -0,0 +1,238 @@
use crate::error::Error;
use estring::EString;
use std::convert::TryFrom;
pub fn get_or_set_default<R>(env_name: &str, default: R) -> Result<R, Error>
where
R: TryFrom<EString> + std::fmt::Display,
{
get::<R>(env_name).or_else(|err| match err {
Error::NotPresent => sset(env_name, &default).parse().map_err(Error::from),
_ => Err(err),
})
}
pub fn get<R>(env_name: &str) -> Result<R, Error>
where
R: TryFrom<EString>,
{
sget(env_name).and_then(|v| v.parse().map_err(Error::from))
}
pub fn sget(env_name: &str) -> Result<EString, Error> {
std::env::var(env_name)
.map_err(Error::from)
.map(EString::from)
}
pub fn sset<V>(env_name: &str, value: V) -> EString
where
V: std::fmt::Display,
{
let val = value.to_string();
std::env::set_var(env_name, &val);
val.into()
}
#[cfg(test)]
mod tests {
use super::*;
struct TestCase<const N: u8>;
impl<const N: u8> std::fmt::Display for TestCase<N> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "test_case_{}", N)
}
}
#[test]
fn should_add_env_variable_to_process() {
let en = TestCase::<0>.to_string();
sset(&en, "hello");
match std::env::var(&en) {
Ok(var) => assert_eq!(&var, "hello"),
_ => unreachable!(),
}
}
#[test]
fn should_return_variable() {
let en = TestCase::<1>.to_string();
std::env::set_var(&en, "hello");
match get::<&str>(&en) {
Ok(res) => assert_eq!(res, "hello"),
_ => unreachable!(),
};
}
#[test]
fn should_throw_no_present_error() {
let en = TestCase::<2>.to_string();
match get::<&str>(&en) {
Err(Error::NotPresent) => {}
_ => unreachable!(),
};
}
#[test]
fn should_set_default_if_var_is_no_present() {
let en = TestCase::<3>.to_string();
let orig = "hello";
match get_or_set_default(&en, orig) {
Ok(res) => {
assert_eq!(res, orig);
assert_eq!(std::env::var(&en).unwrap(), orig);
}
_ => unreachable!(),
};
}
#[cfg(feature = "number")]
mod numbers {
use super::*;
#[test]
fn should_return_parsed_num() {
let en = TestCase::<4>.to_string();
std::env::set_var(&en, "-10");
match get::<i32>(&en) {
Ok(res) => assert_eq!(res, -10),
_ => unreachable!(),
};
}
#[test]
fn should_throw_parse_error() {
let en = TestCase::<5>.to_string();
std::env::set_var(&en, "-10");
match get::<u32>(&en) {
Err(Error::Parse(orig)) => {
assert_eq!(orig, String::from("-10"))
}
_ => unreachable!(),
};
}
#[test]
fn should_set_default_num_if_var_is_no_present() {
let en = TestCase::<6>.to_string();
let orig = 10;
match get_or_set_default(&en, orig) {
Ok(res) => {
assert_eq!(res, orig);
assert_eq!(std::env::var(&en).unwrap(), "10");
}
_ => unreachable!(),
};
}
}
#[cfg(feature = "bool")]
mod boolean {
use super::*;
#[test]
fn should_parse_bool_variable() {
let en = TestCase::<7>.to_string();
[
("1", true),
("y", true),
("yes", true),
("true", true),
("t", true),
("on", true),
("false", false),
("f", false),
("0", false),
]
.iter()
.for_each(|(val, expected)| {
let mut en = en.clone();
en.push_str(val.as_ref());
std::env::set_var(&en, val);
match get::<bool>(&en) {
Ok(res) => assert_eq!(res, *expected),
_ => unreachable!(),
};
})
}
}
#[cfg(feature = "vec")]
mod vector {
use super::*;
use crate::estr::{CommaVec, SemiVec, SepVec};
#[test]
fn should_return_var_as_vector() {
let en = TestCase::<8>.to_string();
std::env::set_var(&en, "1,2,3,4,5");
match get::<CommaVec<i32>>(&en) {
Ok(res) => assert_eq!(*res, vec![1, 2, 3, 4, 5]),
_ => unreachable!(),
};
}
#[test]
fn should_trim_identations_before_parsing() {
let en = TestCase::<9>.to_string();
let input = "
1 , 2, 3,
4,5";
std::env::set_var(&en, input);
match get::<CommaVec<i32>>(&en) {
Ok(res) => assert_eq!(*res, vec![1, 2, 3, 4, 5]),
_ => unreachable!(),
};
}
#[test]
fn should_return_vector_of_vectors() {
let en = TestCase::<10>.to_string();
std::env::set_var(&en, "1,2; 3,4,5; 6,7");
match get::<SemiVec<CommaVec<i32>>>(&en) {
Ok(res) => assert_eq!(
res,
SemiVec::from(vec![
CommaVec::from(vec![1, 2]),
CommaVec::from(vec![3, 4, 5]),
CommaVec::from(vec![6, 7])
])
),
_ => unreachable!(),
};
}
#[test]
fn should_throw_parse_vec_error() {
let en = TestCase::<11>.to_string();
std::env::set_var(&en, "1,2,3,4,5");
match get::<SepVec<i32, '+'>>(&en) {
Err(Error::Parse(orig)) => {
assert_eq!(orig, String::from("1,2,3,4,5"))
}
_ => unreachable!(),
};
}
#[test]
fn should_set_default_vector_if_var_is_no_present() {
let en = TestCase::<12>.to_string();
let orig = CommaVec::from(vec![1, 2, 3, 4]);
match get_or_set_default(&en, orig.clone()) {
Ok(res) => {
assert_eq!(res, orig);
assert_eq!(std::env::var(&en).unwrap(), "1,2,3,4");
}
_ => unreachable!(),
};
}
}
}

53
src/error.rs Normal file
View File

@ -0,0 +1,53 @@
use std::env::VarError;
use std::error;
use std::ffi::OsString;
use std::fmt;
/// The error type for operations interacting with environment variables
#[derive(Debug)]
pub enum Error {
/// The specified environment variable was not present in the current process's environment.
NotPresent,
/// Failed to parse the specified environment variable.
Parse(String),
/// The specified environment variable was found, but it did not contain
/// valid unicode data. The found data is returned as a payload of this
/// variant.
Invalid(OsString),
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use Error::*;
match &self {
NotPresent => f.write_str("The specified env variable was not present"),
Invalid(inner) => write!(
f,
"The specified env variable was found, but it did not valid: '{:?}'",
inner,
),
Parse(env_name) => {
write!(f, r#"Failed to parse environment variable "{}""#, env_name)
}
}
}
}
impl error::Error for Error {}
impl From<VarError> for Error {
fn from(err: VarError) -> Self {
match err {
VarError::NotPresent => Error::NotPresent,
VarError::NotUnicode(inner) => Error::Invalid(inner),
}
}
}
impl From<estring::ParseError> for Error {
fn from(err: estring::ParseError) -> Self {
Error::Parse(err.clone())
}
}

6
src/estr.rs Normal file
View File

@ -0,0 +1,6 @@
#[cfg(feature = "vec")]
pub mod vec;
#[cfg(feature = "vec")]
pub use vec::{CommaVec, SemiVec};
pub use estring::core::*;

7
src/estr/vec.rs Normal file
View File

@ -0,0 +1,7 @@
use estring::SepVec;
const COMMA: char = ',';
const SEMI: char = ';';
pub type CommaVec<T> = SepVec<T, COMMA>;
pub type SemiVec<T> = SepVec<T, SEMI>;

View File

@ -1,4 +1,4 @@
//! # itconfig
//! # enve
//!
//! Simple configuration with macro for rust application.
//!
@ -16,7 +16,7 @@
//!
//! These macros require a Rust compiler version 1.31 or newer.
//!
//! Add `itconfig = { version = "1.0", features = ["macro"] }` as a dependency in `Cargo.toml`.
//! Add `enve = { version = "1.0", features = ["macro"] }` as a dependency in `Cargo.toml`.
//!
//!
//! `Cargo.toml` example:
@ -28,14 +28,14 @@
//! authors = ["Me <user@rust-lang.org>"]
//!
//! [dependencies]
//! itconfig = { version = "1.0", features = ["macro"] }
//! enve = { version = "1.0", features = ["macro"] }
//! ```
//!
//!
//! ## Basic usage
//!
//! ```rust
//! use itconfig::config;
//! use enve::config;
//! use std::env;
//! //use dotenv::dotenv;
//!
@ -90,7 +90,7 @@
//! Macro is an optional feature, disabled by default. You can use this library without macro.
//!
//! ```rust
//! use itconfig::*;
//! use enve::*;
//! use std::env;
//! // use dotenv::dotenv;
//!
@ -142,24 +142,22 @@
unused_imports,
unused_qualifications
)]
#![warn(missing_docs)]
// Clippy lints
#![deny(clippy::all)]
#![allow(clippy::needless_doctest_main)]
/////////////////////////////////////////////////////////////////////////////
mod envstr;
mod core;
mod error;
mod get_env;
mod get_vec_env;
pub(crate) mod utils;
pub mod estr;
pub use self::envstr::*;
pub use self::error::*;
pub use self::get_env::*;
pub use self::get_vec_env::*;
pub use self::core::*;
pub use self::core::{get, get_or_set_default, sget, sset};
pub use self::error::Error;
#[cfg(feature = "macro")]
extern crate itconfig_macro;
extern crate enve_mod;
#[cfg(feature = "macro")]
pub use itconfig_macro::*;
pub use enve_mod::*;