refac: move core to estring crate

This commit is contained in:
Dmitriy Pleshevskiy 2022-07-23 14:05:53 +03:00
parent a8c124aabc
commit 2171d6b6a7
Signed by: pleshevskiy
GPG key ID: 1B59187B161C0215
22 changed files with 846 additions and 401 deletions

7
Cargo.lock generated
View file

@ -1,7 +1,5 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "atty"
version = "0.2.14"
@ -181,6 +179,7 @@ version = "1.1.1"
dependencies = [
"criterion",
"enve_mod",
"estring",
"lazy_static",
]
@ -195,6 +194,10 @@ dependencies = [
"syn",
]
[[package]]
name = "estring"
version = "0.1.0"
[[package]]
name = "half"
version = "1.8.2"

View file

@ -1,5 +1,6 @@
[workspace]
members = [
"estring",
"enve_mod",
]
@ -21,13 +22,14 @@ readme = "../README.md"
[features]
default = []
number = []
bool = []
vec = []
number = ["estring/number"]
bool = ["estring/bool"]
vec = ["estring/vec"]
macro = ["enve_mod"]
[dependencies]
estring = { version = "0", path = "./estring" }
enve_mod = { version = "1.1", path = "./enve_mod", optional = true }
[dev-dependencies]

View file

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2019 pleshevskiy
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

28
estring/Cargo.toml Normal file
View file

@ -0,0 +1,28 @@
[package]
name = "estring"
description = "A simple way to parse a string using type annotations"
version = "0.1.0"
edition = "2018"
authors = ["Dmitriy Pleshevskiy <dmitriy@ideascup.me>"]
readme = "README.md"
repository = "https://github.com/pleshevskiy/itconfig-rs/tree/redesign/estring"
license = "MIT"
keywords = ["parsing", "type", "annotations", "customizable"]
categories = ["data-structures", "parsing"]
# rust-version = "1.51.0" # The first version of Cargo that supports this field is 1.56.0
[metadata]
msrv = "1.51.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
number = []
bool = []
vec = []
[dependencies]
[badges]
maintenance = { status = "actively-developed" }

21
estring/LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 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
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.

57
estring/README.md Normal file
View file

@ -0,0 +1,57 @@
# EString
A simple way to parse a string using type annotations.
This package was originally designed for [enve]
[enve]: https://github.com/pleshevskiy/itconfig-rs/tree/redesign
## Getting started
```rust
use estring::{SepVec, EString};
type PlusVec<T> = SepVec<T, '+'>;
type MulVec<T> = SepVec<T, '*'>;
fn main() -> Result<(), estring::ParseError> {
let res = EString::from("10+5*2+3")
.parse::<PlusVec<MulVec<f32>>>()?
.iter()
.map(|m| m.iter().product::<f32>())
.sum::<f32>();
assert_eq!(res, 23.0);
Ok(())
}
```
You can use custom types as annotations! Just implement `TryFrom<EString>`!
## Installation
**The MSRV is 1.51.0**
Add `estring = { version = "0.1", features = ["vec", "number"] }` as a
dependency in `Cargo.toml`.
`Cargo.toml` example:
```toml
[package]
name = "my-crate"
version = "0.1.0"
authors = ["Me <user@rust-lang.org>"]
[dependencies]
estring = { version = "0.1", features = ["vec", "number"] }
```
## License
**MIT**. See [LICENSE](./LICENSE) to see the full text.
## Contributors
[pleshevskiy](https://github.com/pleshevskiy) (Dmitriy Pleshevskiy) creator,
maintainer.

99
estring/src/core.rs Normal file
View file

@ -0,0 +1,99 @@
//! Contains the ``EString`` type, as well as the basic implementation of conversions to
//! string types
//!
#[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 crate::ParseError;
use std::convert::{Infallible, TryFrom};
/// Wrapper under String type.
#[derive(Debug, Default, PartialEq, Eq, Clone)]
pub struct EString(pub String);
impl EString {
/// Parses inner string by type annotations and returns result.
///
/// # Errors
///
/// Will return `Err` if estring cannot parse inner fragment
///
#[inline]
pub fn parse<T: TryFrom<EString>>(self) -> Result<T, ParseError> {
let orig = self.0.clone();
<T as TryFrom<EString>>::try_from(self).map_err(|_| ParseError(orig))
}
}
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()))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn should_deref_to_string() {
let estr = EString::from("hello");
assert_eq!(*estr, String::from("hello"));
}
#[test]
fn should_parse_into_itself() {
let estr = EString::from("hello");
match estr.parse::<EString>() {
Ok(res) => assert_eq!(res, EString::from("hello")),
_ => unreachable!(),
}
}
#[test]
fn should_parse_into_string() {
let estr = EString::from("hello");
match estr.parse::<String>() {
Ok(res) => assert_eq!(res, String::from("hello")),
_ => unreachable!(),
}
}
}

