Skip to main content

pubhubs/
handle.rs

1//! [`Handle`]s for PubHubs objects like [hub](crate::hub::BasicInfo)s and
2//! [attribute types](crate::attr::Type).
3use std::cell::OnceCell;
4
5/// A handle used to refer to hubs, attributes, etc. - a string that matches [`HANDLE_REGEX`].
6///
7/// The default handle is `_`, and is sometimes used as a placeholder.
8#[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/// When a handle does not match [HANDLE_REGEX].
36#[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    /// Returns the undelying string.
50    ///
51    /// Has the sam effect as `deref`, but `as_str` is more readable.
52    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: &regex::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: &regex::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
95/// The regex pattern for a hub handle
96pub const HANDLE_REGEX: &str = r"^[a-z0-9_]+$";
97
98thread_local! {
99    /// Thread local compiled version of [HANDLE_REGEX]
100    static HANDLE_REGEX_TLK: OnceCell<regex::Regex> = const { OnceCell::new() };
101}
102
103/// Runs `f` with as argument a reference to a compiled [HANDLE_REGEX]
104/// that is cached thread locally.
105pub fn with_handle_regex<R>(f: impl FnOnce(&regex::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")]
114// We use serde(remote... to check the invariant handles.len()>0, see
115//   https://github.com/serde-rs/serde/issues/1220
116pub 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}