1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, marker::PhantomData};
7
8pub mod prelude;
9
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub enum IdPrefixError {
12 EmptyPrefix,
13 InvalidPrefixCharacter {
14 character: char,
15 index: usize,
16 },
17 EmptyValue,
18 InvalidValueCharacter {
19 character: char,
20 index: usize,
21 },
22 MissingSeparator,
23 MismatchedPrefix {
24 expected: &'static str,
25 actual: String,
26 },
27}
28
29impl fmt::Display for IdPrefixError {
30 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
31 match self {
32 Self::EmptyPrefix => formatter.write_str("prefix cannot be empty"),
33 Self::InvalidPrefixCharacter { character, index } => {
34 write!(
35 formatter,
36 "invalid prefix character `{character}` at byte {index}"
37 )
38 },
39 Self::EmptyValue => formatter.write_str("identifier value cannot be empty"),
40 Self::InvalidValueCharacter { character, index } => {
41 write!(
42 formatter,
43 "invalid value character `{character}` at byte {index}"
44 )
45 },
46 Self::MissingSeparator => formatter.write_str("expected a `_` separator"),
47 Self::MismatchedPrefix { expected, actual } => {
48 write!(
49 formatter,
50 "expected prefix `{expected}` but found `{actual}`"
51 )
52 },
53 }
54 }
55}
56
57impl std::error::Error for IdPrefixError {}
58
59#[must_use]
60pub fn normalize_prefix(input: &str) -> String {
61 input.trim().to_ascii_lowercase()
62}
63
64pub fn validate_prefix(input: &str) -> Result<(), IdPrefixError> {
71 if input.is_empty() {
72 return Err(IdPrefixError::EmptyPrefix);
73 }
74
75 for (index, character) in input.char_indices() {
76 let is_valid = if index == 0 {
77 character.is_ascii_lowercase()
78 } else {
79 character.is_ascii_lowercase() || character.is_ascii_digit()
80 };
81
82 if !is_valid {
83 return Err(IdPrefixError::InvalidPrefixCharacter { character, index });
84 }
85 }
86
87 Ok(())
88}
89
90#[must_use]
91pub fn is_valid_prefix(input: &str) -> bool {
92 validate_prefix(&normalize_prefix(input)).is_ok()
93}
94
95fn validate_value(input: &str) -> Result<(), IdPrefixError> {
96 if input.is_empty() {
97 return Err(IdPrefixError::EmptyValue);
98 }
99
100 for (index, character) in input.char_indices() {
101 if !(character.is_ascii_alphanumeric() || matches!(character, '_' | '-' | '.')) {
102 return Err(IdPrefixError::InvalidValueCharacter { character, index });
103 }
104 }
105
106 Ok(())
107}
108
109#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
110pub struct IdPrefix(String);
111
112impl IdPrefix {
113 pub fn new(input: &str) -> Result<Self, IdPrefixError> {
120 let normalized = normalize_prefix(input);
121 validate_prefix(&normalized)?;
122 Ok(Self(normalized))
123 }
124
125 #[must_use]
126 pub fn as_str(&self) -> &str {
127 &self.0
128 }
129}
130
131impl fmt::Display for IdPrefix {
132 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
133 formatter.write_str(self.as_str())
134 }
135}
136
137pub fn format_prefixed_id(prefix: &IdPrefix, value: &str) -> Result<String, IdPrefixError> {
144 let normalized = value.trim();
145 validate_value(normalized)?;
146 Ok(format!("{}_{}", prefix.as_str(), normalized))
147}
148
149#[derive(Debug, Clone, PartialEq, Eq, Hash)]
150pub struct PrefixedId {
151 prefix: IdPrefix,
152 value: String,
153}
154
155impl PrefixedId {
156 pub fn new(prefix: IdPrefix, value: &str) -> Result<Self, IdPrefixError> {
163 let normalized = value.trim().to_owned();
164 validate_value(&normalized)?;
165 Ok(Self {
166 prefix,
167 value: normalized,
168 })
169 }
170
171 pub fn parse(input: &str) -> Result<Self, IdPrefixError> {
178 let Some((prefix, value)) = input.split_once('_') else {
179 return Err(IdPrefixError::MissingSeparator);
180 };
181
182 Self::new(IdPrefix::new(prefix)?, value)
183 }
184
185 #[must_use]
186 pub const fn prefix(&self) -> &IdPrefix {
187 &self.prefix
188 }
189
190 #[must_use]
191 pub fn value(&self) -> &str {
192 &self.value
193 }
194
195 #[must_use]
196 pub fn into_parts(self) -> (IdPrefix, String) {
197 (self.prefix, self.value)
198 }
199}
200
201impl fmt::Display for PrefixedId {
202 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
203 formatter.write_str(&format!("{}_{}", self.prefix, self.value))
204 }
205}
206
207pub trait PrefixedIdentifierKind {
208 const PREFIX: &'static str;
209}
210
211#[derive(Debug, Clone, PartialEq, Eq, Hash)]
212pub struct TypedPrefixedId<K> {
213 prefixed_id: PrefixedId,
214 marker: PhantomData<fn() -> K>,
215}
216
217impl<K> TypedPrefixedId<K> {
218 #[must_use]
219 pub const fn as_prefixed_id(&self) -> &PrefixedId {
220 &self.prefixed_id
221 }
222
223 #[must_use]
224 pub fn value(&self) -> &str {
225 self.prefixed_id.value()
226 }
227}
228
229impl<K: PrefixedIdentifierKind> TypedPrefixedId<K> {
230 pub fn new(value: &str) -> Result<Self, IdPrefixError> {
236 let prefixed_id = PrefixedId::new(IdPrefix::new(K::PREFIX)?, value)?;
237 Ok(Self {
238 prefixed_id,
239 marker: PhantomData,
240 })
241 }
242
243 pub fn parse(input: &str) -> Result<Self, IdPrefixError> {
250 let prefixed_id = PrefixedId::parse(input)?;
251
252 if prefixed_id.prefix().as_str() != K::PREFIX {
253 return Err(IdPrefixError::MismatchedPrefix {
254 expected: K::PREFIX,
255 actual: prefixed_id.prefix().as_str().to_owned(),
256 });
257 }
258
259 Ok(Self {
260 prefixed_id,
261 marker: PhantomData,
262 })
263 }
264
265 #[must_use]
266 pub const fn prefix(&self) -> &'static str {
267 K::PREFIX
268 }
269}
270
271impl<K> fmt::Display for TypedPrefixedId<K> {
272 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
273 self.prefixed_id.fmt(formatter)
274 }
275}
276
277#[cfg(test)]
278mod tests {
279 use super::{IdPrefixError, PrefixedId, PrefixedIdentifierKind, TypedPrefixedId};
280
281 struct User;
282
283 impl PrefixedIdentifierKind for User {
284 const PREFIX: &'static str = "usr";
285 }
286
287 #[test]
288 fn parses_prefixed_ids() -> Result<(), IdPrefixError> {
289 let prefixed = PrefixedId::parse("usr_123")?;
290 assert_eq!(prefixed.prefix().as_str(), "usr");
291 assert_eq!(prefixed.value(), "123");
292 Ok(())
293 }
294
295 #[test]
296 fn typed_ids_enforce_the_expected_prefix() -> Result<(), IdPrefixError> {
297 let typed = TypedPrefixedId::<User>::parse("usr_123")?;
298 assert_eq!(typed.prefix(), "usr");
299 Ok(())
300 }
301}