14
estring/src/core/prim.rs Normal file
View file

@ -0,0 +1,14 @@
//! Contains the implementations to primitive types (number, boolean)
//!
//! **NOTE**: Require the enabling of the same-name features
//!
#[cfg(feature = "bool")]
mod bool;
#[cfg(feature = "bool")]
pub use self::bool::*;
#[cfg(feature = "number")]
mod number;
#[cfg(feature = "number")]
pub use self::number::*;

View file

@ -0,0 +1,57 @@
use crate::core::EString;
use std::convert::TryFrom;
impl TryFrom<EString> for bool {
type Error = ();
#[inline]
fn try_from(s: EString) -> Result<Self, Self::Error> {
match s.to_lowercase().as_str() {
"true" | "t" | "yes" | "y" | "on" | "1" => Ok(true),
"false" | "f" | "no" | "n" | "off" | "0" | "" => Ok(false),
_ => Err(()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ParseError;
#[test]
fn should_parse_bool_variable() {
let test_cases = [
("1", true),
("0", false),
("y", true),
("f", false),
("yes", true),
("true", true),
("false", false),
("t", true),
("f", false),
("on", true),
("off", false),
];
for (val, expected) in test_cases {
let estr = EString::from(val);
match estr.parse::<bool>() {
Ok(res) => assert_eq!(res, expected),
_ => unreachable!(),
};
}
}
#[test]
fn should_throw_parse_error() {
let estr = EString::from("something");
match estr.parse::<bool>() {
Err(ParseError(orig)) => {
assert_eq!(orig, String::from("something"));
}
_ => unreachable!(),
};
}
}

View file

@ -0,0 +1,62 @@
use crate::core::EString;
use std::convert::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(test)]
mod tests {
use super::*;
use crate::ParseError;
#[test]
fn should_parse_number() {
let estr = EString::from("-10");
match estr.parse::<i32>() {
Ok(res) => assert_eq!(res, -10),
_ => unreachable!(),
};
}
#[test]
fn should_parse_float_number() {
let estr = EString::from("-0.15");
match estr.parse::<f32>() {
#[allow(clippy::float_cmp)]
Ok(res) => assert_eq!(res, -0.15),
_ => unreachable!(),
};
}
#[test]
fn should_throw_parse_error() {
let estr = EString::from("-10");
match estr.parse::<u32>() {
Err(ParseError(orig)) => {
assert_eq!(orig, String::from("-10"));
}
_ => unreachable!(),
};
}
}

153
estring/src/core/vec.rs Normal file
View file

@ -0,0 +1,153 @@
//! Contains the implementations to vec type
//!
//! **NOTE**: Require the enabling of the `vec` features
//!
use crate::core::EString;
use std::convert::TryFrom;
use std::fmt::Write;
/// Wrapper for ``Vec`` to split string by a separator (`SEP`).
///
/// **NOTE**: Required the enabling of the `vec` feature.
///
/// # Examples
///
/// ```rust
/// use estring::{SepVec, EString};
///
/// type CommaVec<T> = SepVec<T, ','>;
///
/// fn main() -> Result<(), estring::ParseError> {
/// let res = EString::from("1,2,3").parse::<CommaVec<u8>>()?;
/// assert_eq!(*res, vec![1, 2, 3]);
///
/// Ok(())
/// }
///
/// ```
///
#[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<EString> for SepVec<T, SEP>
where
T: TryFrom<EString> + std::fmt::Display,
{
type Error = T::Error;
fn try_from(value: EString) -> Result<Self, Self::Error> {
let inner = value
.split(SEP)
.map(str::trim)
.map(EString::from)
.map(T::try_from)
.collect::<Result<Vec<_>, _>>()?;
Ok(Self(inner))
}
}
#[cfg(test)]
mod tests {
use super::*;
const COMMA: char = ',';
const SEMI: char = ';';
type CommaVec<T> = SepVec<T, COMMA>;
type SemiVec<T> = SepVec<T, SEMI>;
#[test]
fn should_parse_into_vec() {
let estr = EString::from("a,b,c,d,e");
match estr.parse::<CommaVec<&str>>() {
Ok(res) => assert_eq!(*res, vec!["a", "b", "c", "d", "e"]),
_ => unreachable!(),
};
}
#[test]
fn should_trim_identations_before_parsing() {
let input = "
a , b, c,
d,e";
let estr = EString::from(input);
match estr.parse::<CommaVec<&str>>() {
Ok(res) => assert_eq!(*res, vec!["a", "b", "c", "d", "e"]),
_ => unreachable!(),
};
}
#[test]
fn should_parse_into_vector_of_vectors() {
let estr = EString::from("a,b; c,d,e; f,g");
match estr.parse::<SemiVec<CommaVec<&str>>>() {
Ok(res) => assert_eq!(
res,
SemiVec::from(vec![
CommaVec::from(vec!["a", "b"]),
CommaVec::from(vec!["c", "d", "e"]),
CommaVec::from(vec!["f", "g"])
])
),
_ => unreachable!(),
};
}
#[cfg(feature = "number")]
mod numbers {
use super::*;
use crate::ParseError;
#[test]
fn should_parse_into_num_vec() {
let estr = EString::from("1,2,3,4,5");
match estr.parse::<CommaVec<i32>>() {
Ok(res) => assert_eq!(*res, vec![1, 2, 3, 4, 5]),
_ => unreachable!(),
};
}
#[test]
fn should_throw_parse_vec_error() {
let estr = EString::from("1,2,3,4,5");
match estr.parse::<SemiVec<i32>>() {
Err(ParseError(orig)) => {
assert_eq!(orig, String::from("1,2,3,4,5"));
}
_ => unreachable!(),
};
}
}
}

19
estring/src/error.rs Normal file
View file

@ -0,0 +1,19 @@
/// Failed to parse the specified string.
#[derive(Debug)]
pub struct ParseError(pub String);
impl std::fmt::Display for ParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, r#"Failed to parse: "{}""#, self.0)
}
}
impl std::error::Error for ParseError {}
impl std::ops::Deref for ParseError {
type Target = String;
fn deref(&self) -> &Self::Target {
&self.0
}
}

