Skip to main content

use_go_identifier/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7/// Error returned when an ASCII Go identifier is invalid.
8#[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/// Validated ASCII-safe Go identifier.
37#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
38pub struct GoIdentifier(String);
39
40impl GoIdentifier {
41    /// Creates an ASCII-safe Go identifier.
42    ///
43    /// # Errors
44    ///
45    /// Returns [`GoIdentifierError`] when `value` is empty or not ASCII identifier-shaped.
46    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    /// Returns the identifier as a string slice.
53    #[must_use]
54    pub fn as_str(&self) -> &str {
55        &self.0
56    }
57
58    /// Consumes the identifier and returns the owned text.
59    #[must_use]
60    pub fn into_string(self) -> String {
61        self.0
62    }
63
64    /// Returns whether this identifier is exported by ASCII convention.
65    #[must_use]
66    pub fn is_exported(&self) -> bool {
67        is_exported_go_identifier(self.as_str())
68    }
69
70    /// Returns whether this identifier is unexported by ASCII convention.
71    #[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/// Validated exported ASCII-safe Go identifier.
106#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
107pub struct GoExportedIdentifier(GoIdentifier);
108
109impl GoExportedIdentifier {
110    /// Creates an exported Go identifier.
111    ///
112    /// # Errors
113    ///
114    /// Returns [`GoIdentifierError`] when `value` is not a valid ASCII Go identifier or is not exported.
115    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    /// Returns the identifier as a string slice.
125    #[must_use]
126    pub fn as_str(&self) -> &str {
127        self.0.as_str()
128    }
129
130    /// Consumes the wrapper and returns the general identifier.
131    #[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/// Validated unexported ASCII-safe Go identifier.
166#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
167pub struct GoUnexportedIdentifier(GoIdentifier);
168
169impl GoUnexportedIdentifier {
170    /// Creates an unexported Go identifier.
171    ///
172    /// # Errors
173    ///
174    /// Returns [`GoIdentifierError`] when `value` is not a valid ASCII Go identifier or is not unexported.
175    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    /// Returns the identifier as a string slice.
185    #[must_use]
186    pub fn as_str(&self) -> &str {
187        self.0.as_str()
188    }
189
190    /// Consumes the wrapper and returns the general identifier.
191    #[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/// Returns whether `character` is accepted as an ASCII Go identifier start.
226#[must_use]
227pub const fn is_ascii_go_identifier_start(character: char) -> bool {
228    character == '_' || character.is_ascii_alphabetic()
229}
230
231/// Returns whether `character` is accepted after the first identifier character.
232#[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/// Returns whether `value` is an ASCII-safe Go identifier.
238#[must_use]
239pub fn is_valid_ascii_go_identifier(value: &str) -> bool {
240    validate_ascii_go_identifier(value).is_ok()
241}
242
243/// Returns whether `value` is an exported ASCII-safe Go identifier.
244#[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/// Returns whether `value` is an unexported ASCII-safe Go identifier.
251#[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}