refac: improve core design

- removed all functions, features and examples
- recreate core types
- recreate get_env and get_env_or_set_default
This commit is contained in:
Dmitriy Pleshevskiy 2022-07-22 13:05:54 +03:00
parent cf03c58364
commit 21e6aba270
Signed by: pleshevskiy
GPG key ID: 1B59187B161C0215
15 changed files with 301 additions and 2219 deletions

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

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

1336
Cargo.lock generated

File diff suppressed because it is too large Load diff

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

@ -15,52 +15,19 @@ readme = "../README.md"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features] [features]
default = ["primitives"] default = []
macro = ["itconfig-macro"] macro = ["itconfig-macro"]
primitives = ["numbers", "bool"] number = []
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 = [] bool = []
vec = []
# deprecated since 1.1
json_array = ["serde_json"]
[dependencies] [dependencies]
serde_json = { version = "1", optional = true }
itconfig-macro = { version = "1.1", path = "../itconfig-macro", optional = true } itconfig-macro = { version = "1.1", path = "../itconfig-macro", optional = true }
[dev-dependencies] [dev-dependencies]
lazy_static = "1.4.0" 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] [badges]
maintenance = { status = "passively-maintained" } maintenance = { status = "passively-maintained" }
@ -69,13 +36,3 @@ maintenance = { status = "passively-maintained" }
[package.metadata.docs.rs] [package.metadata.docs.rs]
all-features = true all-features = true
[[example]]
name = "hyper"
path = "../examples/hyper.rs"
required-features = ["macro"]
[[example]]
name = "rocket"
path = "../examples/rocket.rs"
required-features = ["macro"]

56
itconfig/src/core.rs Normal file
View file

@ -0,0 +1,56 @@
#[cfg(any(feature = "number", feature = "bool"))]
pub mod prim;
#[cfg(any(feature = "number", feature = "bool"))]
pub use prim::*;
#[cfg(feature = "vec")]
pub mod vec;
#[cfg(feature = "vec")]
pub use vec::*;
use std::convert::{Infallible, TryFrom};
/// 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 EString(String);
impl<T> From<T> for EString
where
T: std::fmt::Display,
{
#[inline]
fn from(val: T) -> Self {
Self(val.to_string())
}
}
impl std::ops::Deref for EString {
type Target = String;
#[inline]
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl TryFrom<EString> for String {
type Error = Infallible;
#[inline]
fn try_from(s: EString) -> Result<Self, Self::Error> {
Ok(s.0)
}
}
impl TryFrom<EString> for &'static str {
type Error = Infallible;
#[inline]
fn try_from(s: EString) -> Result<Self, Self::Error> {
Ok(Box::leak(s.0.into_boxed_str()))
}
}

39
itconfig/src/core/prim.rs Normal file
View file

@ -0,0 +1,39 @@
use crate::core::EString;
use std::convert::{Infallible, TryFrom};
#[doc(hidden)]
macro_rules! from_env_string_numbers_impl {
($($ty:ty),+$(,)?) => {
$(
#[cfg(feature = "number")]
impl TryFrom<EString> for $ty {
type Error = <$ty as std::str::FromStr>::Err;
#[inline]
fn try_from(s: EString) -> Result<Self, Self::Error> {
s.0.parse::<Self>()
}
}
)+
};
}
#[rustfmt::skip]
from_env_string_numbers_impl![
i8, i16, i32, i64, i128, isize,
u8, u16, u32, u64, u128, usize,
f32, f64
];
#[cfg(feature = "bool")]
impl TryFrom<EString> for bool {
type Error = Infallible;
#[inline]
fn try_from(s: EString) -> Result<Self, Self::Error> {
Ok(matches!(
s.to_lowercase().as_str(),
"true" | "t" | "yes" | "y" | "on" | "1"
))
}
}

59
itconfig/src/core/vec.rs Normal file
View file

@ -0,0 +1,59 @@
use crate::core::EnvString;
use std::convert::TryFrom;
use std::fmt::Write;
pub const COMMA: char = ',';
pub const SEMI: char = ';';
pub type CommaVec<T> = SepVec<T, COMMA>;
pub type SemiVec<T> = SepVec<T, SEMI>;
#[derive(Debug, PartialEq, Clone)]
pub struct SepVec<T, const SEP: char>(pub Vec<T>);
impl<T, const SEP: char> std::ops::Deref for SepVec<T, SEP> {
type Target = Vec<T>;
#[inline]
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<T, const SEP: char> From<Vec<T>> for SepVec<T, SEP> {
#[inline]
fn from(vec: Vec<T>) -> Self {
Self(vec)
}
}
impl<T, const SEP: char> std::fmt::Display for SepVec<T, SEP>
where
T: std::fmt::Display,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.iter().enumerate().try_for_each(|(i, part)| {
if i != 0 {
f.write_char(SEP)?;
}
f.write_str(&part.to_string())
})
}
}
impl<T, const SEP: char> TryFrom<EnvString> for SepVec<T, SEP>
where
T: TryFrom<EnvString> + std::fmt::Display,
{
type Error = T::Error;
fn try_from(value: EnvString) -> Result<Self, Self::Error> {
let inner = value
.split(SEP)
.map(EnvString::from)
.map(T::try_from)
.collect::<Result<Vec<_>, _>>()?;
Ok(Self(inner))
}
}

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

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

