use_python_identifier/
lib.rs1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7use use_python_keyword::is_python_keyword;
8
9#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
11pub struct PythonIdentifier(String);
12
13impl PythonIdentifier {
14 pub fn new(input: &str) -> Result<Self, PythonIdentifierError> {
20 validate_ascii_python_identifier(input)?;
21 if is_python_keyword(input) {
22 return Err(PythonIdentifierError::Keyword);
23 }
24 Ok(Self(input.to_string()))
25 }
26
27 #[must_use]
29 pub fn as_str(&self) -> &str {
30 &self.0
31 }
32}
33
34impl fmt::Display for PythonIdentifier {
35 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
36 formatter.write_str(self.as_str())
37 }
38}
39
40impl FromStr for PythonIdentifier {
41 type Err = PythonIdentifierError;
42
43 fn from_str(input: &str) -> Result<Self, Self::Err> {
44 Self::new(input)
45 }
46}
47
48impl TryFrom<&str> for PythonIdentifier {
49 type Error = PythonIdentifierError;
50
51 fn try_from(value: &str) -> Result<Self, Self::Error> {
52 Self::new(value)
53 }
54}
55
56#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
58pub struct PythonDunderName(PythonIdentifier);
59
60impl PythonDunderName {
61 pub fn new(input: &str) -> Result<Self, PythonIdentifierError> {
67 let identifier = PythonIdentifier::new(input)?;
68 if is_dunder_name(identifier.as_str()) {
69 Ok(Self(identifier))
70 } else {
71 Err(PythonIdentifierError::NotDunderName)
72 }
73 }
74
75 #[must_use]
77 pub fn as_str(&self) -> &str {
78 self.0.as_str()
79 }
80}
81
82impl fmt::Display for PythonDunderName {
83 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
84 formatter.write_str(self.as_str())
85 }
86}
87
88#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
90pub struct PythonPrivateName(PythonIdentifier);
91
92impl PythonPrivateName {
93 pub fn new(input: &str) -> Result<Self, PythonIdentifierError> {
99 let identifier = PythonIdentifier::new(input)?;
100 if is_private_name(identifier.as_str()) {
101 Ok(Self(identifier))
102 } else {
103 Err(PythonIdentifierError::NotPrivateName)
104 }
105 }
106
107 #[must_use]
109 pub fn as_str(&self) -> &str {
110 self.0.as_str()
111 }
112}
113
114impl fmt::Display for PythonPrivateName {
115 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
116 formatter.write_str(self.as_str())
117 }
118}
119
120#[derive(Clone, Copy, Debug, Eq, PartialEq)]
122pub enum PythonIdentifierError {
123 Empty,
124 Keyword,
125 InvalidStart { character: char },
126 InvalidContinue { index: usize, character: char },
127 NotDunderName,
128 NotPrivateName,
129}
130
131impl fmt::Display for PythonIdentifierError {
132 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
133 match self {
134 Self::Empty => formatter.write_str("Python identifier cannot be empty"),
135 Self::Keyword => formatter.write_str("Python identifier cannot be a hard keyword"),
136 Self::InvalidStart { character } => {
137 write!(formatter, "invalid Python identifier start `{character}`")
138 }
139 Self::InvalidContinue { index, character } => write!(
140 formatter,
141 "invalid Python identifier continuation `{character}` at byte index {index}"
142 ),
143 Self::NotDunderName => formatter.write_str("Python identifier is not a dunder name"),
144 Self::NotPrivateName => formatter.write_str("Python identifier is not a private name"),
145 }
146 }
147}
148
149impl Error for PythonIdentifierError {}
150
151#[must_use]
153pub const fn is_ascii_python_identifier_start(character: char) -> bool {
154 character == '_' || character.is_ascii_alphabetic()
155}
156
157#[must_use]
159pub const fn is_ascii_python_identifier_continue(character: char) -> bool {
160 is_ascii_python_identifier_start(character) || character.is_ascii_digit()
161}
162
163#[must_use]
165pub fn is_valid_ascii_python_identifier(input: &str) -> bool {
166 PythonIdentifier::new(input).is_ok()
167}
168
169#[must_use]
171pub fn is_dunder_name(input: &str) -> bool {
172 input.len() > 4 && input.starts_with("__") && input.ends_with("__")
173}
174
175#[must_use]
177pub fn is_private_name(input: &str) -> bool {
178 input.starts_with('_') && !is_dunder_name(input)
179}
180
181fn validate_ascii_python_identifier(input: &str) -> Result<(), PythonIdentifierError> {
182 if input.trim().is_empty() {
183 return Err(PythonIdentifierError::Empty);
184 }
185
186 let mut characters = input.char_indices();
187 let Some((_, first)) = characters.next() else {
188 return Err(PythonIdentifierError::Empty);
189 };
190
191 if !is_ascii_python_identifier_start(first) {
192 return Err(PythonIdentifierError::InvalidStart { character: first });
193 }
194
195 for (index, character) in characters {
196 if !is_ascii_python_identifier_continue(character) {
197 return Err(PythonIdentifierError::InvalidContinue { index, character });
198 }
199 }
200
201 Ok(())
202}
203
204#[cfg(test)]
205mod tests {
206 use super::{
207 PythonDunderName, PythonIdentifier, PythonIdentifierError, PythonPrivateName,
208 is_dunder_name, is_private_name, is_valid_ascii_python_identifier,
209 };
210
211 #[test]
212 fn accepts_ascii_identifiers() -> Result<(), PythonIdentifierError> {
213 let identifier = PythonIdentifier::new("async_task_1")?;
214
215 assert_eq!(identifier.as_str(), "async_task_1");
216 assert!(is_valid_ascii_python_identifier("_internal"));
217 assert!(is_valid_ascii_python_identifier("match"));
218 Ok(())
219 }
220
221 #[test]
222 fn rejects_invalid_identifiers_and_keywords() {
223 assert_eq!(PythonIdentifier::new(""), Err(PythonIdentifierError::Empty));
224 assert_eq!(
225 PythonIdentifier::new("class"),
226 Err(PythonIdentifierError::Keyword)
227 );
228 assert_eq!(
229 PythonIdentifier::new("1value"),
230 Err(PythonIdentifierError::InvalidStart { character: '1' })
231 );
232 assert!(!is_valid_ascii_python_identifier("has-dash"));
233 assert!(!is_valid_ascii_python_identifier("π"));
234 }
235
236 #[test]
237 fn validates_dunder_and_private_names() -> Result<(), PythonIdentifierError> {
238 let dunder = PythonDunderName::new("__init__")?;
239 let private = PythonPrivateName::new("_cache")?;
240
241 assert_eq!(dunder.as_str(), "__init__");
242 assert_eq!(private.as_str(), "_cache");
243 assert!(is_dunder_name("__len__"));
244 assert!(is_private_name("_name"));
245 assert!(!is_private_name("__name__"));
246 Ok(())
247 }
248}