bones_utils/
labeled_id.rs1use std::str::FromStr;
2
3use ulid::Ulid;
4
5use crate::UlidExt;
6
7#[derive(Hash, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
12pub struct LabeledId {
13 prefix: Option<[u8; 63]>,
15 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#[derive(Debug)]
27pub enum LabeledIdCreateError {
28 PrefixTooLong,
30 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 pub fn new(prefix: &str) -> Result<Self, LabeledIdCreateError> {
50 Self::new_with_ulid(prefix, Ulid::create())
51 }
52
53 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 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 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#[derive(Debug)]
119pub enum LabledIdParseError {
120 InvalidFormat,
122 UlidDecode(ulid::DecodeError),
124 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}