aboutsummaryrefslogtreecommitdiff
path: root/src/command.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/command.rs')
-rw-r--r--src/command.rs514
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)]
108pub enum Command { 108pub 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)]
122enum 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)]
122pub enum CommandLineError { 137pub 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
144impl Command { 159impl 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
183fn 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
205fn 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" => { 237fn 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
268fn 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
284fn 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
300fn 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
310fn 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
320fn 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
330fn 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)]
341mod 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}