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}