use derive_more::*;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::borrow::{Borrow, Cow};
use std::cmp::{Ordering, PartialEq, PartialOrd};
use std::convert::TryFrom;
use std::fmt::{Debug, Formatter};
use std::ops::Deref;
pub type TextID<'a> = ConstrainedText<&'a str, NonEmptyAllASCII>;
pub type TextName<'a> = ConstrainedText<Cow<'a, str>, NonEmpty>;
pub trait TextConstraint {
fn new() -> Self;
fn check(text: &str) -> bool;
fn required() -> &'static str;
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct NonEmpty;
impl TextConstraint for NonEmpty {
fn new() -> Self {
Self
}
fn check(text: &str) -> bool {
!text.trim().is_empty()
}
fn required() -> &'static str {
"a non-empty, non-whitespace string"
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct NonEmptyAllASCII;
impl TextConstraint for NonEmptyAllASCII {
fn new() -> Self {
Self
}
fn check(text: &str) -> bool {
!text.trim().is_empty() && text.chars().all(|c| char::is_ascii(&c))
}
fn required() -> &'static str {
"a non-empty, non-whitespace, all-ASCII string"
}
}
#[derive(Display, Clone, Ord, Eq, Hash)]
#[display(fmt = "_0")]
pub struct ConstrainedText<T: AsRef<str>, C: TextConstraint>(T, C);
impl<T: AsRef<str>, C: TextConstraint> Debug for ConstrainedText<T, C> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self.get())
}
}
impl<T: AsRef<str>, C: TextConstraint> ConstrainedText<T, C> {
pub fn new(text: T) -> Option<Self> {
if !C::check(text.as_ref()) {
None
} else {
Some(ConstrainedText(text, C::new()))
}
}
pub fn get(&self) -> &str {
self.0.as_ref()
}
}
impl<'a, T, C> TryFrom<&'a str> for ConstrainedText<T, C>
where
T: AsRef<str> + From<&'a str>,
C: TextConstraint,
{
type Error = String;
fn try_from(value: &'a str) -> Result<Self, Self::Error> {
Self::new(value.into()).ok_or_else(|| format!("invalid value: {} required", C::required()))
}
}
impl<T: AsRef<str>, C: TextConstraint> Deref for ConstrainedText<T, C> {
type Target = str;
fn deref(&self) -> &Self::Target {
self.get()
}
}
impl<T: AsRef<str>, C: TextConstraint> AsRef<str> for ConstrainedText<T, C> {
fn as_ref(&self) -> &str {
self.get()
}
}
impl<T: AsRef<str>, C: TextConstraint> Borrow<str> for ConstrainedText<T, C> {
fn borrow(&self) -> &str {
self.get()
}
}
impl<T: AsRef<str>, V: AsRef<str>, C: TextConstraint> PartialEq<V> for ConstrainedText<T, C> {
fn eq(&self, other: &V) -> bool {
self.get() == other.as_ref()
}
}
impl<T: AsRef<str>, C: TextConstraint> PartialEq<ConstrainedText<T, C>> for str {
fn eq(&self, other: &ConstrainedText<T, C>) -> bool {
self == other.get()
}
}
impl<T: AsRef<str>, C: TextConstraint> PartialEq<ConstrainedText<T, C>> for String {
fn eq(&self, other: &ConstrainedText<T, C>) -> bool {
self == other.get()
}
}
impl<T: AsRef<str>, V: AsRef<str>, C: TextConstraint> PartialOrd<V> for ConstrainedText<T, C> {
fn partial_cmp(&self, other: &V) -> Option<Ordering> {
self.get().partial_cmp(other.as_ref())
}
}
impl<T: AsRef<str>, C: TextConstraint> PartialOrd<ConstrainedText<T, C>> for str {
fn partial_cmp(&self, other: &ConstrainedText<T, C>) -> Option<Ordering> {
self.partial_cmp(other.get())
}
}
impl<T: AsRef<str>, C: TextConstraint> PartialOrd<ConstrainedText<T, C>> for String {
fn partial_cmp(&self, other: &ConstrainedText<T, C>) -> Option<Ordering> {
AsRef::<str>::as_ref(self).partial_cmp(other.get())
}
}
impl<T: AsRef<str>, C: TextConstraint> Serialize for ConstrainedText<T, C> {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
Serialize::serialize(self.get(), serializer)
}
}
impl<'a, 'de: 'a, T, C> Deserialize<'de> for ConstrainedText<T, C>
where
T: AsRef<str> + From<&'a str>,
C: TextConstraint,
{
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let string_val: &str = Deserialize::deserialize(deserializer)?;
let value = string_val.into();
ConstrainedText::new(value).ok_or_else(|| {
serde::de::Error::custom(format!("expected {}, got [{}]", C::required(), string_val))
})
}
}
impl<'a> TextName<'a> {
pub fn new_from_str<T: Into<Cow<'a, str>>>(text: T) -> Option<Self> {
Self::new(text.into())
}
}