@ -137,7 +137,7 @@
#![forbid(non_ascii_idents)] #![forbid(non_ascii_idents)]
#![deny( #![deny(
missing_debug_implementations, missing_debug_implementations,
missing_docs, // missing_docs,
unstable_features, unstable_features,
unused_imports, unused_imports,
unused_qualifications unused_qualifications
@ -148,16 +148,13 @@
///////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////
mod envstr; pub mod core;
mod error; mod error;
mod get_env; mod utils;
mod get_vec_env;
pub(crate) mod utils;
pub use self::envstr::*; pub use self::core::*;
pub use self::error::*; pub use self::error::*;
pub use self::get_env::*; pub use self::utils::*;
pub use self::get_vec_env::*;
#[cfg(feature = "macro")] #[cfg(feature = "macro")]
extern crate itconfig_macro; extern crate itconfig_macro;

View file

@ -1,33 +1,116 @@
use crate::{EnvError, EnvString, FromEnvString, ToEnvString}; use crate::core::EString;
use crate::error::Error;
use std::convert::{TryFrom, TryInto};
use std::env;
pub(crate) fn parse_env_variable<T>(env_name: &str, env_str: EnvString) -> Result<T, EnvError> pub fn get_env_or_set_default<R>(env_name: &str, default: R) -> Result<R, Error>
where where
T: FromEnvString, R: TryFrom<EString> + std::fmt::Display,
{ {
FromEnvString::from_env_string(&env_str) get_env::<R>(env_name).or_else(|err| match err {
.map_err(|_| EnvError::FailedToParse(env_name.to_string())) Error::NotPresent => {
let val = default.to_string();
env::set_var(env_name, &val);
EString::from(val)
.try_into()
.map_err(|_| Error::Parse(default.to_string()))
} }
_ => Err(err),
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> pub fn get_env<R>(env_name: &str) -> Result<R, Error>
where where
T: ToEnvString, R: TryFrom<EString>,
{ {
values.into_iter().map(EnvString::from).collect() env::var(env_name)
.map_err(From::from)
.map(EString::from)
.and_then(|val| {
val.clone()
.try_into()
.map_err(|_| Error::Parse(val.to_string()))
})
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(feature = "vec")]
use crate::core::vec::{CommaVec, SepVec};
#[test]
fn should_return_variable() {
env::set_var("get_env_1", "hello");
match get_env::<&str>("get_env_1") {
Ok(res) => assert_eq!(res, "hello"),
_ => unreachable!(),
};
}
#[test]
fn should_throw_no_present_error() {
match get_env::<&str>("get_env_2") {
Err(Error::NotPresent) => {}
_ => unreachable!(),
};
}
#[test]
fn should_throw_parse_error() {
env::set_var("get_env_3", "-10");
match get_env::<u32>("get_env_3") {
Err(Error::Parse(orig)) => {
assert_eq!(orig, String::from("-10"))
}
_ => unreachable!(),
};
}
#[test]
fn should_set_default_if_var_is_no_present() {
let orig = 10;
match get_env_or_set_default("get_env_4", orig) {
Ok(res) => {
assert_eq!(res, orig);
assert_eq!(env::var("get_env_4").unwrap(), "10");
}
_ => unreachable!(),
};
}
#[cfg(feature = "vec")]
#[test]
fn should_return_var_as_vector() {
env::set_var("get_env_5", "1,2,3,4,5");
match get_env::<CommaVec<i32>>("get_env_5") {
Ok(res) => assert_eq!(*res, vec![1, 2, 3, 4, 5]),
_ => unreachable!(),
};
}
#[cfg(feature = "vec")]
#[test]
fn should_throw_parse_vec_error() {
env::set_var("get_env_6", "1,2,3,4,5");
match get_env::<SepVec<i32, '+'>>("get_env_6") {
Err(Error::Parse(orig)) => {
assert_eq!(orig, String::from("1,2,3,4,5"))
}
_ => unreachable!(),
};
}
#[cfg(feature = "vec")]
#[test]
fn should_set_default_vector_if_var_is_no_present() {
let orig = CommaVec::from(vec![1, 2, 3, 4]);
match get_env_or_set_default("get_env_7", orig.clone()) {
Ok(res) => {
assert_eq!(res, orig);
assert_eq!(env::var("get_env_7").unwrap(), "1,2,3,4");
}
_ => unreachable!(),
};
}
} }