Review comment: Use combine to parse arguments.

Move over to using Result rather than enums with err
This commit is contained in:
Nick Alexander 2017-05-16 16:40:33 -07:00 committed by Emily Toop
parent 45d00c43ac
commit b3ff534690
7 changed files with 189 additions and 127 deletions

View file

@ -19,6 +19,7 @@ log = "0.3"
tempfile = "1.1" tempfile = "1.1"
combine = "2.2.2" combine = "2.2.2"
lazy_static = "0.2.2" lazy_static = "0.2.2"
error-chain = "0.8.1"
[dependencies.rusqlite] [dependencies.rusqlite]
version = "0.11" version = "0.11"

View file

@ -8,9 +8,25 @@
// CONDITIONS OF ANY KIND, either express or implied. See the License for the // CONDITIONS OF ANY KIND, either express or implied. See the License for the
// specific language governing permissions and limitations under the License. // specific language governing permissions and limitations under the License.
use combine::{any, eof, many, many1, sep_by, skip_many, token, Parser}; use combine::{
use combine::combinator::{choice, try}; eof,
use combine::char::{space, string}; many1,
satisfy,
sep_end_by,
token,
Parser
};
use combine::char::{
space,
spaces,
string
};
use combine::combinator::{
choice,
try
};
use errors as cli;
pub static HELP_COMMAND: &'static str = &"help"; pub static HELP_COMMAND: &'static str = &"help";
pub static OPEN_COMMAND: &'static str = &"open"; pub static OPEN_COMMAND: &'static str = &"open";
@ -23,58 +39,67 @@ pub enum Command {
Help(Vec<String>), Help(Vec<String>),
Open(String), Open(String),
Close, Close,
Err(String),
} }
impl Command { impl Command {
/// is_complete returns true if no more input is required for the command to be successfully executed.
/// false is returned if the command is not considered valid.
/// Defaults to true for all commands except Query and Transact.
/// TODO: for query and transact commands, they will be considered complete if a parsable EDN has been entered as an argument
pub fn is_complete(&self) -> bool { pub fn is_complete(&self) -> bool {
match self { match self {
&Command::Query(_) | &Command::Query(_) |
&Command::Transact(_) => false, &Command::Transact(_) => false,
&Command::Help(_) | &Command::Help(_) |
&Command::Open(_) | &Command::Open(_) |
&Command::Close | &Command::Close => true
&Command::Err(_) => true
} }
} }
} }
pub fn command(s: &str) -> Command { pub fn command(s: &str) -> Result<Command, cli::Error> {
let arguments = || sep_end_by::<Vec<_>, _, _>(many1(satisfy(|c: char| !c.is_whitespace())), many1::<Vec<_>, _>(space())).expected("arguments");
let help_parser = string(HELP_COMMAND) let help_parser = string(HELP_COMMAND)
.and(skip_many(space())) .with(spaces())
.and(many::<Vec<_>, _>(try(any()))) .with(arguments())
.map(|x| { .map(|args| {
let remainder: String = x.1.iter().collect(); Ok(Command::Help(args.clone()))
let args: Vec<String> = remainder.split(" ").filter_map(|s| if s.is_empty() { None } else { Some(s.to_string())}).collect();
Command::Help(args)
}); });
let open_parser = string(OPEN_COMMAND) let open_parser = string(OPEN_COMMAND)
.and(skip_many(space())) .with(spaces())
.and(many1::<Vec<_>, _>(try(any()))) .with(arguments())
.map(|x| { .map(|args| {
let remainder: String = x.1.iter().collect(); if args.len() < 1 {
let args: Vec<String> = remainder.split(" ").filter_map(|s| if s.is_empty() { None } else { Some(s.to_string())}).collect(); return Err(cli::ErrorKind::CommandParse("Missing required argument".to_string()).into());
if args.len() > 1 {
return Command::Err(format!("Unrecognized argument {:?}", (&args[1]).clone()));
} }
Command::Open((&args[0]).clone()) if args.len() > 1 {
return Err(cli::ErrorKind::CommandParse(format!("Unrecognized argument {:?}", args[1])).into());
}
Ok(Command::Open(args[0].clone()))
}); });
let close_parser = string(CLOSE_COMMAND) let close_parser = string(CLOSE_COMMAND)
.and(skip_many(space())) .with(arguments())
.map( |_| Command::Close ); .map(|args| {
if args.len() > 0 {
return Err(cli::ErrorKind::CommandParse(format!("Unrecognized argument {:?}", args[0])).into());
}
Ok(Command::Close)
});
skip_many(space()) spaces()
.and(token('.')) .skip(token('.'))
.and(choice::<[&mut Parser<Input = _, Output = Command>; 3], _> .with(choice::<[&mut Parser<Input = _, Output = Result<Command, cli::Error>>; 3], _>
([&mut try(help_parser), ([&mut try(help_parser),
&mut try(open_parser), &mut try(open_parser),
&mut try(close_parser),])) &mut try(close_parser),]))
.skip(eof()) .skip(spaces())
.parse(s) .skip(eof())
.map(|x| x.0) .parse(s)
.unwrap_or((((), '0'), Command::Err(format!("Invalid command {:?}", s)))).1 .map(|x| x.0)
.unwrap_or(Err(cli::ErrorKind::CommandParse(format!("Invalid command {:?}", s)).into()))
} }
#[cfg(test)] #[cfg(test)]
@ -84,7 +109,7 @@ mod tests {
#[test] #[test]
fn test_help_parser_multiple_args() { fn test_help_parser_multiple_args() {
let input = ".help command1 command2"; let input = ".help command1 command2";
let cmd = command(&input); let cmd = command(&input).expect("Expected help command");
match cmd { match cmd {
Command::Help(args) => { Command::Help(args) => {
assert_eq!(args, vec!["command1", "command2"]); assert_eq!(args, vec!["command1", "command2"]);
@ -96,7 +121,7 @@ mod tests {
#[test] #[test]
fn test_help_parser_dot_arg() { fn test_help_parser_dot_arg() {
let input = ".help .command1"; let input = ".help .command1";
let cmd = command(&input); let cmd = command(&input).expect("Expected help command");
match cmd { match cmd {
Command::Help(args) => { Command::Help(args) => {
assert_eq!(args, vec![".command1"]); assert_eq!(args, vec![".command1"]);
@ -108,7 +133,7 @@ mod tests {
#[test] #[test]
fn test_help_parser_no_args() { fn test_help_parser_no_args() {
let input = ".help"; let input = ".help";
let cmd = command(&input); let cmd = command(&input).expect("Expected help command");
match cmd { match cmd {
Command::Help(args) => { Command::Help(args) => {
let empty: Vec<String> = vec![]; let empty: Vec<String> = vec![];
@ -121,7 +146,7 @@ mod tests {
#[test] #[test]
fn test_help_parser_no_args_trailing_whitespace() { fn test_help_parser_no_args_trailing_whitespace() {
let input = ".help "; let input = ".help ";
let cmd = command(&input); let cmd = command(&input).expect("Expected help command");
match cmd { match cmd {
Command::Help(args) => { Command::Help(args) => {
let empty: Vec<String> = vec![]; let empty: Vec<String> = vec![];
@ -134,19 +159,14 @@ mod tests {
#[test] #[test]
fn test_open_parser_multiple_args() { fn test_open_parser_multiple_args() {
let input = ".open database1 database2"; let input = ".open database1 database2";
let cmd = command(&input); let err = command(&input).expect_err("Expected an error");
match cmd { assert_eq!(err.to_string(), "Unrecognized argument \"database2\"");
Command::Err(message) => {
assert_eq!(message, "Unrecognized argument \"database2\"");
},
_ => assert!(false)
}
} }
#[test] #[test]
fn test_open_parser_single_arg() { fn test_open_parser_single_arg() {
let input = ".open database1"; let input = ".open database1";
let cmd = command(&input); let cmd = command(&input).expect("Expected open command");
match cmd { match cmd {
Command::Open(arg) => { Command::Open(arg) => {
assert_eq!(arg, "database1".to_string()); assert_eq!(arg, "database1".to_string());
@ -158,7 +178,7 @@ mod tests {
#[test] #[test]
fn test_open_parser_path_arg() { fn test_open_parser_path_arg() {
let input = ".open /path/to/my.db"; let input = ".open /path/to/my.db";
let cmd = command(&input); let cmd = command(&input).expect("Expected open command");
match cmd { match cmd {
Command::Open(arg) => { Command::Open(arg) => {
assert_eq!(arg, "/path/to/my.db".to_string()); assert_eq!(arg, "/path/to/my.db".to_string());
@ -170,7 +190,7 @@ mod tests {
#[test] #[test]
fn test_open_parser_file_arg() { fn test_open_parser_file_arg() {
let input = ".open my.db"; let input = ".open my.db";
let cmd = command(&input); let cmd = command(&input).expect("Expected open command");
match cmd { match cmd {
Command::Open(arg) => { Command::Open(arg) => {
assert_eq!(arg, "my.db".to_string()); assert_eq!(arg, "my.db".to_string());
@ -182,31 +202,21 @@ mod tests {
#[test] #[test]
fn test_open_parser_no_args() { fn test_open_parser_no_args() {
let input = ".open"; let input = ".open";
let cmd = command(&input); let err = command(&input).expect_err("Expected an error");
match cmd { assert_eq!(err.to_string(), "Missing required argument");
Command::Err(message) => {
assert_eq!(message, format!("Invalid command {:?}", input));
},
_ => assert!(false)
}
} }
#[test] #[test]
fn test_close_parser_with_args() { fn test_close_parser_with_args() {
let input = ".close arg1"; let input = ".close arg1";
let cmd = command(&input); let err = command(&input).expect_err("Expected an error");
match cmd { assert_eq!(err.to_string(), format!("Invalid command {:?}", input));
Command::Err(message) => {
assert_eq!(message, format!("Invalid command {:?}", input));
},
_ => assert!(false)
}
} }
#[test] #[test]
fn test_close_parser_no_args() { fn test_close_parser_no_args() {
let input = ".close"; let input = ".close";
let cmd = command(&input); let cmd = command(&input).expect("Expected close command");
match cmd { match cmd {
Command::Close => assert!(true), Command::Close => assert!(true),
_ => assert!(false) _ => assert!(false)
@ -216,7 +226,7 @@ mod tests {
#[test] #[test]
fn test_close_parser_no_args_trailing_whitespace() { fn test_close_parser_no_args_trailing_whitespace() {
let input = ".close "; let input = ".close ";
let cmd = command(&input); let cmd = command(&input).expect("Expected close command");
match cmd { match cmd {
Command::Close => assert!(true), Command::Close => assert!(true),
_ => assert!(false) _ => assert!(false)
@ -226,7 +236,7 @@ mod tests {
#[test] #[test]
fn test_parser_preceeding_trailing_whitespace() { fn test_parser_preceeding_trailing_whitespace() {
let input = " .close "; let input = " .close ";
let cmd = command(&input); let cmd = command(&input).expect("Expected close command");
match cmd { match cmd {
Command::Close => assert!(true), Command::Close => assert!(true),
_ => assert!(false) _ => assert!(false)
@ -236,25 +246,14 @@ mod tests {
#[test] #[test]
fn test_command_parser_no_dot() { fn test_command_parser_no_dot() {
let input = "help command1 command2"; let input = "help command1 command2";
let cmd = command(&input); let err = command(&input).expect_err("Expected an error");
match cmd { assert_eq!(err.to_string(), format!("Invalid command {:?}", input));
Command::Err(message) => {
assert_eq!(message, format!("Invalid command {:?}", input));
},
_ => assert!(false)
}
} }
#[test] #[test]
fn test_command_parser_invalid_cmd() { fn test_command_parser_invalid_cmd() {
let input = ".foo command1"; let input = ".foo command1";
let cmd = command(&input); let err = command(&input).expect_err("Expected an error");
match cmd { assert_eq!(err.to_string(), format!("Invalid command {:?}", input));
Command::Err(message) => {
assert_eq!(message, format!("Invalid command {:?}", input));
},
_ => assert!(false)
}
} }
} }

View file

@ -0,0 +1,36 @@
// Copyright 2016 Mozilla
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use
// this file except in compliance with the License. You may obtain a copy of the
// License at http://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software distributed
// under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
// CONDITIONS OF ANY KIND, either express or implied. See the License for the
// specific language governing permissions and limitations under the License.
#![allow(dead_code)]
use rusqlite;
use mentat::errors as mentat;
error_chain! {
types {
Error, ErrorKind, ResultExt, Result;
}
foreign_links {
Rusqlite(rusqlite::Error);
}
links {
MentatError(mentat::Error, mentat::ErrorKind);
}
errors {
CommandParse(message: String) {
description("An error occured parsing the entered command")
display("{}", message)
}
}
}

View file

@ -8,14 +8,19 @@
// CONDITIONS OF ANY KIND, either express or implied. See the License for the // CONDITIONS OF ANY KIND, either express or implied. See the License for the
// specific language governing permissions and limitations under the License. // specific language governing permissions and limitations under the License.
use std::io::{stdin}; use std::io::stdin;
use linefeed::Reader; use linefeed::Reader;
use linefeed::terminal::DefaultTerminal; use linefeed::terminal::DefaultTerminal;
use self::InputResult::*; use self::InputResult::*;
use command_parser::{Command, command}; use command_parser::{
Command,
command
};
use errors as cli;
/// Possible results from reading input from `InputReader` /// Possible results from reading input from `InputReader`
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@ -28,8 +33,6 @@ pub enum InputResult {
More(Command), More(Command),
/// End of file reached /// End of file reached
Eof, Eof,
/// Error while parsing input;
InputError(String),
} }
/// Reads input from `stdin` /// Reads input from `stdin`
@ -63,30 +66,30 @@ impl InputReader {
/// Reads a single command, item, or statement from `stdin`. /// Reads a single command, item, or statement from `stdin`.
/// Returns `More` if further input is required for a complete result. /// Returns `More` if further input is required for a complete result.
/// In this case, the input received so far is buffered internally. /// In this case, the input received so far is buffered internally.
pub fn read_input(&mut self, prompt: &str) -> InputResult { pub fn read_input(&mut self, prompt: &str) -> Result<InputResult, cli::Error> {
let line = match self.read_line(prompt) { let line = match self.read_line(prompt) {
Some(s) => s, Some(s) => s,
None => return Eof, None => return Ok(Eof),
}; };
self.buffer.push_str(&line); self.buffer.push_str(&line);
if self.buffer.is_empty() { if self.buffer.is_empty() {
return Empty; return Ok(Empty);
} }
self.add_history(&line); self.add_history(&line);
let cmd = command(&self.buffer); let cmd = try!(command(&self.buffer));
match cmd { match cmd {
Command::Query(_) | Command::Query(_) |
Command::Transact(_) if !cmd.is_complete() => { Command::Transact(_) if !cmd.is_complete() => {
More(cmd) Ok(More(cmd))
}, },
_ => { _ => {
self.buffer.clear(); self.buffer.clear();
InputResult::MetaCommand(cmd) Ok(InputResult::MetaCommand(cmd))
} }
} }
} }

View file

@ -12,6 +12,7 @@
#[macro_use] extern crate log; #[macro_use] extern crate log;
#[macro_use] extern crate lazy_static; #[macro_use] extern crate lazy_static;
#[macro_use] extern crate error_chain;
extern crate combine; extern crate combine;
extern crate env_logger; extern crate env_logger;
@ -27,6 +28,7 @@ pub mod command_parser;
pub mod store; pub mod store;
pub mod input; pub mod input;
pub mod repl; pub mod repl;
pub mod errors;
pub fn run() -> i32 { pub fn run() -> i32 {
env_logger::init().unwrap(); env_logger::init().unwrap();
@ -58,8 +60,12 @@ pub fn run() -> i32 {
let db_name = matches.opt_str("d"); let db_name = matches.opt_str("d");
let mut repl = repl::Repl::new(db_name); let repl = repl::Repl::new(db_name);
repl.run(); if repl.is_ok() {
repl.unwrap().run();
} else {
println!("{}", repl.err().unwrap());
}
0 0
} }

View file

@ -10,10 +10,22 @@
use std::collections::HashMap; use std::collections::HashMap;
use command_parser::{Command, HELP_COMMAND, OPEN_COMMAND}; use command_parser::{
use input::{InputReader}; Command,
use input::InputResult::{MetaCommand, Empty, More, Eof, InputError}; HELP_COMMAND,
use store::Store; OPEN_COMMAND
};
use input::InputReader;
use input::InputResult::{
MetaCommand,
Empty,
More,
Eof
};
use store::{
Store,
db_output_name
};
/// Starting prompt /// Starting prompt
const DEFAULT_PROMPT: &'static str = "mentat=> "; const DEFAULT_PROMPT: &'static str = "mentat=> ";
@ -37,10 +49,11 @@ pub struct Repl {
impl Repl { impl Repl {
/// Constructs a new `Repl`. /// Constructs a new `Repl`.
pub fn new(db_name: Option<String>) -> Repl { pub fn new(db_name: Option<String>) -> Result<Repl, String> {
Repl{ let store = try!(Store::new(db_name.clone()).map_err(|e| e.to_string()));
store: Store::new(db_name), Ok(Repl{
} store: store,
})
} }
/// Runs the REPL interactively. /// Runs the REPL interactively.
@ -50,26 +63,22 @@ impl Repl {
loop { loop {
let res = input.read_input(if more.is_some() { MORE_PROMPT } else { DEFAULT_PROMPT }); let res = input.read_input(if more.is_some() { MORE_PROMPT } else { DEFAULT_PROMPT });
match res { match res {
MetaCommand(cmd) => { Ok(MetaCommand(cmd)) => {
debug!("read command: {:?}", cmd); debug!("read command: {:?}", cmd);
more = None; more = None;
self.handle_command(cmd); self.handle_command(cmd);
}, },
Empty => (), Ok(Empty) => (),
More(cmd) => { more = Some(cmd); }, Ok(More(cmd)) => { more = Some(cmd); },
Eof => { Ok(Eof) => {
if input.is_tty() { if input.is_tty() {
println!(""); println!("");
} }
break; break;
}, },
InputError(err) => { Err(e) => println!("{}", e.to_string()),
println!("{}", err); }
more = None;
},
};
} }
} }
@ -78,10 +87,18 @@ impl Repl {
match cmd { match cmd {
Command::Help(args) => self.help_command(args), Command::Help(args) => self.help_command(args),
Command::Open(db) => { Command::Open(db) => {
self.store.open(Some(db)); match self.store.open(Some(db.clone())) {
Ok(_) => println!("Database {:?} opened", db_output_name(&db)),
Err(e) => println!("{}", e.to_string())
};
},
Command::Close => {
let old_db_name = self.store.db_name.clone();
match self.store.close() {
Ok(_) => println!("Database {:?} closed", db_output_name(&old_db_name)),
Err(e) => println!("{}", e.to_string())
};
}, },
Command::Close => self.store.close(),
Command::Err(message) => println!("{}", message),
_ => unimplemented!(), _ => unimplemented!(),
} }
} }

View file

@ -15,37 +15,37 @@ use mentat::{
use mentat::conn::Conn; use mentat::conn::Conn;
use errors as cli;
pub struct Store { pub struct Store {
handle: rusqlite::Connection, handle: rusqlite::Connection,
conn: Conn, conn: Conn,
db_name: String, pub db_name: String,
} }
fn db_output_name(db_name: &String) -> String { pub fn db_output_name(db_name: &String) -> String {
if db_name.is_empty() { "in-memory db".to_string() } else { db_name.clone() } if db_name.is_empty() { "in-memory db".to_string() } else { db_name.clone() }
} }
impl Store { impl Store {
pub fn new(database: Option<String>) -> Store { pub fn new(database: Option<String>) -> Result<Store, cli::Error> {
let db_name = database.unwrap_or("".to_string()); let db_name = database.unwrap_or("".to_string());
let mut handle = new_connection(&db_name).expect("Couldn't open conn.");
let conn = Conn::connect(&mut handle).expect("Couldn't open DB."); let mut handle = try!(new_connection(&db_name));
println!("Database {:?} opened", db_output_name(&db_name)); let conn = try!(Conn::connect(&mut handle));
Store { handle, conn, db_name } Ok(Store { handle, conn, db_name })
} }
pub fn open(&mut self, database: Option<String>) { pub fn open(&mut self, database: Option<String>) -> Result<(), cli::Error> {
self.db_name = database.unwrap_or("".to_string()); self.db_name = database.unwrap_or("".to_string());
self.handle = new_connection(&self.db_name).expect("Couldn't open conn."); self.handle = try!(new_connection(&self.db_name));
self.conn = Conn::connect(&mut self.handle).expect("Couldn't open DB."); self.conn = try!(Conn::connect(&mut self.handle));
println!("Database {:?} opened", db_output_name(&self.db_name)); Ok(())
} }
pub fn close(&mut self) { pub fn close(&mut self) -> Result<(), cli::Error> {
self.handle = new_connection("").expect("Couldn't close conn.");
self.conn = Conn::connect(&mut self.handle).expect("Couldn't close DB.");
println!("Database {:?} closed", db_output_name(&self.db_name));
self.db_name = "".to_string(); self.db_name = "".to_string();
self.open(None)
} }
} }