1use derive_more::*;
2use serde::{Deserialize, Deserializer, Serialize, Serializer};
3use std::borrow::{Borrow, Cow};
4use std::cmp::{Ordering, PartialEq, PartialOrd};
5use std::convert::TryFrom;
6use std::fmt::{Debug, Formatter};
7use std::ops::Deref;
8
9pub type TextID<'a> = ConstrainedText<&'a str, NonEmptyAllASCII>;
16
17pub type TextName<'a> = ConstrainedText<Cow<'a, str>, NonEmpty>;
22
23pub trait TextConstraint {
26 fn new() -> Self;
28
29 fn check(text: &str) -> bool;
31
32 fn required() -> &'static str;
34}
35
36#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
39pub struct NonEmpty;
40
41impl TextConstraint for NonEmpty {
42 fn new() -> Self {
43 Self
44 }
45 fn check(text: &str) -> bool {
46 !text.trim().is_empty()
47 }
48 fn required() -> &'static str {
49 "a non-empty, non-whitespace string"
50 }
51}
52
53#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
57pub struct NonEmptyAllASCII;
58
59impl TextConstraint for NonEmptyAllASCII {
60 fn new() -> Self {
61 Self
62 }
63 fn check(text: &str) -> bool {
64 !text.trim().is_empty() && text.chars().all(|c| char::is_ascii(&c))
65 }
66 fn required() -> &'static str {
67 "a non-empty, non-whitespace, all-ASCII string"
68 }
69}
70
71#[derive(Display, Clone, Ord, Eq, Hash)]
75#[display(fmt = "_0")]
76pub struct ConstrainedText<T: AsRef<str>, C: TextConstraint>(T, C);
77
78impl<T: AsRef<str>, C: TextConstraint> Debug for ConstrainedText<T, C> {
79 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
80 write!(f, "{:?}", self.get())
81 }
82}
83
84impl<T: AsRef<str>, C: TextConstraint> ConstrainedText<T, C> {
85 pub fn new(text: T) -> Option<Self> {
109 if !C::check(text.as_ref()) {
110 None
111 } else {
112 Some(ConstrainedText(text, C::new()))
113 }
114 }
115
116 pub fn get(&self) -> &str {
126 self.0.as_ref()
127 }
128}
129
130impl<'a, T, C> TryFrom<&'a str> for ConstrainedText<T, C>
131where
132 T: AsRef<str> + From<&'a str>,
133 C: TextConstraint,
134{
135 type Error = String;
136
137 fn try_from(value: &'a str) -> Result<Self, Self::Error> {
138 Self::new(value.into()).ok_or_else(|| format!("invalid value: {} required", C::required()))
139 }
140}
141
142impl<T: AsRef<str>, C: TextConstraint> Deref for ConstrainedText<T, C> {
143 type Target = str;
144
145 fn deref(&self) -> &Self::Target {
146 self.get()
147 }
148}
149
150impl<T: AsRef<str>, C: TextConstraint> AsRef<str> for ConstrainedText<T, C> {
151 fn as_ref(&self) -> &str {
152 self.get()
153 }
154}
155
156impl<T: AsRef<str>, C: TextConstraint> Borrow<str> for ConstrainedText<T, C> {
157 fn borrow(&self) -> &str {
158 self.get()
159 }
160}
161
162impl<T: AsRef<str>, V: AsRef<str>, C: TextConstraint> PartialEq<V> for ConstrainedText<T, C> {
163 fn eq(&self, other: &V) -> bool {
164 self.get() == other.as_ref()
165 }
166}
167
168impl<T: AsRef<str>, C: TextConstraint> PartialEq<ConstrainedText<T, C>> for str {
169 fn eq(&self, other: &ConstrainedText<T, C>) -> bool {
170 self == other.get()
171 }
172}
173
174impl<T: AsRef<str>, C: TextConstraint> PartialEq<ConstrainedText<T, C>> for String {
175 fn eq(&self, other: &ConstrainedText<T, C>) -> bool {
176 self == other.get()
177 }
178}
179
180impl<T: AsRef<str>, V: AsRef<str>, C: TextConstraint> PartialOrd<V> for ConstrainedText<T, C> {
181 fn partial_cmp(&self, other: &V) -> Option<Ordering> {
182 self.get().partial_cmp(other.as_ref())
183 }
184}
185
186impl<T: AsRef<str>, C: TextConstraint> PartialOrd<ConstrainedText<T, C>> for str {
187 fn partial_cmp(&self, other: &ConstrainedText<T, C>) -> Option<Ordering> {
188 self.partial_cmp(other.get())
189 }
190}
191
192impl<T: AsRef<str>, C: TextConstraint> PartialOrd<ConstrainedText<T, C>> for String {
193 fn partial_cmp(&self, other: &ConstrainedText<T, C>) -> Option<Ordering> {
194 AsRef::<str>::as_ref(self).partial_cmp(other.get())
195 }
196}
197
198impl<T: AsRef<str>, C: TextConstraint> Serialize for ConstrainedText<T, C> {
199 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
200 Serialize::serialize(self.get(), serializer)
201 }
202}
203
204impl<'a, 'de: 'a, T, C> Deserialize<'de> for ConstrainedText<T, C>
205where
206 T: AsRef<str> + From<&'a str>,
207 C: TextConstraint,
208{
209 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
210 let string_val: &str = Deserialize::deserialize(deserializer)?;
211 let value = string_val.into();
212
213 ConstrainedText::new(value).ok_or_else(|| {
214 serde::de::Error::custom(format!("expected {}, got [{}]", C::required(), string_val))
215 })
216 }
217}
218
219impl<'a> TextName<'a> {
220 pub fn new_from_str<T: Into<Cow<'a, str>>>(text: T) -> Option<Self> {
221 Self::new(text.into())
222 }
223}