commit 7d58a94d5a00ae4794e6fd2d1add0d2b16714424 Author: Dmitriy Pleshevskiy Date: Sat Jul 23 22:36:22 2022 +0300 . diff --git a/.vim/coc-settings.json b/.vim/coc-settings.json new file mode 100644 index 0000000..6f5c4ed --- /dev/null +++ b/.vim/coc-settings.json @@ -0,0 +1,3 @@ +{ + "rust-analyzer.cargo.features": "all" +} diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..20da45e --- /dev/null +++ b/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/LICENSE b/LICENSE new file mode 100644 index 0000000..8afbf27 --- /dev/null +++ b/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/README.md b/README.md new file mode 100644 index 0000000..b49dbaa --- /dev/null +++ b/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/src/core.rs b/src/core.rs new file mode 100644 index 0000000..68a2151 --- /dev/null +++ b/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/src/core/prim.rs b/src/core/prim.rs new file mode 100644 index 0000000..ff19878 --- /dev/null +++ b/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/src/core/prim/bool.rs b/src/core/prim/bool.rs new file mode 100644 index 0000000..f0aacc9 --- /dev/null +++ b/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/src/core/prim/number.rs b/src/core/prim/number.rs new file mode 100644 index 0000000..3b93389 --- /dev/null +++ b/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/src/core/vec.rs b/src/core/vec.rs new file mode 100644 index 0000000..c59d45c --- /dev/null +++ b/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/src/error.rs b/src/error.rs new file mode 100644 index 0000000..a484b68 --- /dev/null +++ b/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/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..e38b638 --- /dev/null +++ b/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); + } +}