.
This commit is contained in:
commit
7d58a94d5a
11 changed files with 585 additions and 0 deletions
3
.vim/coc-settings.json
Normal file
3
.vim/coc-settings.json
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"rust-analyzer.cargo.features": "all"
|
||||
}
|
28
Cargo.toml
Normal file
28
Cargo.toml
Normal 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
21
LICENSE
Normal 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
57
README.md
Normal 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
99
src/core.rs
Normal 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
14
src/core/prim.rs
Normal 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
57
src/core/prim/bool.rs
Normal 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
62
src/core/prim/number.rs
Normal 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
153
src/core/vec.rs
Normal 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
19
src/error.rs
Normal 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
72
src/lib.rs
Normal 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);
|
||||
}
|
||||
}
|
Reference in a new issue