add str traits for split, between, unquote; consolidate tests

Signed-off-by: Jason Volk <jason@zemos.net>
This commit is contained in:
Jason Volk 2024-08-31 02:13:23 +00:00 committed by strawberry
parent 2db017af37
commit 99ad404ea9
8 changed files with 212 additions and 51 deletions

View file

@ -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}; use crate::{utils::exchange, Result};
pub const EMPTY: &str = ""; 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. /// Parses the bytes into a string.
pub fn string_from_bytes(bytes: &[u8]) -> Result<String> { pub fn string_from_bytes(bytes: &[u8]) -> Result<String> {
let str: &str = str_from_bytes(bytes)?; let str: &str = str_from_bytes(bytes)?;

View 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)
}
}

View 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)) }
}

View 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");
}

View 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) }
}

View 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 }
}

View file

@ -36,33 +36,6 @@ fn increment_wrap() {
assert_eq!(res, 0); 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] #[test]
fn checked_add() { fn checked_add() {
use crate::checked; use crate::checked;
@ -134,19 +107,3 @@ async fn mutex_map_contend() {
tokio::try_join!(join_b, join_a).expect("joined"); tokio::try_join!(join_b, join_a).expect("joined");
assert!(map.is_empty(), "Must be empty"); 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");
}

View file

@ -7,7 +7,7 @@ use std::{
}; };
use async_trait::async_trait; use async_trait::async_trait;
use conduit::{err, error::inspect_log, utils::string::split_once_infallible, Err, Result, Server}; use conduit::{err, error::inspect_log, utils::string::SplitInfallible, Err, Result, Server};
use database::Database; use database::Database;
/// Abstract interface for a Service /// Abstract interface for a Service
@ -147,4 +147,4 @@ where
/// Utility for service implementations; see Service::name() in the trait. /// Utility for service implementations; see Service::name() in the trait.
#[inline] #[inline]
pub(crate) fn make_name(module_path: &str) -> &str { split_once_infallible(module_path, "::").1 } pub(crate) fn make_name(module_path: &str) -> &str { module_path.split_once_infallible("::").1 }