hcl_primitives/ident.rs
1//! Construct and validate HCL identifiers.
2
3use crate::{Error, InternalString};
4use alloc::borrow::{Borrow, Cow};
5use alloc::format;
6#[cfg(not(feature = "std"))]
7use alloc::string::String;
8use core::fmt;
9use core::ops;
10use core::str::FromStr;
11
12/// Represents an HCL identifier.
13#[derive(Clone, PartialEq, Eq, Hash)]
14pub struct Ident(InternalString);
15
16impl Ident {
17 /// Create a new `Ident` after validating that it only contains characters that are allowed in
18 /// HCL identifiers.
19 ///
20 /// See [`Ident::try_new`] for a fallible alternative to this function.
21 ///
22 /// # Example
23 ///
24 /// ```
25 /// # use hcl_primitives::Ident;
26 /// assert_eq!(Ident::new("some_ident").as_str(), "some_ident");
27 /// ```
28 ///
29 /// # Panics
30 ///
31 /// This function panics if `ident` contains characters that are not allowed in HCL identifiers
32 /// or if it is empty.
33 pub fn new<T>(ident: T) -> Ident
34 where
35 T: Into<InternalString>,
36 {
37 let ident = ident.into();
38
39 assert!(is_ident(&ident), "invalid identifier `{ident}`");
40
41 Ident(ident)
42 }
43
44 /// Create a new `Ident` after validating that it only contains characters that are allowed in
45 /// HCL identifiers.
46 ///
47 /// In contrast to [`Ident::new`], this function returns an error instead of panicking when an
48 /// invalid identifier is encountered.
49 ///
50 /// See [`Ident::new_sanitized`] for an infallible alternative to this function.
51 ///
52 /// # Example
53 ///
54 /// ```
55 /// # use hcl_primitives::Ident;
56 /// assert!(Ident::try_new("some_ident").is_ok());
57 /// assert!(Ident::try_new("").is_err());
58 /// assert!(Ident::try_new("1two3").is_err());
59 /// assert!(Ident::try_new("with whitespace").is_err());
60 /// ```
61 ///
62 /// # Errors
63 ///
64 /// If `ident` contains characters that are not allowed in HCL identifiers or if it is empty an
65 /// error will be returned.
66 pub fn try_new<T>(ident: T) -> Result<Ident, Error>
67 where
68 T: Into<InternalString>,
69 {
70 let ident = ident.into();
71
72 if !is_ident(&ident) {
73 return Err(Error::new(format!("invalid identifier `{ident}`")));
74 }
75
76 Ok(Ident(ident))
77 }
78
79 /// Create a new `Ident` after sanitizing the input if necessary.
80 ///
81 /// If `ident` contains characters that are not allowed in HCL identifiers will be sanitized
82 /// according to the following rules:
83 ///
84 /// - An empty `ident` results in an identifier containing a single underscore.
85 /// - Invalid characters in `ident` will be replaced with underscores.
86 /// - If `ident` starts with a character that is invalid in the first position but would be
87 /// valid in the rest of an HCL identifier it is prefixed with an underscore.
88 ///
89 /// See [`Ident::try_new`] for a fallible alternative to this function if you prefer rejecting
90 /// invalid identifiers instead of sanitizing them.
91 ///
92 /// # Example
93 ///
94 /// ```
95 /// # use hcl_primitives::Ident;
96 /// assert_eq!(Ident::new_sanitized("some_ident").as_str(), "some_ident");
97 /// assert_eq!(Ident::new_sanitized("").as_str(), "_");
98 /// assert_eq!(Ident::new_sanitized("1two3").as_str(), "_1two3");
99 /// assert_eq!(Ident::new_sanitized("with whitespace").as_str(), "with_whitespace");
100 /// ```
101 pub fn new_sanitized<T>(ident: T) -> Self
102 where
103 T: AsRef<str>,
104 {
105 let input = ident.as_ref();
106
107 if input.is_empty() {
108 return Ident(InternalString::from("_"));
109 }
110
111 let mut ident = String::with_capacity(input.len());
112
113 for (i, ch) in input.chars().enumerate() {
114 if i == 0 && is_id_start(ch) {
115 ident.push(ch);
116 } else if is_id_continue(ch) {
117 if i == 0 {
118 ident.push('_');
119 }
120 ident.push(ch);
121 } else {
122 ident.push('_');
123 }
124 }
125
126 Ident(InternalString::from(ident))
127 }
128
129 /// Create a new `Ident` without checking if it is valid.
130 ///
131 /// It is the caller's responsibility to ensure that the identifier is valid.
132 ///
133 /// For most use cases [`Ident::new`], [`Ident::try_new`] or [`Ident::new_sanitized`] should be
134 /// preferred.
135 ///
136 /// This function is not marked as unsafe because it does not cause undefined behaviour.
137 /// However, attempting to serialize an invalid identifier to HCL will produce invalid output.
138 #[inline]
139 pub fn new_unchecked<T>(ident: T) -> Self
140 where
141 T: Into<InternalString>,
142 {
143 Ident(ident.into())
144 }
145
146 /// Converts the `Ident` to a mutable string type.
147 #[inline]
148 #[must_use]
149 pub fn into_string(self) -> String {
150 self.0.into_string()
151 }
152
153 /// Return a reference to the wrapped `str`.
154 #[inline]
155 pub fn as_str(&self) -> &str {
156 self.0.as_str()
157 }
158}
159
160impl TryFrom<InternalString> for Ident {
161 type Error = Error;
162
163 #[inline]
164 fn try_from(s: InternalString) -> Result<Self, Self::Error> {
165 Ident::try_new(s)
166 }
167}
168
169impl TryFrom<String> for Ident {
170 type Error = Error;
171
172 #[inline]
173 fn try_from(s: String) -> Result<Self, Self::Error> {
174 Ident::try_new(s)
175 }
176}
177
178impl TryFrom<&str> for Ident {
179 type Error = Error;
180
181 #[inline]
182 fn try_from(s: &str) -> Result<Self, Self::Error> {
183 Ident::try_new(s)
184 }
185}
186
187impl<'a> TryFrom<Cow<'a, str>> for Ident {
188 type Error = Error;
189
190 #[inline]
191 fn try_from(s: Cow<'a, str>) -> Result<Self, Self::Error> {
192 Ident::try_new(s)
193 }
194}
195
196impl FromStr for Ident {
197 type Err = Error;
198
199 #[inline]
200 fn from_str(s: &str) -> Result<Self, Self::Err> {
201 Ident::try_new(s)
202 }
203}
204
205impl From<Ident> for InternalString {
206 #[inline]
207 fn from(ident: Ident) -> Self {
208 ident.0
209 }
210}
211
212impl From<Ident> for String {
213 #[inline]
214 fn from(ident: Ident) -> Self {
215 ident.into_string()
216 }
217}
218
219impl fmt::Debug for Ident {
220 #[inline]
221 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
222 write!(f, "Ident({self})")
223 }
224}
225
226impl fmt::Display for Ident {
227 #[inline]
228 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
229 f.write_str(self.as_str())
230 }
231}
232
233impl ops::Deref for Ident {
234 type Target = str;
235
236 #[inline]
237 fn deref(&self) -> &Self::Target {
238 self.as_str()
239 }
240}
241
242impl AsRef<str> for Ident {
243 #[inline]
244 fn as_ref(&self) -> &str {
245 self.as_str()
246 }
247}
248
249impl Borrow<str> for Ident {
250 #[inline]
251 fn borrow(&self) -> &str {
252 self.as_str()
253 }
254}
255
256#[cfg(feature = "serde")]
257impl serde::Serialize for Ident {
258 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
259 where
260 S: serde::Serializer,
261 {
262 self.0.serialize(serializer)
263 }
264}
265
266#[cfg(feature = "serde")]
267impl<'de> serde::Deserialize<'de> for Ident {
268 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
269 where
270 D: serde::Deserializer<'de>,
271 {
272 let string = InternalString::deserialize(deserializer)?;
273 Ident::try_new(string).map_err(serde::de::Error::custom)
274 }
275}
276
277/// Determines if `ch` is a valid HCL identifier start character.
278///
279/// # Example
280///
281/// ```
282/// # use hcl_primitives::ident::is_id_start;
283/// assert!(is_id_start('_'));
284/// assert!(is_id_start('a'));
285/// assert!(!is_id_start('-'));
286/// assert!(!is_id_start('1'));
287/// assert!(!is_id_start(' '));
288/// ```
289#[inline]
290pub fn is_id_start(ch: char) -> bool {
291 unicode_ident::is_xid_start(ch) || ch == '_'
292}
293
294/// Determines if `ch` is a valid HCL identifier continue character.
295///
296/// # Example
297///
298/// ```
299/// # use hcl_primitives::ident::is_id_continue;
300/// assert!(is_id_continue('-'));
301/// assert!(is_id_continue('_'));
302/// assert!(is_id_continue('a'));
303/// assert!(is_id_continue('1'));
304/// assert!(!is_id_continue(' '));
305/// ```
306#[inline]
307pub fn is_id_continue(ch: char) -> bool {
308 unicode_ident::is_xid_continue(ch) || ch == '-'
309}
310
311/// Determines if `s` represents a valid HCL identifier.
312///
313/// A string is a valid HCL identifier if:
314///
315/// - [`is_id_start`] returns `true` for the first character, and
316/// - [`is_id_continue`] returns `true` for all remaining chacters
317///
318/// # Example
319///
320/// ```
321/// # use hcl_primitives::ident::is_ident;
322/// assert!(!is_ident(""));
323/// assert!(!is_ident("-foo"));
324/// assert!(!is_ident("123foo"));
325/// assert!(!is_ident("foo bar"));
326/// assert!(is_ident("fööbär"));
327/// assert!(is_ident("foobar123"));
328/// assert!(is_ident("FOO-bar123"));
329/// assert!(is_ident("foo_BAR123"));
330/// assert!(is_ident("_foo"));
331/// assert!(is_ident("_123"));
332/// assert!(is_ident("foo_"));
333/// assert!(is_ident("foo-"));
334/// ```
335#[inline]
336pub fn is_ident(s: &str) -> bool {
337 if s.is_empty() {
338 return false;
339 }
340
341 let mut chars = s.chars();
342 let first = chars.next().unwrap();
343
344 is_id_start(first) && chars.all(is_id_continue)
345}