1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7#[derive(Clone, Copy, Debug, Eq, PartialEq)]
9pub enum GoIdentifierError {
10 Empty,
11 InvalidStart { character: char },
12 InvalidContinue { index: usize, character: char },
13 NotExported,
14 NotUnexported,
15}
16
17impl fmt::Display for GoIdentifierError {
18 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
19 match self {
20 Self::Empty => formatter.write_str("Go identifier cannot be empty"),
21 Self::InvalidStart { character } => {
22 write!(formatter, "invalid Go identifier start `{character}`")
23 }
24 Self::InvalidContinue { index, character } => write!(
25 formatter,
26 "invalid Go identifier continuation `{character}` at byte index {index}"
27 ),
28 Self::NotExported => formatter.write_str("Go identifier is not exported"),
29 Self::NotUnexported => formatter.write_str("Go identifier is not unexported"),
30 }
31 }
32}
33
34impl Error for GoIdentifierError {}
35
36#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
38pub struct GoIdentifier(String);
39
40impl GoIdentifier {
41 pub fn new(value: impl Into<String>) -> Result<Self, GoIdentifierError> {
47 let value = value.into();
48 validate_ascii_go_identifier(&value)?;
49 Ok(Self(value))
50 }
51
52 #[must_use]
54 pub fn as_str(&self) -> &str {
55 &self.0
56 }
57
58 #[must_use]
60 pub fn into_string(self) -> String {
61 self.0
62 }
63
64 #[must_use]
66 pub fn is_exported(&self) -> bool {
67 is_exported_go_identifier(self.as_str())
68 }
69
70 #[must_use]
72 pub fn is_unexported(&self) -> bool {
73 is_unexported_go_identifier(self.as_str())
74 }
75}
76
77impl AsRef<str> for GoIdentifier {
78 fn as_ref(&self) -> &str {
79 self.as_str()
80 }
81}
82
83impl fmt::Display for GoIdentifier {
84 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
85 formatter.write_str(self.as_str())
86 }
87}
88
89impl FromStr for GoIdentifier {
90 type Err = GoIdentifierError;
91
92 fn from_str(value: &str) -> Result<Self, Self::Err> {
93 Self::new(value)
94 }
95}
96
97impl TryFrom<&str> for GoIdentifier {
98 type Error = GoIdentifierError;
99
100 fn try_from(value: &str) -> Result<Self, Self::Error> {
101 Self::new(value)
102 }
103}
104
105#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
107pub struct GoExportedIdentifier(GoIdentifier);
108
109impl GoExportedIdentifier {
110 pub fn new(value: impl Into<String>) -> Result<Self, GoIdentifierError> {
116 let identifier = GoIdentifier::new(value)?;
117 if identifier.is_exported() {
118 Ok(Self(identifier))
119 } else {
120 Err(GoIdentifierError::NotExported)
121 }
122 }
123
124 #[must_use]
126 pub fn as_str(&self) -> &str {
127 self.0.as_str()
128 }
129
130 #[must_use]
132 pub fn into_identifier(self) -> GoIdentifier {
133 self.0
134 }
135}
136
137impl AsRef<str> for GoExportedIdentifier {
138 fn as_ref(&self) -> &str {
139 self.as_str()
140 }
141}
142
143impl fmt::Display for GoExportedIdentifier {
144 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
145 formatter.write_str(self.as_str())
146 }
147}
148
149impl FromStr for GoExportedIdentifier {
150 type Err = GoIdentifierError;
151
152 fn from_str(value: &str) -> Result<Self, Self::Err> {
153 Self::new(value)
154 }
155}
156
157impl TryFrom<&str> for GoExportedIdentifier {
158 type Error = GoIdentifierError;
159
160 fn try_from(value: &str) -> Result<Self, Self::Error> {
161 Self::new(value)
162 }
163}
164
165#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
167pub struct GoUnexportedIdentifier(GoIdentifier);
168
169impl GoUnexportedIdentifier {
170 pub fn new(value: impl Into<String>) -> Result<Self, GoIdentifierError> {
176 let identifier = GoIdentifier::new(value)?;
177 if identifier.is_unexported() {
178 Ok(Self(identifier))
179 } else {
180 Err(GoIdentifierError::NotUnexported)
181 }
182 }
183
184 #[must_use]
186 pub fn as_str(&self) -> &str {
187 self.0.as_str()
188 }
189
190 #[must_use]
192 pub fn into_identifier(self) -> GoIdentifier {
193 self.0
194 }
195}
196
197impl AsRef<str> for GoUnexportedIdentifier {
198 fn as_ref(&self) -> &str {
199 self.as_str()
200 }
201}
202
203impl fmt::Display for GoUnexportedIdentifier {
204 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
205 formatter.write_str(self.as_str())
206 }
207}
208
209impl FromStr for GoUnexportedIdentifier {
210 type Err = GoIdentifierError;
211
212 fn from_str(value: &str) -> Result<Self, Self::Err> {
213 Self::new(value)
214 }
215}
216
217impl TryFrom<&str> for GoUnexportedIdentifier {
218 type Error = GoIdentifierError;
219
220 fn try_from(value: &str) -> Result<Self, Self::Error> {
221 Self::new(value)
222 }
223}
224
225#[must_use]
227pub const fn is_ascii_go_identifier_start(character: char) -> bool {
228 character == '_' || character.is_ascii_alphabetic()
229}
230
231#[must_use]
233pub const fn is_ascii_go_identifier_continue(character: char) -> bool {
234 is_ascii_go_identifier_start(character) || character.is_ascii_digit()
235}
236
237#[must_use]
239pub fn is_valid_ascii_go_identifier(value: &str) -> bool {
240 validate_ascii_go_identifier(value).is_ok()
241}
242
243#[must_use]
245pub fn is_exported_go_identifier(value: &str) -> bool {
246 first_identifier_char(value).is_some_and(|character| character.is_ascii_uppercase())
247 && is_valid_ascii_go_identifier(value)
248}
249
250#[must_use]
252pub fn is_unexported_go_identifier(value: &str) -> bool {
253 first_identifier_char(value)
254 .is_some_and(|character| character == '_' || character.is_ascii_lowercase())
255 && is_valid_ascii_go_identifier(value)
256}
257
258fn first_identifier_char(value: &str) -> Option<char> {
259 value.chars().next()
260}
261
262fn validate_ascii_go_identifier(value: &str) -> Result<(), GoIdentifierError> {
263 let mut characters = value.char_indices();
264 let Some((_, first)) = characters.next() else {
265 return Err(GoIdentifierError::Empty);
266 };
267
268 if !is_ascii_go_identifier_start(first) {
269 return Err(GoIdentifierError::InvalidStart { character: first });
270 }
271
272 for (index, character) in characters {
273 if !is_ascii_go_identifier_continue(character) {
274 return Err(GoIdentifierError::InvalidContinue { index, character });
275 }
276 }
277
278 Ok(())
279}
280
281#[cfg(test)]
282mod tests {
283 use super::{
284 is_exported_go_identifier, is_unexported_go_identifier, is_valid_ascii_go_identifier,
285 GoExportedIdentifier, GoIdentifier, GoIdentifierError, GoUnexportedIdentifier,
286 };
287
288 #[test]
289 fn accepts_ascii_identifiers() -> Result<(), GoIdentifierError> {
290 let identifier = GoIdentifier::new("ServeHTTP")?;
291 assert_eq!(identifier.as_str(), "ServeHTTP");
292 assert!(identifier.is_exported());
293 assert!(is_valid_ascii_go_identifier("handler_1"));
294 assert!(is_valid_ascii_go_identifier("_internal"));
295 Ok(())
296 }
297
298 #[test]
299 fn distinguishes_exported_and_unexported_identifiers() -> Result<(), GoIdentifierError> {
300 let exported = GoExportedIdentifier::new("Client")?;
301 let unexported = GoUnexportedIdentifier::new("_client")?;
302
303 assert_eq!(exported.as_str(), "Client");
304 assert_eq!(unexported.as_str(), "_client");
305 assert!(is_exported_go_identifier("Client"));
306 assert!(is_unexported_go_identifier("client"));
307 assert_eq!(
308 GoExportedIdentifier::new("client"),
309 Err(GoIdentifierError::NotExported)
310 );
311 assert_eq!(
312 GoUnexportedIdentifier::new("Client"),
313 Err(GoIdentifierError::NotUnexported)
314 );
315 Ok(())
316 }
317
318 #[test]
319 fn rejects_invalid_identifiers() {
320 assert_eq!(GoIdentifier::new(""), Err(GoIdentifierError::Empty));
321 assert_eq!(
322 GoIdentifier::new("1value"),
323 Err(GoIdentifierError::InvalidStart { character: '1' })
324 );
325 assert!(!is_valid_ascii_go_identifier("has-dash"));
326 assert!(!is_valid_ascii_go_identifier("has space"));
327 assert!(!is_valid_ascii_go_identifier("π"));
328 }
329}