diff options
-rw-r--r-- | src/command.rs | 514 |
1 files changed, 465 insertions, 49 deletions
diff --git a/src/command.rs b/src/command.rs index 0372065..0ef5121 100644 --- a/src/command.rs +++ b/src/command.rs | |||
@@ -104,7 +104,7 @@ fn call_on_app(s: &mut Cursive, input: &str) { | |||
104 | } | 104 | } |
105 | } | 105 | } |
106 | 106 | ||
107 | #[derive(PartialEq)] | 107 | #[derive(PartialEq, Debug)] |
108 | pub enum Command { | 108 | pub enum Command { |
109 | Add(String, Option<u32>, bool), | 109 | Add(String, Option<u32>, bool), |
110 | MonthPrev, | 110 | MonthPrev, |
@@ -118,7 +118,22 @@ pub enum Command { | |||
118 | Blank, | 118 | Blank, |
119 | } | 119 | } |
120 | 120 | ||
121 | #[derive(Debug)] | 121 | #[derive(PartialEq, Debug)] |
122 | enum CommandName { | ||
123 | Add, | ||
124 | AddAuto, | ||
125 | MonthPrev, | ||
126 | MonthNext, | ||
127 | Delete, | ||
128 | TrackUp, | ||
129 | TrackDown, | ||
130 | Help, | ||
131 | Write, | ||
132 | Quit, | ||
133 | Blank, | ||
134 | } | ||
135 | |||
136 | #[derive(PartialEq, Debug)] | ||
122 | pub enum CommandLineError { | 137 | pub enum CommandLineError { |
123 | InvalidCommand(String), // command name | 138 | InvalidCommand(String), // command name |
124 | InvalidArg(u32), // position | 139 | InvalidArg(u32), // position |
@@ -143,64 +158,465 @@ type Result<T> = std::result::Result<T, CommandLineError>; | |||
143 | 158 | ||
144 | impl Command { | 159 | impl Command { |
145 | pub fn from_string<P: AsRef<str>>(input: P) -> Result<Command> { | 160 | pub fn from_string<P: AsRef<str>>(input: P) -> Result<Command> { |
146 | let mut strings: Vec<&str> = input.as_ref().trim().split(' ').collect(); | 161 | let input_str = input.as_ref().trim(); |
147 | if strings.is_empty() { | 162 | let parsed = parse_command_name(input_str); |
148 | return Ok(Command::Blank); | 163 | if let Ok((command_name, rest)) = parsed { |
164 | match command_name { | ||
165 | CommandName::Add => return parse_add(rest), | ||
166 | CommandName::AddAuto => return parse_add_auto(rest), | ||
167 | CommandName::Delete => return parse_delete(rest), | ||
168 | CommandName::TrackUp => return parse_track_up(rest), | ||
169 | CommandName::TrackDown => return parse_track_down(rest), | ||
170 | CommandName::Help => return parse_help(rest), | ||
171 | CommandName::MonthPrev => return Ok(Command::MonthPrev), | ||
172 | CommandName::MonthNext => return Ok(Command::MonthNext), | ||
173 | CommandName::Quit => return Ok(Command::Quit), | ||
174 | CommandName::Write => return Ok(Command::Write), | ||
175 | CommandName::Blank => return Ok(Command::Blank), | ||
176 | } | ||
177 | } else { | ||
178 | return Err(parsed.err().unwrap()); | ||
149 | } | 179 | } |
180 | } | ||
181 | } | ||
182 | |||
183 | fn parse_command_name(input: &str) -> Result<(CommandName, &str)> { | ||
184 | let pieces: Vec<&str> = input.trim().splitn(2, ' ').collect(); | ||
150 | 185 | ||
151 | let first = strings.first().unwrap().to_string(); | 186 | let command = pieces.first().unwrap(); |
152 | let mut args: Vec<String> = strings.iter_mut().skip(1).map(|s| s.to_string()).collect(); | 187 | let rest = pieces.iter().skip(1).next().map(|&x| x).unwrap_or(""); |
153 | let mut _add = |auto: bool, first: String| { | 188 | |
154 | if args.is_empty() { | 189 | match command.as_ref() { |
155 | return Err(CommandLineError::NotEnoughArgs(first, 1)); | 190 | "add" | "a" => Ok((CommandName::Add, rest)), |
191 | "add-auto" | "aa" => Ok((CommandName::AddAuto, rest)), | ||
192 | "delete" | "d" => Ok((CommandName::Delete, rest)), | ||
193 | "track-up" | "tup" => Ok((CommandName::TrackUp, rest)), | ||
194 | "track-down" | "tdown" => Ok((CommandName::TrackDown, rest)), | ||
195 | "h" | "?" | "help" => Ok((CommandName::Help, rest)), | ||
196 | "mprev" => Ok((CommandName::MonthPrev, "")), | ||
197 | "mnext" => Ok((CommandName::MonthNext, "")), | ||
198 | "quit" | "q" => Ok((CommandName::Quit, "")), | ||
199 | "write" | "w" => Ok((CommandName::Write, "")), | ||
200 | "" => Ok((CommandName::Blank, "")), | ||
201 | c => Err(CommandLineError::InvalidCommand(c.to_string())), | ||
202 | } | ||
203 | } | ||
204 | |||
205 | fn parse_name(input: &str) -> (String, &str) { | ||
206 | let chars = input.trim().chars(); | ||
207 | let mut name = "".to_owned(); | ||
208 | let mut pos = 0; | ||
209 | let mut parenthesis = false; | ||
210 | |||
211 | for c in chars { | ||
212 | pos = pos + 1; | ||
213 | if c == '"' || c == '\"' { | ||
214 | if parenthesis { | ||
215 | return (name, &input[pos..]); | ||
216 | } else { | ||
217 | parenthesis = true; | ||
218 | continue; | ||
156 | } | 219 | } |
157 | let goal = args | 220 | } |
158 | .get(1) | 221 | |
159 | .map(|x| { | 222 | if parenthesis { |
160 | x.parse::<u32>() | 223 | name.push(c); |
161 | .map_err(|_| CommandLineError::InvalidArg(2)) | 224 | continue; |
162 | }) | 225 | } |
163 | .transpose()?; | 226 | |
164 | return Ok(Command::Add( | 227 | if c == ' ' { |
165 | args.get_mut(0).unwrap().to_string(), | 228 | break; |
166 | goal, | 229 | } |
167 | auto, | 230 | |
168 | )); | 231 | name.push(c); |
169 | }; | 232 | } |
170 | 233 | ||
171 | match first.as_ref() { | 234 | (name, &input[pos..]) |
172 | "add" | "a" => _add(false, first), | 235 | } |
173 | "add-auto" | "aa" => _add(true, first), | 236 | |
174 | "delete" | "d" => { | 237 | fn parse_goal(input: &str) -> Option<(Option<u32>, &str)> { |
175 | if args.is_empty() { | 238 | let chars = input.trim().chars(); |
176 | return Err(CommandLineError::NotEnoughArgs(first, 1)); | 239 | let mut goal_string = "".to_owned(); |
240 | let mut pos = 0; | ||
241 | |||
242 | if input.is_empty() { | ||
243 | return Some((None, input)); | ||
244 | } | ||
245 | |||
246 | for c in chars { | ||
247 | pos = pos + 1; | ||
248 | if c == ' ' { | ||
249 | break; | ||
250 | } | ||
251 | |||
252 | goal_string.push(c); | ||
253 | } | ||
254 | |||
255 | let parsed = goal_string.parse::<u32>(); | ||
256 | |||
257 | if parsed.is_err() { | ||
258 | return None; | ||
259 | } | ||
260 | |||
261 | if pos + 1 > input.len() { | ||
262 | return Some((parsed.ok(), "")); | ||
263 | } | ||
264 | |||
265 | Some((parsed.ok(), &input[pos..])) | ||
266 | } | ||
267 | |||
268 | fn parse_add(input: &str) -> Result<Command> { | ||
269 | let (name, rest) = parse_name(input); | ||
270 | |||
271 | if name.is_empty() { | ||
272 | return Err(CommandLineError::NotEnoughArgs("add".to_owned(), 2)); | ||
273 | } | ||
274 | |||
275 | let parsed_goal = parse_goal(rest); | ||
276 | |||
277 | if parsed_goal.is_none() { | ||
278 | return Err(CommandLineError::InvalidArg(2)); | ||
279 | } | ||
280 | |||
281 | Ok(Command::Add(name, parsed_goal.unwrap().0, false)) | ||
282 | } | ||
283 | |||
284 | fn parse_add_auto(input: &str) -> Result<Command> { | ||
285 | let (name, rest) = parse_name(input); | ||
286 | |||
287 | if name.is_empty() { | ||
288 | return Err(CommandLineError::NotEnoughArgs("add-auto".to_owned(), 2)); | ||
289 | } | ||
290 | |||
291 | let parsed_goal = parse_goal(rest); | ||
292 | |||
293 | if parsed_goal.is_none() { | ||
294 | return Err(CommandLineError::InvalidArg(2)); | ||
295 | } | ||
296 | |||
297 | Ok(Command::Add(name, parsed_goal.unwrap().0, true)) | ||
298 | } | ||
299 | |||
300 | fn parse_delete(input: &str) -> Result<Command> { | ||
301 | let (name, _) = parse_name(input); | ||
302 | |||
303 | if name.is_empty() { | ||
304 | return Err(CommandLineError::NotEnoughArgs("delete".to_owned(), 1)); | ||
305 | } | ||
306 | |||
307 | Ok(Command::Delete(name)) | ||
308 | } | ||
309 | |||
310 | fn parse_track_up(input: &str) -> Result<Command> { | ||
311 | let (name, _) = parse_name(input); | ||
312 | |||
313 | if name.is_empty() { | ||
314 | return Err(CommandLineError::NotEnoughArgs("track-up".to_owned(), 1)); | ||
315 | } | ||
316 | |||
317 | Ok(Command::TrackUp(name)) | ||
318 | } | ||
319 | |||
320 | fn parse_track_down(input: &str) -> Result<Command> { | ||
321 | let (name, _) = parse_name(input); | ||
322 | |||
323 | if name.is_empty() { | ||
324 | return Err(CommandLineError::NotEnoughArgs("track-down".to_owned(), 1)); | ||
325 | } | ||
326 | |||
327 | Ok(Command::TrackDown(name)) | ||
328 | } | ||
329 | |||
330 | fn parse_help(input: &str) -> Result<Command> { | ||
331 | let (name, _) = parse_name(input); | ||
332 | |||
333 | if name.is_empty() { | ||
334 | return Ok(Command::Help(None)); | ||
335 | } | ||
336 | |||
337 | Ok(Command::Help(Some(name))) | ||
338 | } | ||
339 | |||
340 | #[cfg(test)] | ||
341 | mod tests { | ||
342 | use super::*; | ||
343 | |||
344 | #[test] | ||
345 | fn parse_add_command() { | ||
346 | let inputs = ["add eat 2", "a eat 2"]; | ||
347 | |||
348 | for input in inputs.iter() { | ||
349 | let result = Command::from_string(input); | ||
350 | |||
351 | assert!(result.is_ok()); | ||
352 | match result.unwrap() { | ||
353 | Command::Add(name, goal, auto) => { | ||
354 | assert_eq!(name, "eat"); | ||
355 | assert_eq!(goal.unwrap(), 2); | ||
356 | assert_eq!(auto, false); | ||
177 | } | 357 | } |
178 | return Ok(Command::Delete(args[0].to_string())); | 358 | _ => panic!(), |
179 | } | 359 | } |
180 | "track-up" | "tup" => { | 360 | } |
181 | if args.is_empty() { | 361 | } |
182 | return Err(CommandLineError::NotEnoughArgs(first, 1)); | 362 | |
363 | #[test] | ||
364 | fn parse_add_command_without_goal() { | ||
365 | let inputs = ["add eat", "a eat"]; | ||
366 | |||
367 | for input in inputs.iter() { | ||
368 | let result = Command::from_string(input); | ||
369 | |||
370 | assert!(result.is_ok()); | ||
371 | match result.unwrap() { | ||
372 | Command::Add(name, goal, auto) => { | ||
373 | assert_eq!(name, "eat"); | ||
374 | assert!(goal.is_none()); | ||
375 | assert_eq!(auto, false); | ||
183 | } | 376 | } |
184 | return Ok(Command::TrackUp(args[0].to_string())); | 377 | _ => panic!(), |
185 | } | 378 | } |
186 | "track-down" | "tdown" => { | 379 | } |
187 | if args.is_empty() { | 380 | } |
188 | return Err(CommandLineError::NotEnoughArgs(first, 1)); | 381 | |
382 | #[test] | ||
383 | fn parse_add_command_with_long_name() { | ||
384 | let inputs = ["add \"eat healthy\" 5", "a \"eat healthy\" 5"]; | ||
385 | |||
386 | for input in inputs.iter() { | ||
387 | let result = Command::from_string(input); | ||
388 | |||
389 | assert!(result.is_ok()); | ||
390 | match result.unwrap() { | ||
391 | Command::Add(name, goal, auto) => { | ||
392 | assert_eq!(name, "eat healthy"); | ||
393 | assert_eq!(goal.unwrap(), 5); | ||
394 | assert_eq!(auto, false); | ||
395 | } | ||
396 | _ => panic!(), | ||
397 | } | ||
398 | } | ||
399 | } | ||
400 | |||
401 | #[test] | ||
402 | fn parse_add_auto_command() { | ||
403 | let inputs = ["add-auto eat 2", "aa eat 2"]; | ||
404 | |||
405 | for input in inputs.iter() { | ||
406 | let result = Command::from_string(input); | ||
407 | |||
408 | assert!(result.is_ok()); | ||
409 | match result.unwrap() { | ||
410 | Command::Add(name, goal, auto) => { | ||
411 | assert_eq!(name, "eat"); | ||
412 | assert_eq!(goal.unwrap(), 2); | ||
413 | assert_eq!(auto, true); | ||
414 | } | ||
415 | _ => panic!(), | ||
416 | } | ||
417 | } | ||
418 | } | ||
419 | |||
420 | #[test] | ||
421 | fn parse_delete_command() { | ||
422 | let inputs = ["delete eat", "d eat"]; | ||
423 | |||
424 | for input in inputs.iter() { | ||
425 | let result = Command::from_string(input); | ||
426 | |||
427 | assert!(result.is_ok()); | ||
428 | match result.unwrap() { | ||
429 | Command::Delete(name) => { | ||
430 | assert_eq!(name, "eat"); | ||
431 | } | ||
432 | _ => panic!(), | ||
433 | } | ||
434 | } | ||
435 | } | ||
436 | |||
437 | #[test] | ||
438 | fn parse_delete_long_name_command() { | ||
439 | let inputs = ["delete \"eat healthy\"", "d \"eat healthy\""]; | ||
440 | |||
441 | for input in inputs.iter() { | ||
442 | let result = Command::from_string(input); | ||
443 | |||
444 | assert!(result.is_ok()); | ||
445 | match result.unwrap() { | ||
446 | Command::Delete(name) => { | ||
447 | assert_eq!(name, "eat healthy"); | ||
448 | } | ||
449 | _ => panic!(), | ||
450 | } | ||
451 | } | ||
452 | } | ||
453 | |||
454 | #[test] | ||
455 | fn parse_track_up_command() { | ||
456 | let inputs = ["track-up eat", "tup eat"]; | ||
457 | |||
458 | for input in inputs.iter() { | ||
459 | let result = Command::from_string(input); | ||
460 | |||
461 | assert!(result.is_ok()); | ||
462 | match result.unwrap() { | ||
463 | Command::TrackUp(name) => { | ||
464 | assert_eq!(name, "eat"); | ||
465 | } | ||
466 | _ => panic!(), | ||
467 | } | ||
468 | } | ||
469 | } | ||
470 | |||
471 | #[test] | ||
472 | fn parse_track_down_command() { | ||
473 | let inputs = ["track-down eat", "tdown eat"]; | ||
474 | |||
475 | for input in inputs.iter() { | ||
476 | let result = Command::from_string(input); | ||
477 | |||
478 | assert!(result.is_ok()); | ||
479 | match result.unwrap() { | ||
480 | Command::TrackDown(name) => { | ||
481 | assert_eq!(name, "eat"); | ||
482 | } | ||
483 | _ => panic!(), | ||
484 | } | ||
485 | } | ||
486 | } | ||
487 | |||
488 | #[test] | ||
489 | fn parse_help_command() { | ||
490 | let inputs = ["help add", "? add", "h add"]; | ||
491 | |||
492 | for input in inputs.iter() { | ||
493 | let result = Command::from_string(input); | ||
494 | |||
495 | assert!(result.is_ok()); | ||
496 | match result.unwrap() { | ||
497 | Command::Help(name) => { | ||
498 | assert_eq!(name.unwrap(), "add"); | ||
189 | } | 499 | } |
190 | return Ok(Command::TrackDown(args[0].to_string())); | 500 | _ => panic!(), |
191 | } | 501 | } |
192 | "h" | "?" | "help" => { | 502 | } |
193 | if args.is_empty() { | 503 | } |
194 | return Ok(Command::Help(None)); | 504 | |
505 | #[test] | ||
506 | fn parse_help_global_command() { | ||
507 | let inputs = ["help", "?", "h"]; | ||
508 | |||
509 | for input in inputs.iter() { | ||
510 | let result = Command::from_string(input); | ||
511 | |||
512 | assert!(result.is_ok()); | ||
513 | match result.unwrap() { | ||
514 | Command::Help(name) => { | ||
515 | assert!(name.is_none()); | ||
195 | } | 516 | } |
196 | return Ok(Command::Help(Some(args[0].to_string()))); | 517 | _ => panic!(), |
197 | } | 518 | } |
198 | "mprev" | "month-prev" => return Ok(Command::MonthPrev), | 519 | } |
199 | "mnext" | "month-next" => return Ok(Command::MonthNext), | 520 | } |
200 | "q" | "quit" => return Ok(Command::Quit), | 521 | |
201 | "w" | "write" => return Ok(Command::Write), | 522 | #[test] |
202 | "" => return Ok(Command::Blank), | 523 | fn parse_month_prev_command() { |
203 | s => return Err(CommandLineError::InvalidCommand(s.into())), | 524 | let result = Command::from_string("mprev"); |
525 | |||
526 | assert!(result.is_ok()); | ||
527 | assert_eq!(result.unwrap(), Command::MonthPrev); | ||
528 | } | ||
529 | |||
530 | #[test] | ||
531 | fn parse_month_next_command() { | ||
532 | let result = Command::from_string("mnext"); | ||
533 | |||
534 | assert!(result.is_ok()); | ||
535 | assert_eq!(result.unwrap(), Command::MonthNext); | ||
536 | } | ||
537 | |||
538 | #[test] | ||
539 | fn parse_quit_command() { | ||
540 | let inputs = ["q", "quit"]; | ||
541 | |||
542 | for input in inputs.iter() { | ||
543 | let result = Command::from_string(input); | ||
544 | |||
545 | assert!(result.is_ok()); | ||
546 | assert_eq!(result.unwrap(), Command::Quit); | ||
547 | } | ||
548 | } | ||
549 | |||
550 | #[test] | ||
551 | fn parse_write_command() { | ||
552 | let inputs = ["w", "write"]; | ||
553 | |||
554 | for input in inputs.iter() { | ||
555 | let result = Command::from_string(input); | ||
556 | |||
557 | assert!(result.is_ok()); | ||
558 | assert_eq!(result.unwrap(), Command::Write); | ||
559 | } | ||
560 | } | ||
561 | |||
562 | #[test] | ||
563 | fn parse_no_command() { | ||
564 | let result = Command::from_string(""); | ||
565 | |||
566 | assert!(result.is_ok()); | ||
567 | assert_eq!(result.unwrap(), Command::Blank); | ||
568 | } | ||
569 | |||
570 | #[test] | ||
571 | fn parse_error_invalid_command() { | ||
572 | let input = "non-existing-command".to_owned(); | ||
573 | let result = Command::from_string(&input); | ||
574 | |||
575 | assert!(result.is_err()); | ||
576 | assert_eq!( | ||
577 | result.err().unwrap(), | ||
578 | CommandLineError::InvalidCommand(input) | ||
579 | ); | ||
580 | } | ||
581 | |||
582 | #[test] | ||
583 | fn parse_error_not_enough_args() { | ||
584 | let test_cases = [ | ||
585 | ("add", "add", 2), | ||
586 | ("add-auto", "add-auto", 2), | ||
587 | ("delete", "delete", 1), | ||
588 | ("track-up", "track-up", 1), | ||
589 | ("track-down", "track-down", 1), | ||
590 | ] | ||
591 | .iter() | ||
592 | .map(|(a, b, c)| (a.to_owned(), b.to_owned(), c)); | ||
593 | |||
594 | for test_case in test_cases { | ||
595 | let result = Command::from_string(test_case.0); | ||
596 | |||
597 | assert!(result.is_err()); | ||
598 | assert_eq!( | ||
599 | result.err().unwrap(), | ||
600 | CommandLineError::NotEnoughArgs(test_case.1.to_string(), *test_case.2) | ||
601 | ); | ||
602 | } | ||
603 | } | ||
604 | |||
605 | #[test] | ||
606 | fn parse_error_invalid_arg() { | ||
607 | let test_cases = [ | ||
608 | ("add habit n".to_owned(), 2), | ||
609 | ("add-auto habit n".to_owned(), 2), | ||
610 | ]; | ||
611 | |||
612 | for test_case in test_cases.iter() { | ||
613 | let result = Command::from_string(&test_case.0); | ||
614 | |||
615 | assert!(result.is_err()); | ||
616 | assert_eq!( | ||
617 | result.err().unwrap(), | ||
618 | CommandLineError::InvalidArg(test_case.1) | ||
619 | ); | ||
204 | } | 620 | } |
205 | } | 621 | } |
206 | } | 622 | } |