command_parser/
parser.rs

1use std::collections::{HashMap, HashSet};
2use crate::command::Command;
3use crate::error::ParseError;
4use crate::error::ParseError::{EscapeError, NameError, PrefixError};
5
6#[derive(Debug, Copy, Clone)]
7enum ParseState {
8    Prefix,
9    Name,
10    Default,
11    Argument,
12    LongArgument,
13    EscapeLongArg,
14    Option,
15    ParamConnector,
16    ParamVal,
17    ParamLongVal,
18    EscapeLongParamVal,
19}
20
21/// Used to parse a [`Command`] from a string.
22///
23/// # Command Syntax
24///
25/// For more information about prefixes look at the fields of this struct.
26/// In any examples in this documentation `!` will be used as a prefix and `-` will be used as a option prefix.
27///
28/// A command that this can parse could look like this:
29///
30/// `!foo arg1 "long arg 2" -opt -opt -key1:val1 -key2:"long val2"`
31///
32/// A command consists of 4 different parts:
33/// - _name_: The name of the command is the first word after the prefix.
34/// In the example above that's `foo`.
35/// - _arguments_: Arguments are simple strings passed to the command.
36/// They are either single words or strings with spaces enclosed by `"`.
37/// In the example the two arguments are `arg1` and `long arg 2`.
38/// - _options_: Options are a set of words.
39/// They are prefixed with the `option_prefix`.
40/// The only option in the example is `opt`.
41/// - _parameters_: Parameters are key-value pairs.
42/// They are prefixed with the `option_prefix` and seperated by `:`.
43/// The value part of the pair can be a word or a string enclosed by `"`.
44/// In the example above `key1`s value is `val1` and `key2`s value is `long val2`.
45///
46/// # Escaping
47///
48/// Since `"` is used to mark the borders of long arguments and values, it's not normally possible
49/// to include them in the string of the argument.
50///
51/// You can escape a long argument or value using \\:
52/// - `\"`: produces `"`
53/// - `\\`: produces `\`
54///
55/// # Example
56///
57/// ```
58/// use std::collections::{HashMap, HashSet};
59/// use command_parser::{Parser, Command};
60///
61/// let p = Parser::new('!', '-');
62/// let command_string = r##"!foo arg1 "long arg 2" -opt -opt -key1:val1 -key2:"long val2""##;
63///
64/// let command = Command {
65///     prefix: '!',
66///     option_prefix: '-',
67///     name: "foo".to_string(),
68///     arguments: vec!["arg1".to_string(), "long arg 2".to_string()],
69///     options: HashSet::from(["opt".to_string()]),
70///     parameters: HashMap::from([
71///         ("key1".to_string(), "val1".to_string()),
72///         ("key2".to_string(), "long val2".to_string())
73///     ])
74/// };
75///
76/// assert_eq!(p.parse(command_string).unwrap(), command);
77/// ```
78#[derive(Debug)]
79pub struct Parser {
80    /// Prefix of the command.
81    ///
82    /// `<prefix><name> ...`
83    ///
84    /// Should not be set to `' '` as most chats trim leading spaces.
85    pub prefix: char,
86    /// Prefix of options and parameters.
87    ///
88    /// `... <option_prefix><option> ... <option_prefix><param key>:<param value>`
89    ///
90    /// Should not be set to `' '` or `'"'` as it may not result in expected outcomes.
91    pub option_prefix: char,
92}
93
94impl Parser {
95    pub fn new(prefix: char, option_prefix: char) -> Parser {
96        Parser {
97            prefix,
98            option_prefix,
99        }
100    }
101
102    pub fn parse<'a>(&'_ self, raw: &'a str) -> Result<Command, ParseError> {
103        let mut name = String::new();
104        let mut arguments: Vec<String> = vec![];
105        let mut options: HashSet<String> = HashSet::new();
106        let mut parameters: HashMap<String, String> = HashMap::new();
107
108        let mut state = ParseState::Prefix;
109        let mut buffer = String::new();
110        let mut key_buffer = String::new();
111
112        for (cursor, c) in raw.chars().enumerate() {
113            match state {
114                ParseState::Prefix => {
115                    match c {
116                        x if x == self.prefix => {
117                            state = ParseState::Name;
118                        }
119                        _ => {return Err(PrefixError(cursor, c))}
120                    }
121                }
122                ParseState::Name => {
123                    match c {
124                        ' ' => {
125                            if cursor == 1 {
126                                return Err(NameError(cursor, c));
127                            } else {
128                                state = ParseState::Default;
129                            }
130                        }
131                        _ => { name.push(c); }
132                    }
133                }
134                ParseState::Argument => {
135                    match c {
136                        ' ' => {
137                            arguments.push(buffer);
138                            buffer = String::new();
139                            state = ParseState::Default;
140                        }
141                        _ => {
142                            buffer.push(c);
143                        }
144                    }
145                }
146                ParseState::LongArgument => {
147                    match c {
148                        '"' => {
149                            arguments.push(buffer);
150                            buffer = String::new();
151                            state = ParseState::Default;
152                        }
153                        '\\' => {
154                            state = ParseState::EscapeLongArg;
155                        }
156                        _ => {
157                            buffer.push(c);
158                        }
159                    }
160                }
161                ParseState::EscapeLongArg => {
162                    match c {
163                        '"' | '\\' => {
164                            state = ParseState::LongArgument;
165                            buffer.push(c);
166                        }
167                        _ => {
168                            return Err(EscapeError(cursor, c));
169                        }
170                    }
171                }
172                ParseState::Option => {
173                    match c {
174                        ' ' => {
175                            options.insert(buffer);
176                            buffer = String::new();
177                            state = ParseState::Default;
178                        }
179                        ':' => {
180                            key_buffer = buffer;
181                            buffer = String::new();
182                            state = ParseState::ParamConnector;
183                        }
184                        _ => {
185                            buffer.push(c);
186                        }
187                    }
188                }
189                ParseState::ParamConnector => {
190                    match c {
191                        '"' => {
192                            state = ParseState::ParamLongVal;
193                        }
194                        ' ' => {
195                            parameters.insert(key_buffer, buffer);
196                            key_buffer = String::new();
197                            buffer = String::new();
198                            state = ParseState::Default;
199                        }
200                        _ => {
201                            state = ParseState::ParamVal;
202                            buffer.push(c);
203                        }
204                    }
205                }
206                ParseState::ParamVal => {
207                    match c {
208                        ' ' => {
209                            parameters.insert(key_buffer, buffer);
210                            key_buffer = String::new();
211                            buffer = String::new();
212                            state = ParseState::Default;
213                        }
214                        _ => {
215                            buffer.push(c);
216                        }
217                    }
218                }
219                ParseState::ParamLongVal => {
220                    match c {
221                        '"' => {
222                            parameters.insert(key_buffer, buffer);
223                            key_buffer = String::new();
224                            buffer = String::new();
225                            state = ParseState::Default;
226                        }
227                        '\\' => {
228                            state = ParseState::EscapeLongParamVal;
229                        }
230                        _ => {
231                            buffer.push(c);
232                        }
233                    }
234                }
235                ParseState::EscapeLongParamVal => {
236                    match c {
237                        '"' | '\\' => {
238                            state = ParseState::ParamLongVal;
239                            buffer.push(c);
240                        }
241                        _ => {
242                            return Err(EscapeError(cursor, c));
243                        }
244                    }
245                }
246                ParseState::Default => {
247                    match c {
248                        ' ' => {}
249                        '"' => {state = ParseState::LongArgument;}
250                        x if x == self.option_prefix => {
251                            state = ParseState::Option;
252                        }
253                        _ => {
254                            state = ParseState::Argument;
255                            buffer.push(c);
256                        }
257                    }
258                }
259            }
260        }
261
262        Ok(Command {
263            prefix: self.prefix,
264            option_prefix: self.option_prefix,
265            name, arguments, options, parameters
266        })
267    }
268}
269
270
271#[cfg(test)]
272pub mod tests {
273    use std::time::{Duration, Instant};
274    use super::*;
275
276    #[test]
277    fn parse_test() {
278        let p = Parser::new('!', '-');
279        let command_string = r##"!foo arg1 "long arg 2" -opt -opt -key1:val1 -key2:"long val2""##;
280
281        let command = Command {
282            prefix: '!',
283            option_prefix: '-',
284            name: "foo".to_string(),
285            arguments: vec!["arg1".to_string(), "long arg 2".to_string()],
286            options: HashSet::from(["opt".to_string()]),
287            parameters: HashMap::from([
288                ("key1".to_string(), "val1".to_string()),
289                ("key2".to_string(), "long val2".to_string())
290            ])
291        };
292
293        assert_eq!(p.parse(command_string).unwrap(), command);
294    }
295
296    #[test]
297    fn time_test() {
298        let p = Parser::new('!', '-');
299        let command_string = r##"!foo arg1 "long arg 2" -opt -opt -key1:val1 -key2:"long val2""##;
300
301        let now = Instant::now();
302
303        for _ in 0..100000 {
304            let _ = p.parse(command_string);
305        }
306
307        println!("{}", now.elapsed().as_micros());
308
309        let p = Parser::new('!', '-');
310        let command_string = r##"just a normal sentence"##;
311
312        let now = Instant::now();
313
314        for _ in 0..100000 {
315            let _ = p.parse(command_string);
316        }
317
318        println!("{}", now.elapsed().as_micros());
319    }
320
321}