Skip to main content

wowlab_parsers/parsers/access/
parser.rs

1use super::{
2    errors::ParseAccessError,
3    lexer::{Token, lex},
4    types::{AccessControl, AccessRule},
5};
6
7/// Parse an access-control expression into an [`AccessControl`].
8///
9/// # Errors
10///
11/// Returns [`ParseAccessError`] when the expression contains an unknown rule or malformed target.
12pub 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}