bones_utils/
labeled_id.rs

1use std::str::FromStr;
2
3use ulid::Ulid;
4
5use crate::UlidExt;
6
7/// A [`Ulid`] with a human-readable ascii prefix.
8///
9/// This is essentially like a [TypeId](https://github.com/jetpack-io/typeid), but the prefix can be
10/// any ascii string instead of only ascii lowercase.
11#[derive(Hash, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
12pub struct LabeledId {
13    /// The prefix
14    prefix: Option<[u8; 63]>,
15    /// The ULID.
16    ulid: Ulid,
17}
18
19impl std::fmt::Debug for LabeledId {
20    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
21        write!(f, "LabeledId({self})")
22    }
23}
24
25/// Error creating a [`LabeledId`].
26#[derive(Debug)]
27pub enum LabeledIdCreateError {
28    /// The prefix was too long ( greater than 63 chars ).
29    PrefixTooLong,
30    /// The prefix was not ASCII.
31    PrefixNotAscii,
32}
33
34impl std::error::Error for LabeledIdCreateError {}
35impl std::fmt::Display for LabeledIdCreateError {
36    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37        match self {
38            LabeledIdCreateError::PrefixTooLong => write!(
39                f,
40                "Labled ID prefix is too long ( maxumum length is 63 chars )."
41            ),
42            LabeledIdCreateError::PrefixNotAscii => write!(f, "Labeled ID prefix is not ASCII"),
43        }
44    }
45}
46
47impl LabeledId {
48    /// Create a new labeled ID with the given prefix.
49    pub fn new(prefix: &str) -> Result<Self, LabeledIdCreateError> {
50        Self::new_with_ulid(prefix, Ulid::create())
51    }
52
53    /// Create a new labeled ID with the given prefix and ULID.
54    pub fn new_with_ulid(prefix: &str, ulid: Ulid) -> Result<Self, LabeledIdCreateError> {
55        if prefix.is_empty() {
56            Ok(Self { prefix: None, ulid })
57        } else if prefix.len() > 63 {
58            Err(LabeledIdCreateError::PrefixTooLong)
59        } else if !prefix.is_ascii() {
60            Err(LabeledIdCreateError::PrefixNotAscii)
61        } else {
62            let mut prefix_bytes = [0; 63];
63            prefix_bytes[0..prefix.len()].copy_from_slice(prefix.as_bytes());
64
65            Ok(Self {
66                prefix: Some(prefix_bytes),
67                ulid,
68            })
69        }
70    }
71
72    /// Get the prefix of the ID.
73    pub fn prefix(&self) -> &str {
74        self.prefix
75            .as_ref()
76            .map(|x| {
77                let prefix_len = Self::prefix_len(x);
78                let bytes = &x[0..prefix_len];
79                std::str::from_utf8(bytes).unwrap()
80            })
81            .unwrap_or("")
82    }
83
84    /// Get the [`Ulid`] of the ID.
85    pub fn ulid(&self) -> Ulid {
86        self.ulid
87    }
88
89    fn prefix_len(prefix: &[u8; 63]) -> usize {
90        let mut len = 0;
91        while prefix[len] != 0 {
92            len += 1;
93        }
94        len
95    }
96}
97
98impl std::fmt::Display for LabeledId {
99    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
100        if let Some(prefix) = &self.prefix {
101            if !prefix.is_ascii() {
102                return Err(std::fmt::Error);
103            }
104            let prefix_len = Self::prefix_len(prefix);
105            write!(
106                f,
107                "{}_{}",
108                String::from_utf8(prefix[0..prefix_len].into()).unwrap(),
109                self.ulid
110            )
111        } else {
112            write!(f, "{}", self.ulid)
113        }
114    }
115}
116
117/// Errors that can happen while parsing a [`LabeledId`].
118#[derive(Debug)]
119pub enum LabledIdParseError {
120    /// The ID is in the wrong format.
121    InvalidFormat,
122    /// The ULID could not be parsed.
123    UlidDecode(ulid::DecodeError),
124    /// Error creating ID
125    CreateError(LabeledIdCreateError),
126}
127
128impl std::fmt::Display for LabledIdParseError {
129    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
130        match self {
131            LabledIdParseError::InvalidFormat => {
132                write!(f, "The Labeled ID is in the wrong format.")
133            }
134            LabledIdParseError::UlidDecode(e) => write!(f, "Error decoding ULID: {e}"),
135            LabledIdParseError::CreateError(e) => write!(f, "Error creating LabeledId: {e}"),
136        }
137    }
138}
139
140impl FromStr for LabeledId {
141    type Err = LabledIdParseError;
142
143    fn from_str(s: &str) -> Result<Self, Self::Err> {
144        use LabledIdParseError::*;
145        if let Some((prefix, ulid_text)) = s.rsplit_once('_') {
146            let ulid = Ulid::from_str(ulid_text).map_err(UlidDecode)?;
147            LabeledId::new_with_ulid(prefix, ulid).map_err(CreateError)
148        } else {
149            let ulid = Ulid::from_str(s).map_err(UlidDecode)?;
150            Ok(LabeledId { prefix: None, ulid })
151        }
152    }
153}
154
155#[cfg(feature = "serde")]
156mod ser_de {
157    use super::*;
158    use serde::{Deserialize, Serialize};
159
160    impl Serialize for LabeledId {
161        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
162        where
163            S: serde::Serializer,
164        {
165            serializer.serialize_str(&self.to_string())
166        }
167    }
168
169    impl<'de> Deserialize<'de> for LabeledId {
170        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
171        where
172            D: serde::Deserializer<'de>,
173        {
174            use serde::de::Error;
175            let s = String::deserialize(deserializer)?;
176            s.parse().map_err(|e| D::Error::custom(format!("{e}")))
177        }
178    }
179}
180
181#[cfg(test)]
182mod test {
183
184    #[cfg(not(miri))]
185    #[test]
186    fn smoke() {
187        use crate::LabeledId;
188
189        let id = LabeledId::new("asset").unwrap();
190        let parsed: LabeledId = id.to_string().parse().unwrap();
191
192        assert_eq!(id, parsed)
193    }
194}