This commit is contained in:
Dmitriy Pleshevskiy 2022-07-23 22:36:22 +03:00
commit 7d58a94d5a
Signed by: pleshevskiy
GPG key ID: 1B59187B161C0215
11 changed files with 585 additions and 0 deletions

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

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

28
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
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
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
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
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::*;

57
src/core/prim/bool.rs Normal file
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!(),
};
}
}

62
src/core/prim/number.rs Normal file
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
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
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
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);
}
}