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}