72
estring/src/lib.rs Normal file
View file

@ -0,0 +1,72 @@
//!
//! # ``EString``
//!
//! A simple way to parse a string using type annotations.
//!
//! This package was originally designed for [enve]
//!
//! [enve]: https://github.com/pleshevskiy/itconfig-rs
//!
//! ## Getting started
//!
//! ```rust
//! use estring::{SepVec, EString};
//!
//! type PlusVec<T> = SepVec<T, '+'>;
//! type MulVec<T> = SepVec<T, '*'>;
//!
//! fn main() -> Result<(), estring::ParseError> {
//! let res = EString::from("10+5*2+3")
//! .parse::<PlusVec<MulVec<f32>>>()?
//! .iter()
//! .map(|m| m.iter().product::<f32>())
//! .sum::<f32>();
//!
//! assert_eq!(res, 23.0);
//! Ok(())
//! }
//! ```
//!
//! ## Installation
//!
//! **The MSRV is 1.51.0**
//!
//! Add `estring = { version = "0.1", features = ["vec", "number"] }` as a
//! dependency in `Cargo.toml`.
//!
//! `Cargo.toml` example:
//!
//! ```toml
//! [package]
//! name = "my-crate"
//! version = "0.1.0"
//! authors = ["Me <user@rust-lang.org>"]
//!
//! [dependencies]
//! estring = { version = "0.1", features = ["vec", "number"] }
//! ```
//!
//! ## License
//!
//! **MIT**.
//!
//! See [LICENSE](./LICENSE) to see the full text.
//!
#![deny(clippy::pedantic)]
#![allow(clippy::module_name_repetitions)]
#![deny(missing_docs)]
pub mod core;
mod error;
pub use crate::core::*;
pub use crate::error::ParseError;
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
let result = 2 + 2;
assert_eq!(result, 4);
}
}

View file

@ -1,4 +1,4 @@
use enve::core::SepVec;
use enve::estr::SepVec;
type MinusVec<T> = SepVec<T, '-'>;
type PlusVec<T> = SepVec<T, '+'>;

View file

