0553b41b119e2e8e5eb74d81f380400aef63dd25
[pspp] / rust / src / identifier.rs
1 use encoding_rs::{EncoderResult, Encoding};
2 use finl_unicode::categories::{CharacterCategories, MajorCategory};
3 use thiserror::Error as ThisError;
4 use unicase::UniCase;
5
6 pub trait IdentifierChar {
7     /// Returns true if `self` may be the first character in an identifier.
8     fn may_start_id(self) -> bool;
9
10     /// Returns true if `self` may be a second or subsequent character in an
11     /// identifier.
12     fn may_continue_id(self) -> bool;
13 }
14
15 impl IdentifierChar for char {
16     fn may_start_id(self) -> bool {
17         use MajorCategory::*;
18
19         ([L, M, S].contains(&self.get_major_category()) || "@#$".contains(self))
20             && self != char::REPLACEMENT_CHARACTER
21     }
22
23     fn may_continue_id(self) -> bool {
24         use MajorCategory::*;
25
26         ([L, M, S, N].contains(&self.get_major_category()) || "@#$._".contains(self))
27             && self != char::REPLACEMENT_CHARACTER
28     }
29 }
30
31 #[derive(Clone, PartialEq, Eq, Debug, Hash)]
32 pub struct Identifier(pub UniCase<String>);
33
34 #[derive(Clone, Debug, ThisError)]
35 pub enum Error {
36     #[error("Identifier cannot be empty string.")]
37     Empty,
38
39     #[error("\"{0}\" may not be used as an identifier because it is a reserved word.")]
40     Reserved(String),
41
42     #[error("\"{0}\" may not be used as an identifier because it begins with disallowed character \"{1}\".")]
43     BadFirstCharacter(String, char),
44
45     #[error("\"{0}\" may not be used as an identifier because it contains disallowed character \"{1}\".")]
46     BadLaterCharacter(String, char),
47
48     #[error("Identifier \"{id}\" is {length} bytes in the encoding in use ({encoding}), which exceeds the {max}-byte limit.")]
49     TooLong {
50         id: String,
51         length: usize,
52         encoding: &'static str,
53         max: usize,
54     },
55
56     #[error("\"{id}\" may not be used as an identifier because the encoding in use ({encoding}) cannot represent \"{c}\".")]
57     NotEncodable {
58         id: String,
59         encoding: &'static str,
60         c: char,
61     },
62 }
63
64 fn is_reserved_word(s: &str) -> bool {
65     for word in [
66         "and", "or", "not", "eq", "ge", "gt", "le", "ne", "all", "by", "to", "with",
67     ] {
68         if s.eq_ignore_ascii_case(word) {
69             return true;
70         }
71     }
72     false
73 }
74
75 impl Identifier {
76     /// Maximum length of an identifier, in bytes.  The limit applies in the
77     /// encoding used by the dictionary, not in UTF-8.
78     pub const MAX_LEN: usize = 64;
79
80     pub fn new(s: &str, encoding: &'static Encoding) -> Result<Identifier, Error> {
81         Self::is_plausible(s)?;
82         let (encoded, _, unencodable) = encoding.encode(s);
83         if unencodable {
84             let mut encoder = encoding.new_encoder();
85             let mut buf =
86                 Vec::with_capacity(encoder.max_buffer_length_from_utf8_without_replacement(s.len()).unwrap());
87             let EncoderResult::Unmappable(c) = encoder
88                 .encode_from_utf8_to_vec_without_replacement(s, &mut buf, true)
89                 .0
90             else {
91                 unreachable!();
92             };
93             return Err(Error::NotEncodable { id: s.into(), encoding: encoding.name(), c });
94         }
95         if encoded.len() > Self::MAX_LEN {
96             return Err(Error::TooLong { id: s.into(), length: encoded.len(), encoding: encoding.name(), max: Self::MAX_LEN });
97         }
98         Ok(Identifier(s.into()))
99     }
100     pub fn is_plausible(s: &str) -> Result<(), Error> {
101         if s.is_empty() {
102             return Err(Error::Empty);
103         }
104         if is_reserved_word(s) {
105             return Err(Error::Reserved(s.into()));
106         }
107
108         let mut i = s.chars();
109         let first = i.next().unwrap();
110         if !first.may_start_id() {
111             return Err(Error::BadFirstCharacter(s.into(), first));
112         }
113         for c in i {
114             if !c.may_continue_id() {
115                 return Err(Error::BadLaterCharacter(s.into(), c));
116             }
117         }
118         Ok(())
119     }
120 }