pickems/datatypes/
name.rs1use std::{cmp::Ordering, hash::Hash};
2
3use serde::{Deserialize, Deserializer, Serialize, Serializer};
4
5#[derive(Debug, Clone)]
10pub struct Name(String);
11
12impl Name {
13 pub fn try_new(name: impl AsRef<str>) -> anyhow::Result<Self> {
14 let name = name.as_ref().trim();
15
16 if name.is_empty() {
17 anyhow::bail!("invalid name: cannot be empty");
18 }
19
20 if name.chars().count() > 30 {
21 anyhow::bail!("invalid name: cannot be longer than 30 characters");
22 }
23
24 Ok(Self(name.to_string()))
25 }
26
27 pub fn new(name: impl AsRef<str>) -> Self {
28 Self::try_new(name).unwrap()
29 }
30
31 #[inline]
34 #[must_use]
35 pub const unsafe fn new_unchecked(name: String) -> Self {
36 Self(name)
37 }
38}
39
40impl std::fmt::Display for Name {
41 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42 self.0.fmt(f)
43 }
44}
45
46impl Serialize for Name {
47 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
48 serializer.serialize_str(&self.0)
49 }
50}
51
52impl<'de> Deserialize<'de> for Name {
53 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
54 Self::try_new(String::deserialize(deserializer)?).map_err(serde::de::Error::custom)
55 }
56}
57
58impl PartialEq for Name {
60 fn eq(&self, other: &Self) -> bool {
61 self.0.eq_ignore_ascii_case(&other.0)
62 }
63}
64
65impl Eq for Name {}
66
67impl Ord for Name {
69 fn cmp(&self, other: &Self) -> Ordering {
70 self.0
71 .chars()
72 .map(|c| c.to_ascii_lowercase())
73 .cmp(other.0.chars().map(|c| c.to_ascii_lowercase()))
74 }
75}
76
77impl PartialOrd for Name {
78 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
79 Some(self.cmp(other))
80 }
81}
82
83impl Hash for Name {
85 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
86 for c in self.0.chars() {
87 c.to_ascii_lowercase().hash(state);
88 }
89 }
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95
96 #[test]
97 fn validates_and_trims_names() {
98 assert_eq!(Name::new(" Vitality ").to_string(), "Vitality");
99 assert!(Name::try_new(" ").is_err());
100 assert!(Name::try_new("1234567890123456789012345678901").is_err());
101 }
102
103 #[test]
104 fn compares_and_hashes_case_insensitively() {
105 use std::collections::{BTreeSet, HashSet};
106
107 let name = Name::new("NAVI");
108 let lowercase = Name::new("navi");
109
110 assert_eq!(name, lowercase);
111
112 let mut hash_set = HashSet::new();
113 hash_set.insert(name.clone());
114 assert!(hash_set.contains(&lowercase));
115
116 let mut tree_set = BTreeSet::new();
117 tree_set.insert(name);
118 tree_set.insert(lowercase);
119 assert_eq!(tree_set.len(), 1);
120 }
121}