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 IdentifierError {
12 Empty,
13 InvalidCharacter { character: char, index: usize },
14}
15
16impl fmt::Display for IdentifierError {
17 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
18 match self {
19 Self::Empty => formatter.write_str("identifier cannot be empty"),
20 Self::InvalidCharacter { character, index } => {
21 write!(
22 formatter,
23 "invalid identifier character `{character}` at byte {index}"
24 )
25 },
26 }
27 }
28}
29
30impl std::error::Error for IdentifierError {}
31
32#[must_use]
33pub fn normalize_identifier(input: &str) -> String {
34 input.trim().to_owned()
35}
36
37pub fn validate_identifier(input: &str) -> Result<(), IdentifierError> {
44 if input.is_empty() {
45 return Err(IdentifierError::Empty);
46 }
47
48 for (index, character) in input.char_indices() {
49 if !(character.is_ascii_alphanumeric() || matches!(character, '_' | '-' | '.')) {
50 return Err(IdentifierError::InvalidCharacter { character, index });
51 }
52 }
53
54 Ok(())
55}
56
57#[must_use]
58pub fn is_valid_identifier(input: &str) -> bool {
59 validate_identifier(input).is_ok()
60}
61
62#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
63pub struct Identifier(String);
64
65impl Identifier {
66 pub fn new(input: &str) -> Result<Self, IdentifierError> {
73 let normalized = normalize_identifier(input);
74 validate_identifier(&normalized)?;
75 Ok(Self(normalized))
76 }
77
78 #[must_use]
79 pub fn as_str(&self) -> &str {
80 &self.0
81 }
82
83 #[must_use]
84 pub fn into_inner(self) -> String {
85 self.0
86 }
87}
88
89impl fmt::Display for Identifier {
90 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
91 formatter.write_str(self.as_str())
92 }
93}
94
95pub trait IdentifierKind {
96 const NAME: &'static str;
97}
98
99#[derive(Debug, Clone, PartialEq, Eq, Hash)]
100pub struct TypedIdentifier<K> {
101 identifier: Identifier,
102 marker: PhantomData<fn() -> K>,
103}
104
105impl<K> TypedIdentifier<K> {
106 #[must_use]
107 pub const fn from_identifier(identifier: Identifier) -> Self {
108 Self {
109 identifier,
110 marker: PhantomData,
111 }
112 }
113
114 #[must_use]
115 pub const fn as_identifier(&self) -> &Identifier {
116 &self.identifier
117 }
118
119 #[must_use]
120 pub fn as_str(&self) -> &str {
121 self.identifier.as_str()
122 }
123
124 #[must_use]
125 pub fn into_inner(self) -> Identifier {
126 self.identifier
127 }
128}
129
130impl<K: IdentifierKind> TypedIdentifier<K> {
131 pub fn new(input: &str) -> Result<Self, IdentifierError> {
138 Identifier::new(input).map(Self::from_identifier)
139 }
140
141 #[must_use]
142 pub const fn kind_name(&self) -> &'static str {
143 K::NAME
144 }
145}
146
147impl<K> fmt::Display for TypedIdentifier<K> {
148 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
149 self.identifier.fmt(formatter)
150 }
151}
152
153#[cfg(test)]
154mod tests {
155 use super::{Identifier, IdentifierError, IdentifierKind, TypedIdentifier};
156
157 struct User;
158
159 impl IdentifierKind for User {
160 const NAME: &'static str = "user";
161 }
162
163 #[test]
164 fn trims_and_preserves_valid_identifiers() -> Result<(), IdentifierError> {
165 let identifier = Identifier::new(" acct_42 ")?;
166 assert_eq!(identifier.as_str(), "acct_42");
167 Ok(())
168 }
169
170 #[test]
171 fn rejects_invalid_characters() {
172 assert_eq!(
173 Identifier::new("bad value"),
174 Err(IdentifierError::InvalidCharacter {
175 character: ' ',
176 index: 3,
177 })
178 );
179 }
180
181 #[test]
182 fn typed_identifier_carries_kind_name() -> Result<(), IdentifierError> {
183 let typed = TypedIdentifier::<User>::new("usr_7")?;
184 assert_eq!(typed.kind_name(), "user");
185 Ok(())
186 }
187}