1use std::cell::OnceCell;
4
5#[derive(Clone, PartialEq, Eq, Hash, Debug)]
9pub struct Handle {
10 inner: String,
11}
12
13impl Default for Handle {
14 fn default() -> Self {
15 Handle {
16 inner: "_".to_string(),
17 }
18 }
19}
20
21impl serde::Serialize for Handle {
22 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
23 s.collect_str(self)
24 }
25}
26
27impl<'de> serde::Deserialize<'de> for Handle {
28 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
29 String::deserialize(d)?
30 .parse()
31 .map_err(serde::de::Error::custom)
32 }
33}
34
35#[derive(thiserror::Error, Debug)]
37#[error("a handle must be a non-empty string of lower-case alphanumeric characters and underscore")]
38pub struct HubHandleError();
39
40impl std::ops::Deref for Handle {
41 type Target = str;
42
43 fn deref(&self) -> &Self::Target {
44 &self.inner
45 }
46}
47
48impl Handle {
49 pub fn as_str(&self) -> &str {
53 &self.inner
54 }
55}
56
57impl TryFrom<String> for Handle {
58 type Error = HubHandleError;
59
60 fn try_from(s: String) -> Result<Self, Self::Error> {
61 if !with_handle_regex(|r: ®ex::Regex| r.is_match(&s)) {
62 return Err(HubHandleError());
63 }
64
65 Ok(Handle { inner: s })
66 }
67}
68
69impl core::str::FromStr for Handle {
70 type Err = HubHandleError;
71
72 fn from_str(s: &str) -> Result<Handle, Self::Err> {
73 if !with_handle_regex(|r: ®ex::Regex| r.is_match(s)) {
74 return Err(HubHandleError());
75 }
76
77 Ok(Handle {
78 inner: s.to_string(),
79 })
80 }
81}
82
83impl std::fmt::Display for Handle {
84 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
85 write!(f, "{}", self.inner)
86 }
87}
88
89impl From<Handle> for String {
90 fn from(n: Handle) -> Self {
91 n.inner
92 }
93}
94
95pub const HANDLE_REGEX: &str = r"^[a-z0-9_]+$";
97
98thread_local! {
99 static HANDLE_REGEX_TLK: OnceCell<regex::Regex> = const { OnceCell::new() };
101}
102
103pub fn with_handle_regex<R>(f: impl FnOnce(®ex::Regex) -> R) -> R {
106 HANDLE_REGEX_TLK.with(|oc: &OnceCell<regex::Regex>| {
107 f(oc.get_or_init(|| regex::Regex::new(HANDLE_REGEX).unwrap()))
108 })
109}
110
111#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, PartialEq, Eq)]
112#[serde(transparent)]
113#[serde(remote = "Self")]
114pub struct Handles {
117 inner: Vec<Handle>,
118}
119
120impl Handles {
121 pub fn preferred(&self) -> &Handle {
122 &self.inner[0]
123 }
124}
125
126impl std::ops::Deref for Handles {
127 type Target = [Handle];
128
129 fn deref(&self) -> &[Handle] {
130 &self.inner
131 }
132}
133
134impl From<Vec<Handle>> for Handles {
135 fn from(handles: Vec<Handle>) -> Handles {
136 Self { inner: handles }
137 }
138}
139
140impl<'de> serde::Deserialize<'de> for Handles {
141 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
142 where
143 D: serde::Deserializer<'de>,
144 {
145 let unchecked = Self::deserialize(deserializer)?;
146 if unchecked.inner.is_empty() {
147 return Err(serde::de::Error::custom("must have at least one handle"));
148 }
149 Ok(unchecked)
150 }
151}
152
153impl serde::Serialize for Handles {
154 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
155 where
156 S: serde::Serializer,
157 {
158 Self::serialize(self, serializer)
159 }
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165
166 #[test]
167 fn test_hub_handle() {
168 assert!(Handle::try_from("no_Capical".to_string()).is_err());
169 assert!(Handle::try_from("no space".to_string()).is_err());
170 assert!(Handle::try_from("no_ümlaut".to_string()).is_err());
171 assert!(Handle::try_from("this_is_fine".to_string()).is_ok());
172 assert!(Handle::try_from("th1s_t00".to_string()).is_ok());
173 assert!(Handle::try_from("".to_string()).is_err());
174 }
175}