1#[derive(Debug, Clone, PartialEq, Eq, Hash)]
25pub struct Symbol {
26 value: String,
27}
28
29impl Symbol {
30 pub fn try_new<S: Into<String>>(value: S) -> Result<Self, SymbolError> {
49 let s = value.into();
50 if Self::is_valid(&s) {
51 Ok(Self { value: s })
52 } else {
53 Err(SymbolError)
54 }
55 }
56
57 pub fn as_str(&self) -> &str {
68 &self.value
69 }
70
71 pub fn is_valid(s: &str) -> bool {
83 let mut chars = s.chars();
84 match chars.next() {
85 Some(first) if first.is_ascii_lowercase() => {}
86 _ => return false,
87 }
88 for c in chars {
89 if !(c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_') {
90 return false;
91 }
92 }
93 true
94 }
95}
96
97impl core::fmt::Display for Symbol {
98 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
99 self.value.fmt(f)
100 }
101}
102
103impl core::ops::Deref for Symbol {
104 type Target = str;
105 fn deref(&self) -> &Self::Target {
106 self.as_str()
107 }
108}
109
110impl core::str::FromStr for Symbol {
111 type Err = SymbolError;
112 fn from_str(s: &str) -> Result<Self, Self::Err> {
113 Symbol::try_new(s)
114 }
115}
116
117impl TryFrom<&str> for Symbol {
118 type Error = SymbolError;
119 fn try_from(value: &str) -> Result<Self, Self::Error> {
120 Symbol::try_new(value)
121 }
122}
123
124impl TryFrom<String> for Symbol {
125 type Error = SymbolError;
126 fn try_from(value: String) -> Result<Self, Self::Error> {
127 Symbol::try_new(value)
128 }
129}
130
131#[derive(Debug, Clone, PartialEq, Eq)]
132pub struct SymbolError;
133
134impl core::fmt::Display for SymbolError {
135 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
136 write!(f, "value must match ^[a-z][a-z0-9_]*$")
137 }
138}
139
140impl std::error::Error for SymbolError {}
141
142impl AsRef<str> for Symbol {
143 fn as_ref(&self) -> &str {
144 self.as_str()
145 }
146}
147
148impl core::borrow::Borrow<str> for Symbol {
149 fn borrow(&self) -> &str {
150 self.as_str()
151 }
152}
153
154impl core::cmp::PartialOrd for Symbol {
155 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
156 Some(self.as_str().cmp(other.as_str()))
157 }
158}
159
160impl core::cmp::Ord for Symbol {
161 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
162 self.as_str().cmp(other.as_str())
163 }
164}
165
166impl From<Symbol> for String {
167 fn from(s: Symbol) -> Self {
168 s.value
169 }
170}
171
172impl From<Symbol> for Box<str> {
173 fn from(s: Symbol) -> Self {
174 s.value.into_boxed_str()
175 }
176}
177
178impl PartialEq<&str> for Symbol {
180 fn eq(&self, other: &&str) -> bool {
181 self.as_str() == *other
182 }
183}
184impl PartialEq<Symbol> for &str {
185 fn eq(&self, other: &Symbol) -> bool {
186 *self == other.as_str()
187 }
188}
189impl PartialEq<String> for Symbol {
190 fn eq(&self, other: &String) -> bool {
191 self.as_str() == other.as_str()
192 }
193}
194impl PartialEq<Symbol> for String {
195 fn eq(&self, other: &Symbol) -> bool {
196 self.as_str() == other.as_str()
197 }
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203
204 use core::str::FromStr;
205
206 #[test]
207 fn is_valid_accepts_simple_lowercase() {
208 assert!(Symbol::is_valid("a"));
209 assert!(Symbol::is_valid("abc"));
210 assert!(Symbol::is_valid("z"));
211 }
212
213 #[test]
214 fn is_valid_accepts_digits_and_underscores_after_first() {
215 assert!(Symbol::is_valid("a1"));
216 assert!(Symbol::is_valid("a_b"));
217 assert!(Symbol::is_valid("a1_b2_c3"));
218 assert!(Symbol::is_valid("a0_9"));
219 assert!(Symbol::is_valid("a__"));
220 }
221
222 #[test]
223 fn is_valid_rejects_empty_and_bad_first_char() {
224 assert!(!Symbol::is_valid(""));
225 assert!(!Symbol::is_valid("1abc"));
226 assert!(!Symbol::is_valid("_abc"));
227 assert!(!Symbol::is_valid("A"));
228 }
229
230 #[test]
231 fn is_valid_rejects_invalid_tail_chars() {
232 assert!(!Symbol::is_valid("a-"));
233 assert!(!Symbol::is_valid("a-1"));
234 assert!(!Symbol::is_valid("a b"));
235 assert!(!Symbol::is_valid("a$"));
236 assert!(!Symbol::is_valid("aB")); assert!(!Symbol::is_valid("aÄ")); }
239
240 #[test]
241 fn try_new_constructs_for_valid_and_errors_for_invalid() {
242 let ok = Symbol::try_new("abc_123");
243 assert!(ok.is_ok());
244 assert_eq!(ok.unwrap().as_str(), "abc_123");
245
246 let err = Symbol::try_new("-bad");
247 assert!(err.is_err());
248 }
249
250 #[test]
251 fn display_and_deref_expose_inner() {
252 let s = Symbol::try_new("abc_123").unwrap();
253 assert_eq!(&*s, "abc_123"); assert_eq!(s.as_str(), "abc_123");
255 assert_eq!(s.to_string(), "abc_123");
256 }
257
258 #[test]
259 fn from_str_and_try_from_work() {
260 let s1 = Symbol::from_str("name1").unwrap();
261 assert_eq!(s1, "name1");
262
263 let s2: Result<Symbol, _> = "x_y".try_into();
264 assert_eq!(s2.unwrap(), "x_y");
265
266 let s3: Result<Symbol, _> = String::from("ok_2").try_into();
267 assert_eq!(s3.unwrap(), "ok_2");
268
269 let bad: Result<Symbol, _> = "Nope".try_into();
270 assert!(bad.is_err());
271 }
272
273 #[test]
274 fn error_display_message_matches_spec() {
275 let err = Symbol::try_new("Bad-Name").unwrap_err();
276 assert_eq!(err.to_string(), "value must match ^[a-z][a-z0-9_]*$");
277 }
278
279 #[test]
280 fn equality_and_hash_semantics() {
281 use std::collections::HashSet;
282 let a = Symbol::try_new("abc").unwrap();
283 let b = Symbol::try_new("abc").unwrap();
284 let c = Symbol::try_new("abd").unwrap();
285
286 assert_eq!(a, b);
287 assert_ne!(a, c);
288
289 let mut set = HashSet::new();
290 set.insert(a);
291 assert!(set.contains(&b));
292 assert!(!set.contains(&c));
293 assert!(set.contains("abc"));
295 assert!(!set.contains("abd"));
296 }
297
298 #[test]
299 fn as_ref_borrow_and_hashmap_lookup() {
300 use std::collections::HashMap;
301 let key = Symbol::try_new("alpha").unwrap();
302 let mut map = HashMap::new();
303 map.insert(key.clone(), 42);
304 assert_eq!(map.get("alpha"), Some(&42));
306
307 fn takes_as_ref<S: AsRef<str>>(s: S) -> usize { s.as_ref().len() }
309 assert_eq!(takes_as_ref(&key), 5);
310 }
311
312 #[test]
313 fn ordering_and_btreeset() {
314 use std::collections::BTreeSet;
315 let inputs = ["beta", "alpha", "alpha_1", "alpha0"];
316 let mut syms: Vec<Symbol> = inputs.iter().map(|s| Symbol::try_new(*s).unwrap()).collect();
317 syms.sort(); let sorted: Vec<&str> = syms.iter().map(|s| s.as_str()).collect();
319 assert_eq!(sorted, vec!["alpha", "alpha0", "alpha_1", "beta"]);
320
321 let set: BTreeSet<Symbol> = syms.into_iter().collect();
322 let ordered: Vec<&str> = set.iter().map(|s| s.as_str()).collect();
323 assert_eq!(ordered, vec!["alpha", "alpha0", "alpha_1", "beta"]);
324 }
325
326 #[test]
327 fn into_string_and_boxed_str() {
328 let s = Symbol::try_new("gamma").unwrap();
329 let owned: String = s.clone().into();
330 assert_eq!(owned, "gamma");
331 let boxed: Box<str> = s.clone().into();
332 assert_eq!(&*boxed, "gamma");
333 }
334
335 #[test]
336 fn cross_type_equality() {
337 let s = Symbol::try_new("delta_1").unwrap();
338 assert!(s == "delta_1");
339 assert!("delta_1" == s);
340 assert!(String::from("delta_1") == s);
341 assert!(s == String::from("delta_1"));
342 assert!(s != "delta2");
343 }
344}