hr_id/
lib.rs

1//! A human-readable ID which is safe to use as a component in a URI path.
2//! and supports constant [`Label`]s.
3//!
4//! Features:
5//!  - `hash`: enable support for [`async-hash`](https://docs.rs/async-hash)
6//!  - `serde`: enable support for [`serde`](https://docs.rs/serde)
7//!  - `stream`: enable support for [`destream`](https://docs.rs/destream)
8//!  - `uuid`: enable support for [`uuid`](https://docs.rs/uuid)
9//!
10//! Example:
11//! ```
12//! # use std::str::FromStr;
13//! use hr_id::{label, Id, Label};
14//!
15//! const HELLO: Label = label("hello"); // unchecked!
16//! let world: Id = "world".parse().expect("id");
17//!
18//! assert_eq!(format!("{}, {}!", HELLO, world), "hello, world!");
19//! assert_eq!(Id::from(HELLO), "hello");
20//! assert!(Id::from_str("this string has whitespace").is_err());
21//! ```
22
23use std::borrow::Borrow;
24use std::cmp::Ordering;
25use std::fmt;
26use std::mem::size_of;
27use std::ops::Deref;
28use std::str::FromStr;
29use std::sync::Arc;
30
31use derive_more::*;
32use get_size::GetSize;
33use regex::Regex;
34use safecast::TryCastFrom;
35
36#[cfg(feature = "stream")]
37mod destream;
38#[cfg(feature = "hash")]
39mod hash;
40#[cfg(feature = "serde")]
41mod serde;
42
43/// A set of prohibited character patterns.
44pub const RESERVED_CHARS: [&str; 21] = [
45    "/", "..", "~", "$", "`", "&", "|", "=", "^", "{", "}", "<", ">", "'", "\"", "?", ":", "@",
46    "#", "(", ")",
47];
48
49/// An error encountered while parsing an [`Id`].
50#[derive(Debug, Display)]
51#[display("{}", msg)]
52pub struct ParseError {
53    msg: Arc<str>,
54}
55
56impl std::error::Error for ParseError {}
57
58impl From<String> for ParseError {
59    fn from(msg: String) -> Self {
60        Self { msg: msg.into() }
61    }
62}
63
64impl From<&str> for ParseError {
65    fn from(msg: &str) -> Self {
66        Self { msg: msg.into() }
67    }
68}
69
70/// A static label which implements `Into<Id>`.
71#[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq)]
72pub struct Label {
73    id: &'static str,
74}
75
76impl Deref for Label {
77    type Target = str;
78
79    fn deref(&self) -> &Self::Target {
80        self.id
81    }
82}
83
84impl From<Label> for Id {
85    fn from(l: Label) -> Id {
86        Id { inner: l.id.into() }
87    }
88}
89
90impl PartialEq<Id> for Label {
91    fn eq(&self, other: &Id) -> bool {
92        self.id == other.as_str()
93    }
94}
95
96impl fmt::Display for Label {
97    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
98        f.write_str(self.id)
99    }
100}
101
102/// Return a [`Label`] with the given static `str`.
103pub const fn label(id: &'static str) -> Label {
104    Label { id }
105}
106
107/// A human-readable ID
108#[derive(Clone, Eq, Hash, PartialEq, Ord, PartialOrd)]
109pub struct Id {
110    inner: Arc<str>,
111}
112
113impl Id {
114    /// Borrows the String underlying this [`Id`].
115    #[inline]
116    pub fn as_str(&self) -> &str {
117        self.inner.as_ref()
118    }
119
120    /// Destructure this [`Id`] into its inner `Arc<str>`.
121    pub fn into_inner(self) -> Arc<str> {
122        self.inner
123    }
124
125    /// Return true if this [`Id`] begins with the specified string.
126    pub fn starts_with(&self, prefix: &str) -> bool {
127        self.inner.starts_with(prefix)
128    }
129}
130
131impl GetSize for Id {
132    fn get_size(&self) -> usize {
133        // err on the side of caution in case there is only one reference to this Id
134        size_of::<Arc<str>>() + self.inner.as_bytes().len()
135    }
136}
137
138#[cfg(feature = "uuid")]
139impl From<uuid::Uuid> for Id {
140    fn from(id: uuid::Uuid) -> Self {
141        Self {
142            inner: id.to_string().into(),
143        }
144    }
145}
146
147impl Borrow<str> for Id {
148    fn borrow(&self) -> &str {
149        &self.inner
150    }
151}
152
153impl PartialEq<String> for Id {
154    fn eq(&self, other: &String) -> bool {
155        self.inner.as_ref() == other.as_str()
156    }
157}
158
159impl PartialEq<str> for Id {
160    fn eq(&self, other: &str) -> bool {
161        self.inner.as_ref() == other
162    }
163}
164
165impl<'a> PartialEq<&'a str> for Id {
166    fn eq(&self, other: &&'a str) -> bool {
167        self.inner.as_ref() == *other
168    }
169}
170
171impl PartialEq<Label> for Id {
172    fn eq(&self, other: &Label) -> bool {
173        self.inner.as_ref() == other.id
174    }
175}
176
177impl PartialEq<Id> for &str {
178    fn eq(&self, other: &Id) -> bool {
179        *self == other.inner.as_ref()
180    }
181}
182
183impl PartialOrd<String> for Id {
184    fn partial_cmp(&self, other: &String) -> Option<Ordering> {
185        self.inner.as_ref().partial_cmp(other.as_str())
186    }
187}
188
189impl PartialOrd<str> for Id {
190    fn partial_cmp(&self, other: &str) -> Option<Ordering> {
191        self.inner.as_ref().partial_cmp(other)
192    }
193}
194
195impl<'a> PartialOrd<&'a str> for Id {
196    fn partial_cmp(&self, other: &&'a str) -> Option<Ordering> {
197        self.inner.as_ref().partial_cmp(*other)
198    }
199}
200
201impl From<usize> for Id {
202    fn from(u: usize) -> Id {
203        u.to_string().parse().expect("usize")
204    }
205}
206
207impl From<u64> for Id {
208    fn from(i: u64) -> Id {
209        i.to_string().parse().expect("64-bit unsigned int")
210    }
211}
212
213impl FromStr for Id {
214    type Err = ParseError;
215
216    fn from_str(id: &str) -> Result<Self, Self::Err> {
217        validate_id(id)?;
218
219        Ok(Id { inner: id.into() })
220    }
221}
222
223impl TryCastFrom<String> for Id {
224    fn can_cast_from(id: &String) -> bool {
225        validate_id(id).is_ok()
226    }
227
228    fn opt_cast_from(id: String) -> Option<Id> {
229        id.parse().ok()
230    }
231}
232
233impl TryCastFrom<Id> for usize {
234    fn can_cast_from(id: &Id) -> bool {
235        id.as_str().parse::<usize>().is_ok()
236    }
237
238    fn opt_cast_from(id: Id) -> Option<usize> {
239        id.as_str().parse::<usize>().ok()
240    }
241}
242
243impl fmt::Debug for Id {
244    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
245        f.write_str(&self.inner)
246    }
247}
248
249impl fmt::Display for Id {
250    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
251        f.write_str(&self.inner)
252    }
253}
254
255fn validate_id(id: &str) -> Result<(), ParseError> {
256    if id.is_empty() {
257        return Err("cannot construct an empty Id".into());
258    }
259
260    let mut invalid_chars = id.chars().filter(|c| (*c as u8) < 32u8);
261    if let Some(invalid) = invalid_chars.next() {
262        return Err(format!(
263            "Id {} contains ASCII control characters {}",
264            id, invalid as u8,
265        )
266        .into());
267    }
268
269    for pattern in &RESERVED_CHARS {
270        if id.contains(pattern) {
271            return Err(format!("Id {} contains disallowed pattern {}", id, pattern).into());
272        }
273    }
274
275    if let Some(w) = Regex::new(r"\s").expect("whitespace regex").find(id) {
276        return Err(format!("Id {} is not allowed to contain whitespace {:?}", id, w).into());
277    }
278
279    Ok(())
280}