strck_ident/rust.rs
1//! Checked strings containing Rust identifiers.
2//!
3//! Raw identifiers are unsupported.
4//!
5//! # Examples
6//!
7//! ```rust
8//! use strck_ident::{IntoCk, rust::RustIdent};
9//!
10//! assert!("foo".ck::<RustIdent>().is_ok());
11//! assert!("_identifier".ck::<RustIdent>().is_ok());
12//! assert!("Москва".ck::<RustIdent>().is_ok());
13//! assert!("東京".ck::<RustIdent>().is_ok());
14//!
15//! assert!("struct".ck::<RustIdent>().is_err());
16//! assert!("r#try".ck::<RustIdent>().is_err());
17//! assert!("👍".ck::<RustIdent>().is_err());
18//! ```
19//!
20//! # Aliases
21//!
22//! This module exposes [`Ident`] and [`IdentBuf`], which alias `Ck<RustIdent>`
23//! and `Check<RustIdent>` respectively. These aliases are preferred to keep
24//! type signatures succinct.
25//!
26//! # Requirements
27//!
28//! This module is only available when the `rust` feature flag is enabled.
29use crate::unicode;
30use core::fmt;
31use strck::{Check, Ck, Invariant};
32
33/// An [`Invariant`] for Rust identifiers.
34///
35/// Raw identifiers are unsupported.
36///
37/// # Invariants
38///
39/// * The string is nonempty.
40/// * The first character is `_` or XID_Start.
41/// * Any following characters are XID_Continue.
42/// * The string isn't a single underscore, e.g. `"_"`.
43/// * The string isn't a [strict] or [reserved] keyword.
44///
45/// [strict]: https://doc.rust-lang.org/reference/keywords.html#strict-keywords
46/// [reserved]: https://doc.rust-lang.org/reference/keywords.html#reserved-keywords
47#[derive(Clone, Debug)]
48pub struct RustIdent;
49
50/// Borrowed checked string containing a Rust identifier.
51///
52/// See [`RustIdent`] for more details.
53pub type Ident = Ck<RustIdent>;
54
55/// Owned checked string containing a Rust identifier.
56///
57/// See [`RustIdent`] for more details.
58pub type IdentBuf<B = String> = Check<RustIdent, B>;
59
60/// The error type returned from checking the invariants of [`RustIdent`].
61#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
62pub enum Error {
63 /// An invalid unicode identifier.
64 Unicode(unicode::Error),
65
66 /// A [strict] or [reserved] keyword.
67 ///
68 /// [strict]: https://doc.rust-lang.org/reference/keywords.html#strict-keywords
69 /// [reserved]: https://doc.rust-lang.org/reference/keywords.html#reserved-keywords
70 Keyword(&'static str),
71
72 /// A single underscore.
73 Wildcard,
74}
75
76impl std::error::Error for Error {}
77
78impl fmt::Display for Error {
79 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
80 match self {
81 Error::Unicode(err) => err.fmt(f),
82 Error::Keyword(keyword) => {
83 write!(f, "Rust keyword: '{keyword}'")
84 }
85 Error::Wildcard => f.pad("wildcard '_' isn't a valid Rust ident"),
86 }
87 }
88}
89
90impl Invariant for RustIdent {
91 type Error = Error;
92
93 fn check(slice: &str) -> Result<(), Self::Error> {
94 match unicode::UnicodeIdent::check(slice) {
95 Ok(()) => match KEYWORDS.binary_search(&slice) {
96 Ok(index) => Err(Error::Keyword(KEYWORDS[index])),
97 Err(_) => Ok(()),
98 },
99 Err(unicode::Error::Start('_')) => match slice.len() {
100 1 => Err(Error::Wildcard), // `_` isn't ok
101 _ => Ok(()), // `_x` is ok
102 },
103 Err(e) => Err(Error::Unicode(e)),
104 }
105 }
106}
107
108static KEYWORDS: [&str; 51] = [
109 "Self", "abstract", "as", "async", "await", "become", "box", "break", "const", "continue",
110 "crate", "do", "dyn", "else", "enum", "extern", "false", "final", "fn", "for", "if", "impl",
111 "in", "let", "loop", "macro", "match", "mod", "move", "mut", "override", "priv", "pub", "ref",
112 "return", "self", "static", "struct", "super", "trait", "true", "try", "type", "typeof",
113 "unsafe", "unsized", "use", "virtual", "where", "while", "yield",
114];
115
116#[cfg(test)]
117mod tests {
118 use super::{Error, RustIdent};
119 use strck::IntoCk;
120
121 #[test]
122 fn test_underscore() {
123 assert_eq!("_".ck::<RustIdent>().unwrap_err(), Error::Wildcard);
124 assert!("_unused".ck::<RustIdent>().is_ok());
125 assert!("__private".ck::<RustIdent>().is_ok());
126 assert!("snake_case".ck::<RustIdent>().is_ok());
127 }
128
129 #[test]
130 fn test_rust_reference() {
131 assert!("foo".ck::<RustIdent>().is_ok());
132 assert!("_identifier".ck::<RustIdent>().is_ok());
133 assert!("Москва".ck::<RustIdent>().is_ok());
134 assert!("東京".ck::<RustIdent>().is_ok());
135 }
136}