#[must_use]
pub fn trim(s: &str) -> String {
s.trim().to_owned()
}
#[must_use]
pub fn downcase(s: &str) -> String {
s.to_lowercase()
}
#[must_use]
pub fn upcase(s: &str) -> String {
s.to_uppercase()
}
#[must_use]
pub fn squish(s: &str) -> String {
s.split_whitespace().collect::<Vec<_>>().join(" ")
}
#[must_use]
pub fn strip_nul(s: &str) -> String {
s.replace('\u{0}', "")
}
pub trait Normalize {
fn normalize(&mut self);
}
#[doc(hidden)]
pub trait NormalizeNoop: Normalize {}
pub trait NormalizedModel {
fn normalize_lookup(column: &str, value: &str) -> Option<String>;
}
#[must_use]
pub fn normalize_lookup_value<M: NormalizedModel>(column: &str, value: &str) -> String {
M::normalize_lookup(column, value).unwrap_or_else(|| value.to_owned())
}
#[doc(hidden)]
pub struct SpezNormalize<'a, T: ?Sized>(pub &'a T);
impl<T: ?Sized> Clone for SpezNormalize<'_, T> {
fn clone(&self) -> Self {
*self
}
}
impl<T: ?Sized> Copy for SpezNormalize<'_, T> {}
#[doc(hidden)]
pub trait SpezNormalizeNoop<'a, T> {
fn spez_normalize_insert(self) -> &'a T;
}
impl<'a, T: NormalizeNoop> SpezNormalizeNoop<'a, T> for &&SpezNormalize<'a, T> {
#[inline]
fn spez_normalize_insert(self) -> &'a T {
self.0
}
}
#[doc(hidden)]
pub trait SpezNormalizeYes<'a, T> {
fn spez_normalize_insert(self) -> T;
}
impl<'a, T: Normalize + Clone> SpezNormalizeYes<'a, T> for &SpezNormalize<'a, T> {
#[inline]
fn spez_normalize_insert(self) -> T {
let mut owned = self.0.clone();
owned.normalize();
owned
}
}
#[doc(hidden)]
pub trait SpezNormalizeNo<'a, T: ?Sized> {
fn spez_normalize_insert(self) -> &'a T;
}
impl<'a, T: ?Sized> SpezNormalizeNo<'a, T> for SpezNormalize<'a, T> {
#[inline]
fn spez_normalize_insert(self) -> &'a T {
self.0
}
}
#[doc(hidden)]
pub trait SpezNormalizeManyNoop<'a, T> {
fn spez_normalize_insert_many(self) -> &'a [T];
}
impl<'a, T: NormalizeNoop> SpezNormalizeManyNoop<'a, T> for &&SpezNormalize<'a, [T]> {
#[inline]
fn spez_normalize_insert_many(self) -> &'a [T] {
self.0
}
}
#[doc(hidden)]
pub trait SpezNormalizeManyYes<'a, T> {
fn spez_normalize_insert_many(self) -> Vec<T>;
}
impl<'a, T: Normalize + Clone> SpezNormalizeManyYes<'a, T> for &SpezNormalize<'a, [T]> {
#[inline]
fn spez_normalize_insert_many(self) -> Vec<T> {
self.0
.iter()
.map(|item| {
let mut owned = item.clone();
owned.normalize();
owned
})
.collect()
}
}
#[doc(hidden)]
pub trait SpezNormalizeManyNo<'a, T> {
fn spez_normalize_insert_many(self) -> &'a [T];
}
impl<'a, T> SpezNormalizeManyNo<'a, T> for SpezNormalize<'a, [T]> {
#[inline]
fn spez_normalize_insert_many(self) -> &'a [T] {
self.0
}
}
#[doc(hidden)]
pub struct SpezLookup<'a, M>(pub core::marker::PhantomData<M>, pub &'a str, pub &'a str);
#[doc(hidden)]
pub trait SpezLookupYes {
fn spez_lookup(self) -> String;
}
impl<M: NormalizedModel> SpezLookupYes for SpezLookup<'_, M> {
#[inline]
fn spez_lookup(self) -> String {
normalize_lookup_value::<M>(self.1, self.2)
}
}
#[doc(hidden)]
pub trait SpezLookupNo {
fn spez_lookup(self) -> String;
}
impl<M> SpezLookupNo for &SpezLookup<'_, M> {
#[inline]
fn spez_lookup(self) -> String {
self.2.to_owned()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn trim_strips_edges() {
assert_eq!(trim(" hi "), "hi");
assert_eq!(trim("hi"), "hi");
}
#[test]
fn strip_nul_removes_embedded_nuls() {
assert_eq!(strip_nul("before\u{0}after"), "beforeafter");
assert_eq!(strip_nul("\u{0}\u{0}a\u{0}"), "a");
}
#[test]
fn strip_nul_leaves_clean_text_untouched() {
assert_eq!(strip_nul("plain text"), "plain text");
assert_eq!(strip_nul("tab\there"), "tab\there");
}
#[test]
fn strip_nul_is_idempotent() {
let once = strip_nul("a\u{0}b");
assert_eq!(strip_nul(&once), once);
}
#[test]
fn downcase_and_upcase() {
assert_eq!(downcase("Foo@X.COM"), "foo@x.com");
assert_eq!(upcase("abc"), "ABC");
}
#[test]
fn squish_collapses_internal_whitespace() {
assert_eq!(squish(" a b\tc\n d "), "a b c d");
}
#[test]
fn builtins_are_idempotent() {
for input in [" Foo@X.COM ", "a b c", "MixedCase", ""] {
let once = squish(&downcase(&trim(input)));
let twice = squish(&downcase(&trim(&once)));
assert_eq!(once, twice, "not idempotent for {input:?}");
}
}
struct Account;
impl NormalizedModel for Account {
fn normalize_lookup(column: &str, value: &str) -> Option<String> {
match column {
"email" => Some(downcase(&trim(value))),
_ => None,
}
}
}
#[test]
fn normalize_lookup_value_canonicalizes_known_column() {
assert_eq!(
normalize_lookup_value::<Account>("email", " FOO@X.com "),
"foo@x.com"
);
assert_eq!(
normalize_lookup_value::<Account>("nickname", " Kept "),
" Kept "
);
}
#[derive(Clone)]
struct Norms(String);
impl Normalize for Norms {
fn normalize(&mut self) {
self.0 = downcase(&trim(&self.0));
}
}
struct Plain(#[allow(dead_code)] String);
#[derive(Clone)]
struct Noop(#[allow(dead_code)] String);
impl Normalize for Noop {
fn normalize(&mut self) {}
}
impl NormalizeNoop for Noop {}
#[test]
#[allow(clippy::needless_borrow)]
fn spez_normalize_picks_noop_then_yes_then_no() {
use super::{SpezNormalizeNo as _, SpezNormalizeNoop as _, SpezNormalizeYes as _};
use std::borrow::Borrow as _;
let n = Norms(" Foo ".into());
let normalized = (&&SpezNormalize(&n)).spez_normalize_insert();
let n_ref: &Norms = normalized.borrow();
assert_eq!(n_ref.0, "foo", "Normalize type must be canonicalized");
assert_eq!(n.0, " Foo ", "input must not be mutated in place");
let z = Noop(" Foo ".into());
let z_ref: &Noop = (&&SpezNormalize(&z)).spez_normalize_insert();
assert!(
std::ptr::eq(z_ref, std::ptr::from_ref(&z)),
"no-op type must not be cloned"
);
let p = Plain(" Foo ".into());
let p_ref: &Plain = (&&SpezNormalize(&p)).spez_normalize_insert();
assert!(
std::ptr::eq(p_ref, std::ptr::from_ref(&p)),
"non-Normalize type must be untouched"
);
}
#[test]
#[allow(clippy::needless_borrow)]
fn spez_normalize_insert_many_picks_noop_then_yes_then_no() {
use super::{
SpezNormalizeManyNo as _, SpezNormalizeManyNoop as _, SpezNormalizeManyYes as _,
};
use std::borrow::Borrow as _;
let n = vec![Norms(" Foo ".into())];
let normalized = (&&SpezNormalize(n.as_slice())).spez_normalize_insert_many();
let n_ref: &[Norms] = normalized.borrow();
assert_eq!(n_ref[0].0, "foo", "each row must be canonicalized");
let z = vec![Noop(" Foo ".into())];
let z_ref: &[Noop] = (&&SpezNormalize(z.as_slice())).spez_normalize_insert_many();
assert!(
std::ptr::eq(z_ref, z.as_slice()),
"no-op batch must not be cloned"
);
let p = vec![Plain(" Foo ".into())];
let p_ref: &[Plain] = (&&SpezNormalize(p.as_slice())).spez_normalize_insert_many();
assert!(
std::ptr::eq(p_ref, p.as_slice()),
"non-Normalize batch must be untouched"
);
}
#[test]
fn normalize_stays_dyn_compatible() {
let mut boxed: Box<dyn Normalize> = Box::new(Norms(" Foo ".into()));
boxed.normalize();
}
#[test]
fn spez_lookup_uses_normalized_model_or_falls_back() {
use super::{SpezLookupNo as _, SpezLookupYes as _};
let got =
SpezLookup::<Account>(std::marker::PhantomData, "email", " FOO@X.com ").spez_lookup();
assert_eq!(got, "foo@x.com");
let got = SpezLookup::<Plain>(std::marker::PhantomData, "email", " Kept ").spez_lookup();
assert_eq!(got, " Kept ");
}
}