@ -1,65 +1,238 @@
#[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 crate::error::Error;
use std::convert::{Infallible, TryFrom};
use estring::EString;
use std::convert::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 EString {
#[inline]
pub fn parse<T: TryFrom<EString>>(self) -> Result<T, Error> {
let orig = self.0.clone();
<T as TryFrom<EString>>::try_from(self).map_err(|_| Error::Parse(orig))
}
}
impl<T> From<T> for EString
pub fn get_or_set_default<R>(env_name: &str, default: R) -> Result<R, Error>
where
T: std::fmt::Display,
R: TryFrom<EString> + std::fmt::Display,
{
#[inline]
fn from(val: T) -> Self {
Self(val.to_string())
}
get::<R>(env_name).or_else(|err| match err {
Error::NotPresent => sset(env_name, &default).parse().map_err(Error::from),
_ => Err(err),
})
}
impl std::ops::Deref for EString {
type Target = String;
#[inline]
fn deref(&self) -> &Self::Target {
&self.0
}
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))
}
impl TryFrom<EString> for String {
type Error = Infallible;
#[inline]
fn try_from(s: EString) -> Result<Self, Self::Error> {
Ok(s.0)
}
pub fn sget(env_name: &str) -> Result<EString, Error> {
std::env::var(env_name)
.map_err(Error::from)
.map(EString::from)
}
impl TryFrom<EString> for &'static str {
type Error = Infallible;
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()
}
#[inline]
fn try_from(s: EString) -> Result<Self, Self::Error> {
Ok(Box::leak(s.0.into_boxed_str()))
#[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!(),
};
}
}
}

View file

@ -1,39 +0,0 @@
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"
))
}
}

View file

@ -1,60 +0,0 @@
use crate::core::EString;
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<EString> for SepVec<T, SEP>
where
T: TryFrom<EString> + std::fmt::Display,
{
type Error = T::Error;
fn try_from(value: EString) -> Result<Self, Self::Error> {
let inner = value
.split(SEP)
.map(|p| p.trim())
.map(EString::from)
.map(T::try_from)
.collect::<Result<Vec<_>, _>>()?;
Ok(Self(inner))
}
}

View file

@ -4,7 +4,7 @@ use std::ffi::OsString;
use std::fmt;
/// The error type for operations interacting with environment variables
#[derive(Debug, PartialEq)]
#[derive(Debug)]
pub enum Error {
/// The specified environment variable was not present in the current process's environment.
NotPresent,
@ -45,3 +45,9 @@ impl From<VarError> for Error {
}
}
}
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

@ -137,24 +137,25 @@
#![forbid(non_ascii_idents)]
#![deny(
missing_debug_implementations,
// missing_docs,
missing_docs,
unstable_features,
unused_imports,
unused_qualifications
)]
#![warn(missing_docs)]
// Clippy lints
#![deny(clippy::all)]
#![allow(clippy::needless_doctest_main)]
/////////////////////////////////////////////////////////////////////////////
pub mod core;
mod core;
mod error;
mod utils;
pub mod estr;
pub use self::core::*;
pub use self::error::*;
pub use self::utils::*;
pub use self::core::{get, get_or_set_default, sget, sset};
pub use self::error::Error;
#[cfg(feature = "macro")]
extern crate enve_mod;

View file

@ -1,236 +0,0 @@
use crate::core::EString;
use crate::error::Error;
use std::convert::TryFrom;
use std::env;
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 => set(env_name, &default).parse(),
_ => Err(err),
})
}
pub fn get<R>(env_name: &str) -> Result<R, Error>
where
R: TryFrom<EString>,
{
env::var(env_name)
.map_err(From::from)
.map(EString::from)
.and_then(EString::parse)
}
pub fn set<V>(env_name: &str, value: V) -> EString
where
V: std::fmt::Display,
{
let val = value.to_string();
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();
set(&en, "hello");
match env::var(&en) {
Ok(var) => assert_eq!(&var, "hello"),
_ => unreachable!(),
}
}
#[test]
fn should_return_variable() {
let en = TestCase::<1>.to_string();
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!(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();
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();
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!(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());
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::core::vec::{CommaVec, SemiVec, SepVec};
#[test]
fn should_return_var_as_vector() {
let en = TestCase::<8>.to_string();
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";
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();
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();
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!(env::var(&en).unwrap(), "1,2,3,4");
}
_ => unreachable!(),
};
}
}
}