use std::borrow::Cow;
pub trait IsEmpty {
fn is_empty(&self) -> bool;
}
impl IsEmpty for () {
fn is_empty(&self) -> bool {
true
}
}
impl IsEmpty for str {
fn is_empty(&self) -> bool {
str::is_empty(self)
}
}
impl IsEmpty for String {
fn is_empty(&self) -> bool {
String::is_empty(self)
}
}
impl IsEmpty for [u8] {
fn is_empty(&self) -> bool {
<[u8]>::is_empty(self)
}
}
impl<const N: usize> IsEmpty for [u8; N] {
fn is_empty(&self) -> bool {
N == 0
}
}
impl IsEmpty for Vec<u8> {
fn is_empty(&self) -> bool {
Vec::is_empty(self)
}
}
impl<T> IsEmpty for Cow<'_, T>
where
T: IsEmpty + ToOwned + ?Sized,
{
fn is_empty(&self) -> bool {
T::is_empty(self.as_ref())
}
}
impl<T> IsEmpty for &T
where
T: IsEmpty + ?Sized,
{
fn is_empty(&self) -> bool {
T::is_empty(self)
}
}
macro_rules! never_empty {
($($ty:ty),+ $(,)?) => {$(
impl IsEmpty for $ty {
fn is_empty(&self) -> bool {
false
}
}
)+};
}
never_empty!(u8, u16, u32, u64, u128, i8, i16, i32, i64, i128);
impl<T> IsEmpty for Option<T>
where
T: IsEmpty,
{
fn is_empty(&self) -> bool {
match self {
Some(value) => value.is_empty(),
None => true,
}
}
}
impl<A, B> IsEmpty for (A, B)
where
A: IsEmpty,
B: IsEmpty,
{
fn is_empty(&self) -> bool {
self.0.is_empty() && self.1.is_empty()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, thiserror::Error)]
#[error("context value carries no caller-supplied data")]
pub struct EmptyError;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct NonEmpty<T>(T);
impl<T> NonEmpty<T>
where
T: IsEmpty,
{
pub fn new(value: T) -> Result<Self, EmptyError> {
if value.is_empty() {
Err(EmptyError)
} else {
Ok(Self(value))
}
}
}
impl<T> NonEmpty<T> {
pub fn into_inner(self) -> T {
self.0
}
pub fn get(&self) -> &T {
&self.0
}
}
impl NonEmpty<&'static str> {
pub const fn from_static(value: &'static str) -> Self {
assert!(!value.is_empty(), "a non-empty context cannot be empty");
Self(value)
}
}
impl NonEmpty<&'static [u8]> {
pub const fn from_static_bytes(value: &'static [u8]) -> Self {
assert!(!value.is_empty(), "a non-empty context cannot be empty");
Self(value)
}
}
#[macro_export]
macro_rules! nonempty {
($value:expr) => {{
const CONTEXT: $crate::NonEmpty<&'static str> = $crate::NonEmpty::from_static($value);
CONTEXT
}};
}
#[macro_export]
macro_rules! nonempty_bytes {
($value:expr) => {{
const CONTEXT: $crate::NonEmpty<&'static [u8]> =
$crate::NonEmpty::from_static_bytes($value);
CONTEXT
}};
}
#[cfg(test)]
mod tests {
use super::*;
use quickcheck_macros::quickcheck;
#[test]
fn empty_shapes_are_empty() {
assert!(().is_empty_ctx());
assert!("".is_empty_ctx());
assert!(String::new().is_empty_ctx());
assert!(b"".as_slice().is_empty_ctx());
assert!([0u8; 0].is_empty_ctx());
assert!(Vec::<u8>::new().is_empty_ctx());
assert!(Cow::<[u8]>::Borrowed(&[]).is_empty_ctx());
assert!(None::<&str>.is_empty_ctx());
assert!(Some("").is_empty_ctx());
assert!(("", "").is_empty_ctx());
assert!(Some(None::<&str>).is_empty_ctx());
assert!((None::<&str>, Some("")).is_empty_ctx());
assert!(((), ()).is_empty_ctx());
}
#[test]
fn shapes_carrying_information_are_not_empty() {
assert!(!"users/email".is_empty_ctx());
assert!(!"x".is_empty_ctx());
assert!(!String::from("x").is_empty_ctx());
assert!(!b"raw".as_slice().is_empty_ctx());
assert!(![1u8].is_empty_ctx());
assert!(!vec![1u8].is_empty_ctx());
assert!(!Cow::<[u8]>::Owned(vec![1]).is_empty_ctx());
assert!(!Some("users/email").is_empty_ctx());
assert!(!("users", "email").is_empty_ctx());
assert!(!("", "email").is_empty_ctx());
assert!(!("users", "").is_empty_ctx());
assert!(!(None::<&str>, "email").is_empty_ctx());
assert!(!Some(Some("x")).is_empty_ctx());
}
#[test]
fn integers_are_never_empty() {
assert!(!0u8.is_empty_ctx());
assert!(!0u16.is_empty_ctx());
assert!(!0u32.is_empty_ctx());
assert!(!0u64.is_empty_ctx());
assert!(!0u128.is_empty_ctx());
assert!(!0i8.is_empty_ctx());
assert!(!0i16.is_empty_ctx());
assert!(!0i32.is_empty_ctx());
assert!(!0i64.is_empty_ctx());
assert!(!0i128.is_empty_ctx());
assert!(!7u64.is_empty_ctx());
assert!(!Some(0u64).is_empty_ctx());
}
#[test]
fn references_defer_to_the_referent() {
let owned = String::from("x");
assert!(!<&String as IsEmpty>::is_empty(&&owned));
assert!(!<&&String as IsEmpty>::is_empty(&&&owned));
let empty = String::new();
assert!(<&String as IsEmpty>::is_empty(&&empty));
assert!(!<&[u8; 3] as IsEmpty>::is_empty(&b"abc"));
assert!(!<&str as IsEmpty>::is_empty(&"abc"));
}
#[test]
fn new_checks_once_at_construction() {
assert_eq!(NonEmpty::new("").unwrap_err(), EmptyError);
assert_eq!(NonEmpty::new(("", "")).unwrap_err(), EmptyError);
assert_eq!(NonEmpty::new(None::<&str>).unwrap_err(), EmptyError);
let context = NonEmpty::new("users/email").unwrap();
assert_eq!(context.get(), &"users/email");
assert_eq!(context.into_inner(), "users/email");
let nested = NonEmpty::new(("users", Some("email"))).unwrap();
assert_eq!(nested.into_inner(), ("users", Some("email")));
}
#[test]
fn from_static_accepts_a_non_empty_literal() {
const CONTEXT: NonEmpty<&'static str> = NonEmpty::from_static("users/email");
assert_eq!(CONTEXT.get(), &"users/email");
assert_eq!(nonempty!("users/email"), CONTEXT);
}
#[test]
#[should_panic(expected = "a non-empty context cannot be empty")]
fn from_static_panics_on_an_empty_string_at_runtime() {
let empty = String::new();
let leaked: &'static str = Box::leak(empty.into_boxed_str());
let _ = NonEmpty::from_static(leaked);
}
#[test]
fn from_static_bytes_accepts_a_non_empty_byte_string() {
const CONTEXT: NonEmpty<&'static [u8]> = NonEmpty::from_static_bytes(b"users/email");
assert_eq!(CONTEXT.get(), &b"users/email".as_slice());
assert_eq!(nonempty_bytes!(b"users/email"), CONTEXT);
}
#[test]
#[should_panic(expected = "a non-empty context cannot be empty")]
fn from_static_bytes_panics_on_empty_bytes_at_runtime() {
let leaked: &'static [u8] = Box::leak(Vec::new().into_boxed_slice());
let _ = NonEmpty::from_static_bytes(leaked);
}
#[test]
fn nonempty_macro_accepts_any_static_constant_expression() {
const TABLE: &str = "users";
assert_eq!(nonempty!(TABLE).get(), &"users");
assert_eq!(
nonempty!(concat!("users", "/", "email")).get(),
&"users/email"
);
}
#[test]
fn cow_is_as_empty_as_its_referent() {
assert!(IsEmpty::is_empty(&Cow::<str>::Borrowed("")));
assert!(IsEmpty::is_empty(&Cow::<str>::Owned(String::new())));
assert!(!IsEmpty::is_empty(&Cow::<str>::Borrowed("x")));
assert!(IsEmpty::is_empty(&Cow::<[u8]>::Owned(Vec::new())));
assert!(!IsEmpty::is_empty(&Cow::<[u8]>::Owned(vec![1])));
}
#[test]
fn error_is_displayable_and_stable() {
assert_eq!(
EmptyError.to_string(),
"context value carries no caller-supplied data"
);
}
#[quickcheck]
fn new_succeeds_exactly_when_the_string_has_bytes(value: String) -> bool {
NonEmpty::new(value.clone()).is_ok() != value.is_empty()
}
#[quickcheck]
fn new_succeeds_exactly_when_the_bytes_are_non_empty(value: Vec<u8>) -> bool {
NonEmpty::new(value.clone()).is_ok() != value.is_empty()
}
#[quickcheck]
fn option_is_as_empty_as_its_payload(value: Option<String>) -> bool {
let expected = value.as_ref().is_none_or(|inner| inner.is_empty());
IsEmpty::is_empty(&value) == expected
}
#[quickcheck]
fn pair_is_empty_only_when_both_sides_are(left: String, right: Vec<u8>) -> bool {
let expected = left.is_empty() && right.is_empty();
IsEmpty::is_empty(&(left, right)) == expected
}
trait IsEmptyCtx {
fn is_empty_ctx(&self) -> bool;
}
impl<T: IsEmpty + ?Sized> IsEmptyCtx for T {
fn is_empty_ctx(&self) -> bool {
IsEmpty::is_empty(self)
}
}
}