diff --git a/Cargo.lock b/Cargo.lock index abb2a7a..ac6a9ae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index fdd4e78..83d08e4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] members = [ - "enve_mod", + "estring", + "enve_mod", ] [package] @@ -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] diff --git a/LICENSE b/LICENSE index 96a1a25..6b5ce3d 100644 --- a/LICENSE +++ b/LICENSE @@ -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 diff --git a/estring/Cargo.toml b/estring/Cargo.toml new file mode 100644 index 0000000..20da45e --- /dev/null +++ b/estring/Cargo.toml @@ -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 "] +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" } diff --git a/estring/LICENSE b/estring/LICENSE new file mode 100644 index 0000000..8afbf27 --- /dev/null +++ b/estring/LICENSE @@ -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. diff --git a/estring/README.md b/estring/README.md new file mode 100644 index 0000000..b49dbaa --- /dev/null +++ b/estring/README.md @@ -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 = SepVec; +type MulVec = SepVec; + +fn main() -> Result<(), estring::ParseError> { + let res = EString::from("10+5*2+3") + .parse::>>()? + .iter() + .map(|m| m.iter().product::()) + .sum::(); + + assert_eq!(res, 23.0); + Ok(()) +} +``` + +You can use custom types as annotations! Just implement `TryFrom`! + +## 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 "] + +[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. diff --git a/estring/src/core.rs b/estring/src/core.rs new file mode 100644 index 0000000..68a2151 --- /dev/null +++ b/estring/src/core.rs @@ -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>(self) -> Result { + let orig = self.0.clone(); + >::try_from(self).map_err(|_| ParseError(orig)) + } +} + +impl From 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 for String { + type Error = Infallible; + + #[inline] + fn try_from(s: EString) -> Result { + Ok(s.0) + } +} + +impl TryFrom for &'static str { + type Error = Infallible; + + #[inline] + fn try_from(s: EString) -> Result { + 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::() { + Ok(res) => assert_eq!(res, EString::from("hello")), + _ => unreachable!(), + } + } + + #[test] + fn should_parse_into_string() { + let estr = EString::from("hello"); + match estr.parse::() { + Ok(res) => assert_eq!(res, String::from("hello")), + _ => unreachable!(), + } + } +} diff --git a/estring/src/core/prim.rs b/estring/src/core/prim.rs new file mode 100644 index 0000000..ff19878 --- /dev/null +++ b/estring/src/core/prim.rs @@ -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::*; diff --git a/estring/src/core/prim/bool.rs b/estring/src/core/prim/bool.rs new file mode 100644 index 0000000..f0aacc9 --- /dev/null +++ b/estring/src/core/prim/bool.rs @@ -0,0 +1,57 @@ +use crate::core::EString; +use std::convert::TryFrom; + +impl TryFrom for bool { + type Error = (); + + #[inline] + fn try_from(s: EString) -> Result { + 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::() { + Ok(res) => assert_eq!(res, expected), + _ => unreachable!(), + }; + } + } + + #[test] + fn should_throw_parse_error() { + let estr = EString::from("something"); + match estr.parse::() { + Err(ParseError(orig)) => { + assert_eq!(orig, String::from("something")); + } + _ => unreachable!(), + }; + } +} diff --git a/estring/src/core/prim/number.rs b/estring/src/core/prim/number.rs new file mode 100644 index 0000000..3b93389 --- /dev/null +++ b/estring/src/core/prim/number.rs @@ -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 for $ty { + type Error = <$ty as std::str::FromStr>::Err; + + #[inline] + fn try_from(s: EString) -> Result { + s.0.parse::() + } + } + )+ + }; +} + +#[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::() { + Ok(res) => assert_eq!(res, -10), + _ => unreachable!(), + }; + } + + #[test] + fn should_parse_float_number() { + let estr = EString::from("-0.15"); + match estr.parse::() { + #[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::() { + Err(ParseError(orig)) => { + assert_eq!(orig, String::from("-10")); + } + _ => unreachable!(), + }; + } +} diff --git a/estring/src/core/vec.rs b/estring/src/core/vec.rs new file mode 100644 index 0000000..c59d45c --- /dev/null +++ b/estring/src/core/vec.rs @@ -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 = SepVec; +/// +/// fn main() -> Result<(), estring::ParseError> { +/// let res = EString::from("1,2,3").parse::>()?; +/// assert_eq!(*res, vec![1, 2, 3]); +/// +/// Ok(()) +/// } +/// +/// ``` +/// +#[derive(Debug, PartialEq, Clone)] +pub struct SepVec(pub Vec); + +impl std::ops::Deref for SepVec { + type Target = Vec; + + #[inline] + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl From> for SepVec { + #[inline] + fn from(vec: Vec) -> Self { + Self(vec) + } +} + +impl std::fmt::Display for SepVec +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 TryFrom for SepVec +where + T: TryFrom + std::fmt::Display, +{ + type Error = T::Error; + + fn try_from(value: EString) -> Result { + let inner = value + .split(SEP) + .map(str::trim) + .map(EString::from) + .map(T::try_from) + .collect::, _>>()?; + Ok(Self(inner)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const COMMA: char = ','; + const SEMI: char = ';'; + + type CommaVec = SepVec; + type SemiVec = SepVec; + + #[test] + fn should_parse_into_vec() { + let estr = EString::from("a,b,c,d,e"); + match estr.parse::>() { + 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::>() { + 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::>>() { + 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::>() { + 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::>() { + Err(ParseError(orig)) => { + assert_eq!(orig, String::from("1,2,3,4,5")); + } + _ => unreachable!(), + }; + } + } +} diff --git a/estring/src/error.rs b/estring/src/error.rs new file mode 100644 index 0000000..a484b68 --- /dev/null +++ b/estring/src/error.rs @@ -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 + } +} diff --git a/estring/src/lib.rs b/estring/src/lib.rs new file mode 100644 index 0000000..e38b638 --- /dev/null +++ b/estring/src/lib.rs @@ -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 = SepVec; +//! type MulVec = SepVec; +//! +//! fn main() -> Result<(), estring::ParseError> { +//! let res = EString::from("10+5*2+3") +//! .parse::>>()? +//! .iter() +//! .map(|m| m.iter().product::()) +//! .sum::(); +//! +//! 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 "] +//! +//! [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); + } +} diff --git a/examples/calc.rs b/examples/calc.rs index 3460667..129b488 100644 --- a/examples/calc.rs +++ b/examples/calc.rs @@ -1,4 +1,4 @@ -use enve::core::SepVec; +use enve::estr::SepVec; type MinusVec = SepVec; type PlusVec = SepVec; diff --git a/src/core.rs b/src/core.rs index 95a9f85..61712a4 100644 --- a/src/core.rs +++ b/src/core.rs @@ -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>(self) -> Result { - let orig = self.0.clone(); - >::try_from(self).map_err(|_| Error::Parse(orig)) - } -} - -impl From for EString +pub fn get_or_set_default(env_name: &str, default: R) -> Result where - T: std::fmt::Display, + R: TryFrom + std::fmt::Display, { - #[inline] - fn from(val: T) -> Self { - Self(val.to_string()) - } + get::(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(env_name: &str) -> Result +where + R: TryFrom, +{ + sget(env_name).and_then(|v| v.parse().map_err(Error::from)) } -impl TryFrom for String { - type Error = Infallible; - - #[inline] - fn try_from(s: EString) -> Result { - Ok(s.0) - } +pub fn sget(env_name: &str) -> Result { + std::env::var(env_name) + .map_err(Error::from) + .map(EString::from) } -impl TryFrom for &'static str { - type Error = Infallible; +pub fn sset(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 { - Ok(Box::leak(s.0.into_boxed_str())) +#[cfg(test)] +mod tests { + use super::*; + + struct TestCase; + + impl std::fmt::Display for TestCase { + 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::(&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::(&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::(&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::>(&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::>(&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::>>(&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::>(&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!(), + }; + } } } diff --git a/src/core/prim.rs b/src/core/prim.rs deleted file mode 100644 index d0ba318..0000000 --- a/src/core/prim.rs +++ /dev/null @@ -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 for $ty { - type Error = <$ty as std::str::FromStr>::Err; - - #[inline] - fn try_from(s: EString) -> Result { - s.0.parse::() - } - } - )+ - }; -} - -#[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 for bool { - type Error = Infallible; - - #[inline] - fn try_from(s: EString) -> Result { - Ok(matches!( - s.to_lowercase().as_str(), - "true" | "t" | "yes" | "y" | "on" | "1" - )) - } -} diff --git a/src/core/vec.rs b/src/core/vec.rs deleted file mode 100644 index cc2a76a..0000000 --- a/src/core/vec.rs +++ /dev/null @@ -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 = SepVec; -pub type SemiVec = SepVec; - -#[derive(Debug, PartialEq, Clone)] -pub struct SepVec(pub Vec); - -impl std::ops::Deref for SepVec { - type Target = Vec; - - #[inline] - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl From> for SepVec { - #[inline] - fn from(vec: Vec) -> Self { - Self(vec) - } -} - -impl std::fmt::Display for SepVec -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 TryFrom for SepVec -where - T: TryFrom + std::fmt::Display, -{ - type Error = T::Error; - - fn try_from(value: EString) -> Result { - let inner = value - .split(SEP) - .map(|p| p.trim()) - .map(EString::from) - .map(T::try_from) - .collect::, _>>()?; - Ok(Self(inner)) - } -} diff --git a/src/error.rs b/src/error.rs index 002554e..a6ce455 100644 --- a/src/error.rs +++ b/src/error.rs @@ -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 for Error { } } } + +impl From for Error { + fn from(err: estring::ParseError) -> Self { + Error::Parse(err.clone()) + } +} diff --git a/src/estr.rs b/src/estr.rs new file mode 100644 index 0000000..e0fe52c --- /dev/null +++ b/src/estr.rs @@ -0,0 +1,6 @@ +#[cfg(feature = "vec")] +pub mod vec; +#[cfg(feature = "vec")] +pub use vec::{CommaVec, SemiVec}; + +pub use estring::core::*; diff --git a/src/estr/vec.rs b/src/estr/vec.rs new file mode 100644 index 0000000..4307856 --- /dev/null +++ b/src/estr/vec.rs @@ -0,0 +1,7 @@ +use estring::SepVec; + +const COMMA: char = ','; +const SEMI: char = ';'; + +pub type CommaVec = SepVec; +pub type SemiVec = SepVec; diff --git a/src/lib.rs b/src/lib.rs index d442af4..ebf8312 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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; diff --git a/src/utils.rs b/src/utils.rs deleted file mode 100644 index 5673892..0000000 --- a/src/utils.rs +++ /dev/null @@ -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(env_name: &str, default: R) -> Result -where - R: TryFrom + std::fmt::Display, -{ - get::(env_name).or_else(|err| match err { - Error::NotPresent => set(env_name, &default).parse(), - _ => Err(err), - }) -} - -pub fn get(env_name: &str) -> Result -where - R: TryFrom, -{ - env::var(env_name) - .map_err(From::from) - .map(EString::from) - .and_then(EString::parse) -} - -pub fn set(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; - - impl std::fmt::Display for TestCase { - 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::(&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::(&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::(&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::>(&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::>(&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::>>(&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::>(&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!(), - }; - } - } -}