Skip to main content

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