add str traits for split, between, unquote; consolidate tests
Signed-off-by: Jason Volk <jason@zemos.net>
This commit is contained in:
parent
2db017af37
commit
99ad404ea9
8 changed files with 212 additions and 51 deletions
|
@ -1,3 +1,10 @@
|
|||
mod between;
|
||||
mod split;
|
||||
mod tests;
|
||||
mod unquote;
|
||||
mod unquoted;
|
||||
|
||||
pub use self::{between::Between, split::SplitInfallible, unquote::Unquote, unquoted::Unquoted};
|
||||
use crate::{utils::exchange, Result};
|
||||
|
||||
pub const EMPTY: &str = "";
|
||||
|
@ -95,12 +102,6 @@ pub fn common_prefix<'a>(choice: &'a [&str]) -> &'a str {
|
|||
})
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn split_once_infallible<'a>(input: &'a str, delim: &'_ str) -> (&'a str, &'a str) {
|
||||
input.split_once(delim).unwrap_or((input, EMPTY))
|
||||
}
|
||||
|
||||
/// Parses the bytes into a string.
|
||||
pub fn string_from_bytes(bytes: &[u8]) -> Result<String> {
|
||||
let str: &str = str_from_bytes(bytes)?;
|
||||
|
|
26
src/core/utils/string/between.rs
Normal file
26
src/core/utils/string/between.rs
Normal file
|
@ -0,0 +1,26 @@
|
|||
type Delim<'a> = (&'a str, &'a str);
|
||||
|
||||
/// Slice a string between a pair of delimeters.
|
||||
pub trait Between<'a> {
|
||||
/// Extract a string between the delimeters. If the delimeters were not
|
||||
/// found None is returned, otherwise the first extraction is returned.
|
||||
fn between(&self, delim: Delim<'_>) -> Option<&'a str>;
|
||||
|
||||
/// Extract a string between the delimeters. If the delimeters were not
|
||||
/// found the original string is returned; take note of this behavior,
|
||||
/// if an empty slice is desired for this case use the fallible version and
|
||||
/// unwrap to EMPTY.
|
||||
fn between_infallible(&self, delim: Delim<'_>) -> &'a str;
|
||||
}
|
||||
|
||||
impl<'a> Between<'a> for &'a str {
|
||||
#[inline]
|
||||
fn between_infallible(&self, delim: Delim<'_>) -> &'a str { self.between(delim).unwrap_or(self) }
|
||||
|
||||
#[inline]
|
||||
fn between(&self, delim: Delim<'_>) -> Option<&'a str> {
|
||||
self.split_once(delim.0)
|
||||
.and_then(|(_, b)| b.rsplit_once(delim.1))
|
||||
.map(|(a, _)| a)
|
||||
}
|
||||
}
|
22
src/core/utils/string/split.rs
Normal file
22
src/core/utils/string/split.rs
Normal file
|
@ -0,0 +1,22 @@
|
|||
use super::EMPTY;
|
||||
|
||||
type Pair<'a> = (&'a str, &'a str);
|
||||
|
||||
/// Split a string with default behaviors on non-match.
|
||||
pub trait SplitInfallible<'a> {
|
||||
/// Split a string at the first occurrence of delim. If not found, the
|
||||
/// entire string is returned in \[0\], while \[1\] is empty.
|
||||
fn split_once_infallible(&self, delim: &str) -> Pair<'a>;
|
||||
|
||||
/// Split a string from the last occurrence of delim. If not found, the
|
||||
/// entire string is returned in \[0\], while \[1\] is empty.
|
||||
fn rsplit_once_infallible(&self, delim: &str) -> Pair<'a>;
|
||||
}
|
||||
|
||||
impl<'a> SplitInfallible<'a> for &'a str {
|
||||
#[inline]
|
||||
fn rsplit_once_infallible(&self, delim: &str) -> Pair<'a> { self.rsplit_once(delim).unwrap_or((self, EMPTY)) }
|
||||
|
||||
#[inline]
|
||||
fn split_once_infallible(&self, delim: &str) -> Pair<'a> { self.split_once(delim).unwrap_or((self, EMPTY)) }
|
||||
}
|
70
src/core/utils/string/tests.rs
Normal file
70
src/core/utils/string/tests.rs
Normal file
|
@ -0,0 +1,70 @@
|
|||
#![cfg(test)]
|
||||
|
||||
#[test]
|
||||
fn common_prefix() {
|
||||
let input = ["conduwuit", "conduit", "construct"];
|
||||
let output = super::common_prefix(&input);
|
||||
assert_eq!(output, "con");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn common_prefix_empty() {
|
||||
let input = ["abcdefg", "hijklmn", "opqrstu"];
|
||||
let output = super::common_prefix(&input);
|
||||
assert_eq!(output, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn common_prefix_none() {
|
||||
let input = [];
|
||||
let output = super::common_prefix(&input);
|
||||
assert_eq!(output, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn camel_to_snake_case_0() {
|
||||
let res = super::camel_to_snake_string("CamelToSnakeCase");
|
||||
assert_eq!(res, "camel_to_snake_case");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn camel_to_snake_case_1() {
|
||||
let res = super::camel_to_snake_string("CAmelTOSnakeCase");
|
||||
assert_eq!(res, "camel_tosnake_case");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unquote() {
|
||||
use super::Unquote;
|
||||
|
||||
assert_eq!("\"foo\"".unquote(), Some("foo"));
|
||||
assert_eq!("\"foo".unquote(), None);
|
||||
assert_eq!("foo".unquote(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unquote_infallible() {
|
||||
use super::Unquote;
|
||||
|
||||
assert_eq!("\"foo\"".unquote_infallible(), "foo");
|
||||
assert_eq!("\"foo".unquote_infallible(), "\"foo");
|
||||
assert_eq!("foo".unquote_infallible(), "foo");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn between() {
|
||||
use super::Between;
|
||||
|
||||
assert_eq!("\"foo\"".between(("\"", "\"")), Some("foo"));
|
||||
assert_eq!("\"foo".between(("\"", "\"")), None);
|
||||
assert_eq!("foo".between(("\"", "\"")), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn between_infallible() {
|
||||
use super::Between;
|
||||
|
||||
assert_eq!("\"foo\"".between_infallible(("\"", "\"")), "foo");
|
||||
assert_eq!("\"foo".between_infallible(("\"", "\"")), "\"foo");
|
||||
assert_eq!("foo".between_infallible(("\"", "\"")), "foo");
|
||||
}
|
33
src/core/utils/string/unquote.rs
Normal file
33
src/core/utils/string/unquote.rs
Normal file
|
@ -0,0 +1,33 @@
|
|||
const QUOTE: char = '"';
|
||||
|
||||
/// Slice a string between quotes
|
||||
pub trait Unquote<'a> {
|
||||
/// Whether the input is quoted. If this is false the fallible methods of
|
||||
/// this interface will fail.
|
||||
fn is_quoted(&self) -> bool;
|
||||
|
||||
/// Unquotes a string. If the input is not quoted it is simply returned
|
||||
/// as-is. If the input is partially quoted on either end that quote is not
|
||||
/// removed.
|
||||
fn unquote(&self) -> Option<&'a str>;
|
||||
|
||||
/// Unquotes a string. The input must be quoted on each side for Some to be
|
||||
/// returned
|
||||
fn unquote_infallible(&self) -> &'a str;
|
||||
}
|
||||
|
||||
impl<'a> Unquote<'a> for &'a str {
|
||||
#[inline]
|
||||
fn unquote_infallible(&self) -> &'a str {
|
||||
self.strip_prefix(QUOTE)
|
||||
.unwrap_or(self)
|
||||
.strip_suffix(QUOTE)
|
||||
.unwrap_or(self)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn unquote(&self) -> Option<&'a str> { self.strip_prefix(QUOTE).and_then(|s| s.strip_suffix(QUOTE)) }
|
||||
|
||||
#[inline]
|
||||
fn is_quoted(&self) -> bool { self.starts_with(QUOTE) && self.ends_with(QUOTE) }
|
||||
}
|
52
src/core/utils/string/unquoted.rs
Normal file
52
src/core/utils/string/unquoted.rs
Normal file
|
@ -0,0 +1,52 @@
|
|||
use std::ops::Deref;
|
||||
|
||||
use serde::{de, Deserialize, Deserializer};
|
||||
|
||||
use super::Unquote;
|
||||
use crate::{err, Result};
|
||||
|
||||
/// Unquoted string which deserialized from a quoted string. Construction from a
|
||||
/// &str is infallible such that the input can already be unquoted. Construction
|
||||
/// from serde deserialization is fallible and the input must be quoted.
|
||||
#[repr(transparent)]
|
||||
pub struct Unquoted(str);
|
||||
|
||||
impl<'a> Unquoted {
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn as_str(&'a self) -> &'a str { &self.0 }
|
||||
}
|
||||
|
||||
impl<'a, 'de: 'a> Deserialize<'de> for &'a Unquoted {
|
||||
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||
let s = <&'a str>::deserialize(deserializer)?;
|
||||
s.is_quoted()
|
||||
.then_some(s)
|
||||
.ok_or(err!(SerdeDe("expected quoted string")))
|
||||
.map_err(de::Error::custom)
|
||||
.map(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a str> for &'a Unquoted {
|
||||
fn from(s: &'a str) -> &'a Unquoted {
|
||||
let s: &'a str = s.unquote_infallible();
|
||||
|
||||
//SAFETY: This is a pattern I lifted from ruma-identifiers for strong-type strs
|
||||
// by wrapping in a tuple-struct.
|
||||
#[allow(clippy::transmute_ptr_to_ptr)]
|
||||
unsafe {
|
||||
std::mem::transmute(s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Unquoted {
|
||||
type Target = str;
|
||||
|
||||
fn deref(&self) -> &Self::Target { &self.0 }
|
||||
}
|
||||
|
||||
impl<'a> AsRef<str> for &'a Unquoted {
|
||||
fn as_ref(&self) -> &'a str { &self.0 }
|
||||
}
|
|
@ -36,33 +36,6 @@ fn increment_wrap() {
|
|||
assert_eq!(res, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn common_prefix() {
|
||||
use utils::string;
|
||||
|
||||
let input = ["conduwuit", "conduit", "construct"];
|
||||
let output = string::common_prefix(&input);
|
||||
assert_eq!(output, "con");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn common_prefix_empty() {
|
||||
use utils::string;
|
||||
|
||||
let input = ["abcdefg", "hijklmn", "opqrstu"];
|
||||
let output = string::common_prefix(&input);
|
||||
assert_eq!(output, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn common_prefix_none() {
|
||||
use utils::string;
|
||||
|
||||
let input = [];
|
||||
let output = string::common_prefix(&input);
|
||||
assert_eq!(output, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn checked_add() {
|
||||
use crate::checked;
|
||||
|
@ -134,19 +107,3 @@ async fn mutex_map_contend() {
|
|||
tokio::try_join!(join_b, join_a).expect("joined");
|
||||
assert!(map.is_empty(), "Must be empty");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn camel_to_snake_case_0() {
|
||||
use utils::string::camel_to_snake_string;
|
||||
|
||||
let res = camel_to_snake_string("CamelToSnakeCase");
|
||||
assert_eq!(res, "camel_to_snake_case");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn camel_to_snake_case_1() {
|
||||
use utils::string::camel_to_snake_string;
|
||||
|
||||
let res = camel_to_snake_string("CAmelTOSnakeCase");
|
||||
assert_eq!(res, "camel_tosnake_case");
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue