#[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(" ")
}
pub trait Normalize {
fn normalize(&mut self);
}
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);
#[doc(hidden)]
pub trait SpezNormalizeYes<'a, T> {
fn spez_normalize(self) -> T;
}
impl<'a, T: Normalize + Clone> SpezNormalizeYes<'a, T> for SpezNormalize<'a, T> {
#[inline]
fn spez_normalize(self) -> T {
let mut owned = self.0.clone();
owned.normalize();
owned
}
}
#[doc(hidden)]
pub trait SpezNormalizeNo<'a, T: ?Sized> {
fn spez_normalize(self) -> &'a T;
}
impl<'a, T: ?Sized> SpezNormalizeNo<'a, T> for &SpezNormalize<'a, T> {
#[inline]
fn spez_normalize(self) -> &'a T {
self.0
}
}
#[doc(hidden)]
pub trait SpezNormalizeManyYes<'a, T> {
fn spez_normalize_many(self) -> Vec<T>;
}
impl<'a, T: Normalize + Clone> SpezNormalizeManyYes<'a, T> for SpezNormalize<'a, [T]> {
#[inline]
fn spez_normalize_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_many(self) -> &'a [T];
}
impl<'a, T> SpezNormalizeManyNo<'a, T> for &SpezNormalize<'a, [T]> {
#[inline]
fn spez_normalize_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 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);
#[test]
fn spez_normalize_runs_for_normalize_types_and_noops_otherwise() {
use super::{SpezNormalizeNo as _, SpezNormalizeYes as _};
use std::borrow::Borrow as _;
let n = Norms(" Foo ".into());
let normalized = SpezNormalize(&n).spez_normalize();
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 p = Plain(" Foo ".into());
let p_ref: &Plain = SpezNormalize(&p).spez_normalize();
assert_eq!(p_ref.0, " Foo ", "non-Normalize type must be untouched");
}
#[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 ");
}
}