wowlab_parsers/parsers/access/
parser.rs1use super::{
2 errors::ParseAccessError,
3 lexer::{Token, lex},
4 types::{AccessControl, AccessRule},
5};
6
7pub fn parse(input: &str) -> Result<AccessControl, ParseAccessError> {
13 let input = input.trim();
14
15 if input.is_empty() {
16 return Ok(AccessControl::private());
17 }
18
19 Parser::new(input)?.parse()
20}
21
22struct Parser<'a> {
23 tokens: Vec<(Token<'a>, usize)>,
24 pos: usize,
25}
26
27impl<'a> Parser<'a> {
28 fn new(input: &'a str) -> Result<Self, ParseAccessError> {
29 let tokens = lex(input)
30 .spanned()
31 .map(|(result, span)| {
32 result
33 .map(|token| (token, span.start))
34 .map_err(|()| ParseAccessError::invalid_character(span.start))
35 })
36 .collect::<Result<Vec<_>, _>>()?;
37
38 Ok(Self { tokens, pos: 0 })
39 }
40
41 fn peek(&self) -> Option<&Token<'a>> {
42 self.tokens.get(self.pos).map(|(t, _)| t)
43 }
44
45 fn advance(&mut self) -> Option<&Token<'a>> {
46 let t = self.tokens.get(self.pos).map(|(t, _)| t);
47
48 if t.is_some() {
49 self.pos += 1;
50 }
51
52 t
53 }
54
55 fn current_pos(&self) -> usize {
56 self.tokens.get(self.pos).map_or(0, |(_, p)| *p)
57 }
58
59 fn parse(mut self) -> Result<AccessControl, ParseAccessError> {
60 let mut rules = Vec::new();
61
62 loop {
63 let rule = self.parse_rule()?;
64
65 rules.push(rule);
66
67 match self.peek() {
68 Some(Token::Comma) => {
69 self.advance();
70 }
71 None => break,
72 _ => {
73 return Err(ParseAccessError::unexpected_token(self.current_pos()));
74 }
75 }
76 }
77
78 Ok(AccessControl { rules })
79 }
80
81 fn parse_rule(&mut self) -> Result<AccessRule, ParseAccessError> {
82 match self.advance() {
83 Some(Token::Public) => Ok(AccessRule::Public),
84 Some(Token::Discord) => {
85 self.expect_colon()?;
86 let id = self.expect_ident("discord")?;
87
88 Ok(AccessRule::Discord(id.to_string()))
89 }
90 Some(Token::Friends) => {
91 self.expect_colon()?;
92 let id = self.expect_ident("friends")?;
93
94 Ok(AccessRule::Friends(id.to_string()))
95 }
96 Some(Token::Ident(s)) => Err(ParseAccessError::unknown_access_type(s.to_string())),
97 _ => Err(ParseAccessError::unexpected_eof()),
98 }
99 }
100
101 fn expect_colon(&mut self) -> Result<(), ParseAccessError> {
102 match self.advance() {
103 Some(Token::Colon) => Ok(()),
104 _ => Err(ParseAccessError::unexpected_token(self.current_pos())),
105 }
106 }
107
108 fn expect_ident(&mut self, keyword: &'static str) -> Result<&'a str, ParseAccessError> {
109 match self.advance() {
110 Some(Token::Ident(s)) => Ok(*s),
111 _ => Err(ParseAccessError::missing_target(keyword)),
112 }
113 }
114}
115
116impl std::str::FromStr for AccessControl {
117 type Err = ParseAccessError;
118
119 fn from_str(s: &str) -> Result<Self, Self::Err> {
120 parse(s)
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 use googletest::prelude::*;
127
128 use super::*;
129
130 #[gtest]
131 fn test_empty() -> Result<()> {
132 let ac: AccessControl = "".parse().or_fail()?;
133
134 verify_true!(ac.is_private())?;
135 verify_true!(ac.rules.is_empty())?;
136
137 Ok(())
138 }
139
140 #[gtest]
141 fn test_public() -> Result<()> {
142 let ac: AccessControl = "public".parse().or_fail()?;
143
144 verify_true!(ac.is_public())?;
145 verify_that!(ac.rules.len(), eq(1))?;
146
147 Ok(())
148 }
149
150 #[gtest]
151 fn test_discord() -> Result<()> {
152 let ac: AccessControl = "discord:123456789".parse().or_fail()?;
153
154 verify_false!(ac.is_public())?;
155 let ids: Vec<_> = ac.discord_guild_ids().collect();
156
157 verify_that!(ids, container_eq(vec!["123456789"]))?;
158
159 Ok(())
160 }
161
162 #[gtest]
163 fn test_friends() -> Result<()> {
164 let ac: AccessControl = "friends:abc-def-123".parse().or_fail()?;
165
166 verify_false!(ac.is_public())?;
167 let ids: Vec<_> = ac.friend_list_ids().collect();
168
169 verify_that!(ids, container_eq(vec!["abc-def-123"]))?;
170
171 Ok(())
172 }
173
174 #[gtest]
175 fn test_multiple() -> Result<()> {
176 let ac: AccessControl = "public,discord:123,discord:456,friends:abc"
177 .parse()
178 .or_fail()?;
179
180 verify_true!(ac.is_public())?;
181 let discord: Vec<_> = ac.discord_guild_ids().collect();
182
183 verify_that!(discord, container_eq(vec!["123", "456"]))?;
184 let friends: Vec<_> = ac.friend_list_ids().collect();
185
186 verify_that!(friends, container_eq(vec!["abc"]))?;
187
188 Ok(())
189 }
190
191 #[gtest]
192 fn test_roundtrip() -> Result<()> {
193 let input = "public,discord:123,friends:abc-def";
194 let ac: AccessControl = input.parse().or_fail()?;
195
196 verify_that!(ac.to_string(), eq(input))?;
197
198 Ok(())
199 }
200
201 #[gtest]
202 fn test_unknown_type() {
203 let result: Result<AccessControl, _> = "invalid".parse();
204
205 expect_that!(result, err(anything()));
206 }
207
208 #[gtest]
209 fn test_invalid_character_is_rejected() {
210 let result: Result<AccessControl, _> = "public!".parse();
211
212 expect_that!(result, err(anything()));
213 }
214
215 #[gtest]
216 fn test_invalid_character_cannot_join_target_fragments() {
217 let result: Result<AccessControl, _> = "discord:123@456".parse();
218
219 expect_that!(result, err(anything()));
220 }
221
222 #[gtest]
223 fn test_missing_target() {
224 let result: Result<AccessControl, _> = "discord:".parse();
225
226 expect_that!(result, err(anything()));
227 }
228
229 #[gtest]
230 fn test_whitespace_handling() -> Result<()> {
231 let ac: AccessControl = " public ".parse().or_fail()?;
232
233 verify_true!(ac.is_public())?;
234
235 Ok(())
236 }
237}