twilight_command_parser/
parser.rs

1use crate::{Arguments, CommandParserConfig};
2
3/// Indicator that a command was used.
4#[derive(Clone, Debug)]
5#[non_exhaustive]
6pub struct Command<'a> {
7    /// A lazy iterator of command arguments. Refer to its documentation on
8    /// how to use it.
9    pub arguments: Arguments<'a>,
10    /// The name of the command that was called.
11    pub name: &'a str,
12    /// The prefix used to call the command.
13    pub prefix: &'a str,
14}
15
16/// A struct to parse prefixes, commands, and arguments out of messages.
17///
18/// While parsing, the parser takes into account the configuration that it was
19/// configured with. This configuration is mutable during runtime via the
20/// [`Parser::config_mut`] method.
21///
22/// After parsing, you're given an optional [`Command`]: a struct representing a
23/// command and its relevant information. Refer to its documentation for more
24/// information.
25///
26/// # Examples
27///
28/// Using a parser configured with the commands `"echo"` and `"ping"` and the
29/// prefix `"!"`, parse the message "!echo foo bar baz":
30///
31/// ```
32/// use twilight_command_parser::{Command, CommandParserConfig, Parser};
33///
34/// let mut config = CommandParserConfig::new();
35/// config.add_command("echo", false);
36/// config.add_command("ping", false);
37/// config.add_prefix("!");
38///
39/// let parser = Parser::new(config);
40///
41/// if let Some(command) = parser.parse("!echo foo bar baz") {
42///     match command {
43///         Command { name: "echo", arguments, .. } => {
44///             let content = arguments.as_str();
45///
46///             println!("Got a request to echo `{}`", content);
47///         },
48///         Command { name: "ping", .. } => {
49///             println!("Got a ping request");
50///         },
51///         _ => {},
52///     }
53/// }
54/// ```
55#[derive(Clone, Debug)]
56pub struct Parser<'a> {
57    config: CommandParserConfig<'a>,
58}
59
60impl<'a> Parser<'a> {
61    /// Creates a new parser from a given configuration.
62    pub fn new(config: impl Into<CommandParserConfig<'a>>) -> Self {
63        Self {
64            config: config.into(),
65        }
66    }
67
68    /// Returns an immutable reference to the configuration.
69    pub const fn config(&self) -> &CommandParserConfig<'a> {
70        &self.config
71    }
72
73    /// Returns a mutable reference to the configuration.
74    pub fn config_mut(&mut self) -> &mut CommandParserConfig<'a> {
75        &mut self.config
76    }
77
78    /// Parses a command out of a buffer.
79    ///
80    /// If a configured prefix and command are in the buffer, then some
81    /// [`Command`] is returned with them and a lazy iterator of the
82    /// argument list.
83    ///
84    /// If a matching prefix or command weren't found, then `None` is returned.
85    ///
86    /// Refer to the struct-level documentation on how to use this.
87    pub fn parse(&'a self, buf: &'a str) -> Option<Command<'a>> {
88        let prefix = self.find_prefix(buf)?;
89        self.parse_with_prefix(prefix, buf)
90    }
91
92    /// Parse a command out of a buffer with a specific prefix.
93    ///
94    /// Instead of using the list of set prefixes, give a specific prefix
95    /// to parse the message, this can be used to have a kind of dynamic
96    /// prefixes.
97    ///
98    /// # Example
99    ///
100    /// ```
101    /// # use twilight_command_parser::{Command, CommandParserConfig, Parser};
102    /// # fn example() -> Option<()> {
103    /// let mut config = CommandParserConfig::new();
104    /// config.add_prefix("!");
105    /// config.add_command("echo", false);
106    ///
107    /// let parser = Parser::new(config);
108    ///
109    /// let command = parser.parse_with_prefix("=", "=echo foo")?;
110    ///
111    /// assert_eq!("=", command.prefix);
112    /// assert_eq!("echo", command.name);
113    /// # Some(())
114    /// # }
115    /// ```
116    pub fn parse_with_prefix(&'a self, prefix: &'a str, buf: &'a str) -> Option<Command<'a>> {
117        if !buf.starts_with(prefix) {
118            return None;
119        }
120
121        let mut idx = prefix.len();
122        let command_buf = buf.get(idx..)?;
123        let command = self.find_command(command_buf)?;
124
125        idx += command.len();
126
127        // Advance from the amount of whitespace that was between the prefix and
128        // the command name.
129        idx += command_buf.len() - command_buf.trim_start().len();
130
131        Some(Command {
132            arguments: Arguments::new(buf.get(idx..)?),
133            name: command,
134            prefix,
135        })
136    }
137
138    fn find_command(&'a self, buf: &'a str) -> Option<&'a str> {
139        let buf = buf.split_whitespace().next()?;
140        self.config.commands.iter().find_map(|command| {
141            if command == buf {
142                Some(command.as_ref())
143            } else {
144                None
145            }
146        })
147    }
148
149    fn find_prefix(&self, buf: &str) -> Option<&str> {
150        self.config.prefixes.iter().find_map(|prefix| {
151            if buf.starts_with(prefix.as_ref()) {
152                Some(prefix.as_ref())
153            } else {
154                None
155            }
156        })
157    }
158}
159
160impl<'a, T: Into<CommandParserConfig<'a>>> From<T> for Parser<'a> {
161    fn from(config: T) -> Self {
162        Self::new(config)
163    }
164}
165
166#[cfg(test)]
167mod tests {
168    use crate::{Command, CommandParserConfig, Parser};
169    use static_assertions::{assert_fields, assert_impl_all};
170    use std::fmt::Debug;
171
172    assert_fields!(Command<'_>: arguments, name, prefix);
173    assert_impl_all!(Command<'_>: Clone, Debug, Send, Sync);
174    assert_impl_all!(Parser<'_>: Clone, Debug, Send, Sync);
175
176    fn simple_config() -> Parser<'static> {
177        let mut config = CommandParserConfig::new();
178        config.add_prefix("!");
179        config.add_command("echo", false);
180
181        Parser::new(config)
182    }
183
184    #[test]
185    fn double_command() {
186        let parser = simple_config();
187
188        assert!(parser.parse("!echoecho").is_none(), "double match");
189    }
190
191    #[test]
192    fn test_case_sensitive() {
193        let mut parser = simple_config();
194        let message_ascii = "!EcHo this is case insensitive";
195        let message_unicode = "!wEiSS is white";
196        let message_unicode_2 = "!\u{3b4} is delta";
197
198        // Case insensitive - ASCII
199        let command = parser
200            .parse(message_ascii)
201            .expect("Parser is case sensitive");
202        assert_eq!(
203            "echo", command.name,
204            "Command name should have the same case as in the CommandParserConfig"
205        );
206
207        // Case insensitive - Unicode
208        parser.config.add_command("wei\u{df}", false);
209        let command = parser
210            .parse(message_unicode)
211            .expect("Parser is case sensitive");
212        assert_eq!(
213            "wei\u{df}", command.name,
214            "Command name should have the same case as in the CommandParserConfig"
215        );
216
217        parser.config.add_command("\u{394}", false);
218        let command = parser
219            .parse(message_unicode_2)
220            .expect("Parser is case sensitive");
221        assert_eq!(
222            "\u{394}", command.name,
223            "Command name should have the same case as in the CommandParserConfig"
224        );
225
226        // Case sensitive
227        let config = parser.config_mut();
228        config.commands.clear();
229        config.add_command("echo", true);
230        config.add_command("wei\u{df}", true);
231        config.add_command("\u{394}", true);
232        assert!(
233            parser.parse(message_ascii).is_none(),
234            "Parser is not case sensitive"
235        );
236        assert!(
237            parser.parse(message_unicode).is_none(),
238            "Parser is not case sensitive"
239        );
240        assert!(
241            parser.parse(message_unicode_2).is_none(),
242            "Parser is not case sensitive"
243        );
244    }
245
246    #[test]
247    fn test_simple_config_no_prefix() {
248        let mut parser = simple_config();
249        parser.config_mut().remove_prefix("!");
250    }
251
252    #[test]
253    fn test_simple_config_parser() {
254        let parser = simple_config();
255
256        match parser.parse("!echo what a test") {
257            Some(Command { name, prefix, .. }) => {
258                assert_eq!("echo", name);
259                assert_eq!("!", prefix);
260            }
261            other => panic!("Not command: {:?}", other),
262        }
263    }
264
265    #[test]
266    fn test_unicode_command() {
267        let mut parser = simple_config();
268        parser.config_mut().add_command("\u{1f44e}", false);
269
270        assert!(parser.parse("!\u{1f44e}").is_some());
271    }
272
273    #[test]
274    fn test_unicode_prefix() {
275        let mut parser = simple_config();
276        parser.config_mut().add_prefix("\u{1f44d}"); // thumbs up unicode
277
278        assert!(parser.parse("\u{1f44d}echo foo").is_some());
279    }
280
281    #[test]
282    fn test_dynamic_prefix() {
283        let parser = simple_config();
284
285        let command = parser.parse_with_prefix("=", "=echo foo").unwrap();
286
287        assert_eq!("=", command.prefix);
288        assert_eq!("echo", command.name);
289    }
290
291    #[test]
292    fn test_prefix_mention() {
293        let mut config = CommandParserConfig::new();
294        config.add_prefix("foo");
295        config.add_command("dump", false);
296        let parser = Parser::new(config);
297
298        let Command {
299            mut arguments,
300            name,
301            prefix,
302        } = parser.parse("foo dump test").unwrap();
303        assert_eq!("foo", prefix);
304        assert_eq!("dump", name);
305        assert_eq!(Some("test"), arguments.next());
306        assert!(arguments.next().is_none());
307    }
308}