ichen_openprotocol/
text.rs

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
9/// A text string ID that cannot be empty or all-whitespace, and must be all-ASCII.
10///
11/// This type is usually used for specifying a unique ID.
12///
13/// It `Deref`s to `&str`.
14///
15pub type TextID<'a> = ConstrainedText<&'a str, NonEmptyAllASCII>;
16
17/// A `Cow<str>` for a name that cannot be empty or all-whitespace.
18///
19/// It `Deref`s to `&str`.
20///
21pub type TextName<'a> = ConstrainedText<Cow<'a, str>, NonEmpty>;
22
23/// A trait that constrains the format of a text string.
24///
25pub trait TextConstraint {
26    /// Create a new instance of the text constraint.
27    fn new() -> Self;
28
29    /// Check if a text string is valid under the text constraint.
30    fn check(text: &str) -> bool;
31
32    /// Description of valid text strings.
33    fn required() -> &'static str;
34}
35
36/// A text constraint that rejects empty strings and strings containing only whitespaces.
37///
38#[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/// A text constraint that rejects empty strings and strings containing only whitespaces.
54/// Only ASCII characters can be in the text string.
55///
56#[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/// A data structure that wraps a text string (or anything that dereferences into a text string)
72/// while guaranteeing that the specified text constraint is upheld.
73///
74#[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    /// Create a new `ConstrainedText` from a text string and a constraint.
86    ///
87    /// # Errors
88    ///
89    /// Returns `None` if `text` violates the text constraint.
90    ///
91    /// ## Error Examples
92    ///
93    /// ~~~
94    /// # use ichen_openprotocol::*;
95    /// # use std::str::FromStr;
96    /// assert_eq!(None, TextID::new("     "));
97    /// assert_eq!(None, TextID::new(""));
98    /// assert_eq!(None, TextID::new("你好吗?"));
99    /// ~~~
100    ///
101    /// # Examples
102    ///
103    /// ~~~
104    /// # use ichen_openprotocol::*;
105    /// let id = TextName::new_from_str("你好吗?").unwrap();
106    /// assert_eq!("你好吗?", &id);
107    /// ~~~
108    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    /// Convert a `ConstrainedText` into a string.
117    ///
118    /// # Examples
119    ///
120    /// ~~~
121    /// # use ichen_openprotocol::*;
122    /// let id = TextID::new("hello").unwrap();
123    /// assert_eq!("hello", &id);
124    /// ~~~
125    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}