From 9b30a2c0a73cac2c25e184b5136b4f939c74d4da Mon Sep 17 00:00:00 2001 From: Emily Toop Date: Tue, 9 May 2017 10:54:11 +0100 Subject: [PATCH 1/7] Create mentat command line. * Create tools directory containing new crate for mentat_cli. * Add simple cli with mentat prompt. --- Cargo.toml | 3 + tools/cli/Cargo.toml | 23 ++++++ tools/cli/src/bin/mentat_cli.rs | 15 ++++ tools/cli/src/mentat_cli/input.rs | 117 ++++++++++++++++++++++++++++++ tools/cli/src/mentat_cli/lib.rs | 75 +++++++++++++++++++ tools/cli/src/mentat_cli/repl.rs | 75 +++++++++++++++++++ 6 files changed, 308 insertions(+) create mode 100644 tools/cli/Cargo.toml create mode 100644 tools/cli/src/bin/mentat_cli.rs create mode 100644 tools/cli/src/mentat_cli/input.rs create mode 100644 tools/cli/src/mentat_cli/lib.rs create mode 100644 tools/cli/src/mentat_cli/repl.rs diff --git a/Cargo.toml b/Cargo.toml index 00c0ecf69..6cb47d112 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,3 +61,6 @@ path = "query-translator" [dependencies.mentat_tx_parser] path = "tx-parser" + +[dependencies.mentat_cli] +path = "tools/cli" diff --git a/tools/cli/Cargo.toml b/tools/cli/Cargo.toml new file mode 100644 index 000000000..0583e311b --- /dev/null +++ b/tools/cli/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "mentat_cli" +version = "0.0.1" +workspace = "../.." + +[lib] +name = "mentat_cli" +path = "src/mentat_cli/lib.rs" + +[[bin]] +name = "mentat_cli" +doc = false +test = false + +[build-dependencies] +rustc-serialize = "0.3.24" + +[dependencies] +getopts = "0.2" +env_logger = "0.3" +linefeed = "0.1" +log = "0.3" +tempfile = "1.1" diff --git a/tools/cli/src/bin/mentat_cli.rs b/tools/cli/src/bin/mentat_cli.rs new file mode 100644 index 000000000..28e035792 --- /dev/null +++ b/tools/cli/src/bin/mentat_cli.rs @@ -0,0 +1,15 @@ +// Copyright 2017 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. +extern crate mentat_cli; + +fn main() { + let status = mentat_cli::run(); + std::process::exit(status); +} diff --git a/tools/cli/src/mentat_cli/input.rs b/tools/cli/src/mentat_cli/input.rs new file mode 100644 index 000000000..267b3cf41 --- /dev/null +++ b/tools/cli/src/mentat_cli/input.rs @@ -0,0 +1,117 @@ +// Copyright 2017 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. + +use std::io::{self, stdin, BufRead, BufReader}; + +use linefeed::Reader; +use linefeed::terminal::DefaultTerminal; + +use self::InputResult::*; + +/// Possible results from reading input from `InputReader` +#[derive(Clone, Debug)] +pub enum InputResult { + /// rusti command as input; (name, rest of line) + Command(String, Option), + /// An empty line + Empty, + /// Needs more input; i.e. there is an unclosed delimiter + More, + /// End of file reached + Eof, +} + +/// Reads input from `stdin` +pub struct InputReader { + buffer: String, + reader: Option>, +} + +impl InputReader { + /// Constructs a new `InputReader` reading from `stdin`. + pub fn new() -> InputReader { + let r = match Reader::new("mentat") { + Ok(mut r) => { + r.set_word_break_chars(" \t\n!\"#$%&'()*+,-./:;<=>?@[\\]^`"); + Some(r) + } + Err(_) => None + }; + + InputReader{ + buffer: String::new(), + reader: r, + } + } + + /// Returns whether the `InputReader` is reading from a TTY. + pub fn is_tty(&self) -> bool { + self.reader.is_some() + } + + /// Reads a single command, item, or statement from `stdin`. + /// Returns `More` if further input is required for a complete result. + /// In this case, the input received so far is buffered internally. + pub fn read_input(&mut self, prompt: &str) -> InputResult { + let line = match self.read_line(prompt) { + Some(s) => s, + None => return Eof, + }; + + self.buffer.push_str(&line); + + if self.buffer.is_empty() { + return Empty; + } + + self.add_history(&line); + + let res = More; + + match res { + More => (), + _ => self.buffer.clear(), + }; + + res + } + + fn read_line(&mut self, prompt: &str) -> Option { + match self.reader { + Some(ref mut r) => { + r.set_prompt(prompt); + r.read_line().ok().and_then(|line| line) + } + None => self.read_stdin() + } + } + + fn read_stdin(&self) -> Option { + let mut s = String::new(); + + match stdin().read_line(&mut s) { + Ok(0) | Err(_) => None, + Ok(_) => Some(s) + } + } + + fn add_history(&mut self, line: &str) { + if let Some(ref mut r) = self.reader { + r.add_history(line.to_owned()); + } + } +} + +#[cfg(test)] +mod tests { + #[test] + fn it_works() { + } +} diff --git a/tools/cli/src/mentat_cli/lib.rs b/tools/cli/src/mentat_cli/lib.rs new file mode 100644 index 000000000..41d196fda --- /dev/null +++ b/tools/cli/src/mentat_cli/lib.rs @@ -0,0 +1,75 @@ +// Copyright 2017 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. +#![crate_name = "mentat_cli"] + +#[macro_use] extern crate log; + +extern crate env_logger; +extern crate getopts; +extern crate linefeed; + +use getopts::Options; + +pub mod input; +pub mod repl; + +pub fn run() -> i32 { + env_logger::init().unwrap(); + + let args = std::env::args().collect::>(); + let mut opts = Options::new(); + + opts.optflag("h", "help", "Print this help message and exit"); + opts.optflag("v", "version", "Print version and exit"); + + let matches = match opts.parse(&args[1..]) { + Ok(m) => m, + Err(e) => { + println!("{}: {}", args[0], e); + return 1; + } + }; + + if matches.opt_present("version") { + print_version(); + return 0; + } + if matches.opt_present("help") { + print_usage(&args[0], &opts); + return 0; + } + + let mut repl = repl::Repl::new(); + repl.run(); + + 0 +} + +/// Returns a version string. +pub fn version() -> &'static str { + env!("CARGO_PKG_VERSION") +} + +fn print_usage(arg0: &str, opts: &Options) { + print!("{}", opts.usage(&format!( + "Usage: {} [OPTIONS] [FILE]", arg0))); +} + +fn print_version() { + println!("mentat {}", version()); +} + + +#[cfg(test)] +mod tests { + #[test] + fn it_works() { + } +} diff --git a/tools/cli/src/mentat_cli/repl.rs b/tools/cli/src/mentat_cli/repl.rs new file mode 100644 index 000000000..d7b95f581 --- /dev/null +++ b/tools/cli/src/mentat_cli/repl.rs @@ -0,0 +1,75 @@ +// Copyright 2017 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. +use input::{InputReader}; +use input::InputResult::{Command, Empty, More, Eof}; + +/// Starting prompt +const DEFAULT_PROMPT: &'static str = "mentat=> "; +/// Prompt when further input is being read +const MORE_PROMPT: &'static str = "mentat.> "; +/// Prompt when a `.block` command is in effect +const BLOCK_PROMPT: &'static str = "mentat+> "; + +/// Executes input and maintains state of persistent items. +pub struct Repl { +} + +impl Repl { + /// Constructs a new `Repl`. + pub fn new() -> Repl { + Repl{} + } + + + /// Runs the REPL interactively. + pub fn run(&mut self) { + let mut more = false; + let mut input = InputReader::new(); + + loop { + let res = input.read_input(if more { MORE_PROMPT } else { DEFAULT_PROMPT }); + // let res = if self.read_block { + // self.read_block = false; + // input.read_block_input(BLOCK_PROMPT) + // } else { + // input.read_input(if more { MORE_PROMPT } else { DEFAULT_PROMPT }) + // }; + + match res { + Command(name, args) => { + debug!("read command: {} {:?}", name, args); + + more = false; + self.handle_command(name, args); + }, + Empty => (), + More => { more = true; }, + Eof => { + if input.is_tty() { + println!(""); + } + break; + } + }; + } + } + + /// Runs a single command input. + fn handle_command(&mut self, cmd: String, args: Option) { + println!("{:?} {:?}", cmd, args); + } +} + +#[cfg(test)] +mod tests { + #[test] + fn it_works() { + } +} From 5a6c3f6598ae2b4f4283f56380b5b12f3cded0f3 Mon Sep 17 00:00:00 2001 From: Emily Toop Date: Tue, 9 May 2017 11:10:13 +0100 Subject: [PATCH 2/7] Remove rustc-serialize dependency --- tools/cli/Cargo.toml | 3 --- 1 file changed, 3 deletions(-) diff --git a/tools/cli/Cargo.toml b/tools/cli/Cargo.toml index 0583e311b..67554a936 100644 --- a/tools/cli/Cargo.toml +++ b/tools/cli/Cargo.toml @@ -12,9 +12,6 @@ name = "mentat_cli" doc = false test = false -[build-dependencies] -rustc-serialize = "0.3.24" - [dependencies] getopts = "0.2" env_logger = "0.3" From 46fc1615fb9a4fa637375f470eed6aca7ab8c218 Mon Sep 17 00:00:00 2001 From: Emily Toop Date: Tue, 30 May 2017 11:05:58 +0100 Subject: [PATCH 3/7] Open DB inside CLI (#452) (#463) * Open named database OR default to in memory database if no name provided Rearrange workspace to allow import of mentat crate in cli crate Create store object inside repl when started for connecting to mentat Use provided DB name to open connection in store Accept DB name as command line arg. Open on CLI start Implement '.open' command to open desired DB from inside CLI * Implement Close command to close current DB. * Closes existing open db and opens new in memory db * Review comment: Use `combine` to parse arguments. Move over to using Result rather than enums with err * Accept and parse EDN Query and Transact commands (#453) (#465) * Parse query and transact commands * Implement is_complete for transactions and queries * Improve query parser. Am still not happy with it though. There must be some way that I can retain the eof() after the `then` that means I don't have to move the skip on spaces and eof Make in process command storing clearer. Add comments around in process commands. Add alternative commands for transact/t and query/q * Address review comments r=nalexander. * Bump rust version number. * Use `bail` when throwing errors. * Improve edn parser. * Remove references to unused `more` flag. * Improve naming of query and transact commands. * Send queries and transactions to mentat and output the results (#466) * Send queries and transactions to mentat and output the results move outputting query and transaction results out of store and into repl * Add query and transact commands to help * Execute queries and transacts passed in at startup * Address review comments =nalexander. * Bump rust version number. * Use `bail` when throwing errors. * Improve edn parser. * Remove references to unused `more` flag. * Improve naming of query and transact commands. * Execute command line args in order * Addressing rebase issues --- Cargo.toml | 5 +- build/version.rs | 2 +- db/src/lib.rs | 2 +- tools/cli/Cargo.toml | 27 +- tools/cli/src/mentat_cli/command_parser.rs | 428 +++++++++++++++++++++ tools/cli/src/mentat_cli/errors.rs | 36 ++ tools/cli/src/mentat_cli/input.rs | 68 +++- tools/cli/src/mentat_cli/lib.rs | 50 ++- tools/cli/src/mentat_cli/repl.rs | 193 ++++++++-- tools/cli/src/mentat_cli/store.rs | 61 +++ 10 files changed, 817 insertions(+), 55 deletions(-) create mode 100644 tools/cli/src/mentat_cli/command_parser.rs create mode 100644 tools/cli/src/mentat_cli/errors.rs create mode 100644 tools/cli/src/mentat_cli/store.rs diff --git a/Cargo.toml b/Cargo.toml index 6cb47d112..ff694e36f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ version = "0.4.0" build = "build/version.rs" [workspace] -members = [] +members = ["tools/cli"] [build-dependencies] rustc_version = "0.1.7" @@ -61,6 +61,3 @@ path = "query-translator" [dependencies.mentat_tx_parser] path = "tx-parser" - -[dependencies.mentat_cli] -path = "tools/cli" diff --git a/build/version.rs b/build/version.rs index 54f8b2a40..6ad731574 100644 --- a/build/version.rs +++ b/build/version.rs @@ -16,7 +16,7 @@ use rustc_version::version_matches; /// MIN_VERSION should be changed when there's a new minimum version of rustc required /// to build the project. -static MIN_VERSION: &'static str = ">= 1.15.1"; +static MIN_VERSION: &'static str = ">= 1.17.0"; fn main() { if !version_matches(MIN_VERSION) { diff --git a/db/src/lib.rs b/db/src/lib.rs index 07f5bc5bb..a67c6562b 100644 --- a/db/src/lib.rs +++ b/db/src/lib.rs @@ -43,7 +43,7 @@ mod entids; pub mod errors; mod metadata; mod schema; -mod types; +pub mod types; mod internal_types; mod upsert_resolution; mod tx; diff --git a/tools/cli/Cargo.toml b/tools/cli/Cargo.toml index 67554a936..cd74523b5 100644 --- a/tools/cli/Cargo.toml +++ b/tools/cli/Cargo.toml @@ -1,7 +1,6 @@ [package] name = "mentat_cli" version = "0.0.1" -workspace = "../.." [lib] name = "mentat_cli" @@ -18,3 +17,29 @@ env_logger = "0.3" linefeed = "0.1" log = "0.3" tempfile = "1.1" +combine = "2.2.2" +lazy_static = "0.2.2" +error-chain = "0.8.1" + +[dependencies.rusqlite] +version = "0.11" +# System sqlite might be very old. +features = ["bundled", "limits"] + +[dependencies.mentat] +path = "../.." + +[dependencies.mentat_parser_utils] +path = "../../parser-utils" + +[dependencies.edn] +path = "../../edn" + +[dependencies.mentat_query] +path = "../../query" + +[dependencies.mentat_core] +path = "../../core" + +[dependencies.mentat_db] +path = "../../db" diff --git a/tools/cli/src/mentat_cli/command_parser.rs b/tools/cli/src/mentat_cli/command_parser.rs new file mode 100644 index 000000000..aa81e4d71 --- /dev/null +++ b/tools/cli/src/mentat_cli/command_parser.rs @@ -0,0 +1,428 @@ +// Copyright 2017 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. + +use combine::{ + any, + eof, + look_ahead, + many1, + parser, + satisfy, + sep_end_by, + token, + Parser +}; +use combine::char::{ + space, + spaces, + string +}; +use combine::combinator::{ + choice, + try +}; + +use combine::primitives::Consumed; + +use errors as cli; + +use edn; + +pub static HELP_COMMAND: &'static str = &"help"; +pub static OPEN_COMMAND: &'static str = &"open"; +pub static CLOSE_COMMAND: &'static str = &"close"; +pub static LONG_QUERY_COMMAND: &'static str = &"query"; +pub static SHORT_QUERY_COMMAND: &'static str = &"q"; +pub static LONG_TRANSACT_COMMAND: &'static str = &"transact"; +pub static SHORT_TRANSACT_COMMAND: &'static str = &"t"; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum Command { + Transact(String), + Query(String), + Help(Vec), + Open(String), + Close, +} + +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 { + match self { + &Command::Query(ref args) | + &Command::Transact(ref args) => { + edn::parse::value(&args).is_ok() + }, + &Command::Help(_) | + &Command::Open(_) | + &Command::Close => true + } + } + + pub fn output(&self) -> String { + match self { + &Command::Query(ref args) => { + format!(".{} {}", LONG_QUERY_COMMAND, args) + }, + &Command::Transact(ref args) => { + format!(".{} {}", LONG_TRANSACT_COMMAND, args) + }, + &Command::Help(ref args) => { + format!(".{} {:?}", HELP_COMMAND, args) + }, + &Command::Open(ref args) => { + format!(".{} {}", OPEN_COMMAND, args) + } + &Command::Close => { + format!(".{}", CLOSE_COMMAND) + }, + } + } +} + +pub fn command(s: &str) -> Result { + let arguments = || sep_end_by::, _, _>(many1(satisfy(|c: char| !c.is_whitespace())), many1::, _>(space())).expected("arguments"); + + let help_parser = string(HELP_COMMAND) + .with(spaces()) + .with(arguments()) + .map(|args| { + Ok(Command::Help(args.clone())) + }); + + let open_parser = string(OPEN_COMMAND) + .with(spaces()) + .with(arguments()) + .map(|args| { + if args.len() < 1 { + bail!(cli::ErrorKind::CommandParse("Missing required argument".to_string())); + } + if args.len() > 1 { + bail!(cli::ErrorKind::CommandParse(format!("Unrecognized argument {:?}", args[1]))); + } + Ok(Command::Open(args[0].clone())) + }); + + let close_parser = string(CLOSE_COMMAND) + .with(arguments()) + .skip(spaces()) + .skip(eof()) + .map(|args| { + if args.len() > 0 { + bail!(cli::ErrorKind::CommandParse(format!("Unrecognized argument {:?}", args[0])) ); + } + Ok(Command::Close) + }); + + let edn_arg_parser = || spaces() + .with(look_ahead(string("[").or(string("{"))) + .with(many1::, _>(try(any()))) + .and_then(|args| -> Result { + Ok(args.iter().collect()) + }) + ); + + let query_parser = try(string(LONG_QUERY_COMMAND)).or(try(string(SHORT_QUERY_COMMAND))) + .with(edn_arg_parser()) + .map(|x| { + Ok(Command::Query(x)) + }); + + let transact_parser = try(string(LONG_TRANSACT_COMMAND)).or(try(string(SHORT_TRANSACT_COMMAND))) + .with(edn_arg_parser()) + .map( |x| { + Ok(Command::Transact(x)) + }); + + spaces() + .skip(token('.')) + .with(choice::<[&mut Parser>; 5], _> + ([&mut try(help_parser), + &mut try(open_parser), + &mut try(close_parser), + &mut try(query_parser), + &mut try(transact_parser)])) + .parse(s) + .unwrap_or((Err(cli::ErrorKind::CommandParse(format!("Invalid command {:?}", s)).into()), "")).0 +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_help_parser_multiple_args() { + let input = ".help command1 command2"; + let cmd = command(&input).expect("Expected help command"); + match cmd { + Command::Help(args) => { + assert_eq!(args, vec!["command1", "command2"]); + }, + _ => assert!(false) + } + } + + #[test] + fn test_help_parser_dot_arg() { + let input = ".help .command1"; + let cmd = command(&input).expect("Expected help command"); + match cmd { + Command::Help(args) => { + assert_eq!(args, vec![".command1"]); + }, + _ => assert!(false) + } + } + + #[test] + fn test_help_parser_no_args() { + let input = ".help"; + let cmd = command(&input).expect("Expected help command"); + match cmd { + Command::Help(args) => { + let empty: Vec = vec![]; + assert_eq!(args, empty); + }, + _ => assert!(false) + } + } + + #[test] + fn test_help_parser_no_args_trailing_whitespace() { + let input = ".help "; + let cmd = command(&input).expect("Expected help command"); + match cmd { + Command::Help(args) => { + let empty: Vec = vec![]; + assert_eq!(args, empty); + }, + _ => assert!(false) + } + } + + #[test] + fn test_open_parser_multiple_args() { + let input = ".open database1 database2"; + let err = command(&input).expect_err("Expected an error"); + assert_eq!(err.to_string(), "Unrecognized argument \"database2\""); + } + + #[test] + fn test_open_parser_single_arg() { + let input = ".open database1"; + let cmd = command(&input).expect("Expected open command"); + match cmd { + Command::Open(arg) => { + assert_eq!(arg, "database1".to_string()); + }, + _ => assert!(false) + } + } + + #[test] + fn test_open_parser_path_arg() { + let input = ".open /path/to/my.db"; + let cmd = command(&input).expect("Expected open command"); + match cmd { + Command::Open(arg) => { + assert_eq!(arg, "/path/to/my.db".to_string()); + }, + _ => assert!(false) + } + } + + #[test] + fn test_open_parser_file_arg() { + let input = ".open my.db"; + let cmd = command(&input).expect("Expected open command"); + match cmd { + Command::Open(arg) => { + assert_eq!(arg, "my.db".to_string()); + }, + _ => assert!(false) + } + } + + #[test] + fn test_open_parser_no_args() { + let input = ".open"; + let err = command(&input).expect_err("Expected an error"); + assert_eq!(err.to_string(), "Missing required argument"); + } + + #[test] + fn test_open_parser_no_args_trailing_whitespace() { + let input = ".open "; + let err = command(&input).expect_err("Expected an error"); + assert_eq!(err.to_string(), "Missing required argument"); + } + + #[test] + fn test_close_parser_with_args() { + let input = ".close arg1"; + let err = command(&input).expect_err("Expected an error"); + assert_eq!(err.to_string(), format!("Invalid command {:?}", input)); + } + + #[test] + fn test_close_parser_no_args() { + let input = ".close"; + let cmd = command(&input).expect("Expected close command"); + match cmd { + Command::Close => assert!(true), + _ => assert!(false) + } + } + + #[test] + fn test_close_parser_no_args_trailing_whitespace() { + let input = ".close "; + let cmd = command(&input).expect("Expected close command"); + match cmd { + Command::Close => assert!(true), + _ => assert!(false) + } + } + + #[test] + fn test_query_parser_complete_edn() { + let input = ".q [:find ?x :where [?x foo/bar ?y]]"; + let cmd = command(&input).expect("Expected query command"); + match cmd { + Command::Query(edn) => assert_eq!(edn, "[:find ?x :where [?x foo/bar ?y]]"), + _ => assert!(false) + } + } + + #[test] + fn test_query_parser_alt_query_command() { + let input = ".query [:find ?x :where [?x foo/bar ?y]]"; + let cmd = command(&input).expect("Expected query command"); + match cmd { + Command::Query(edn) => assert_eq!(edn, "[:find ?x :where [?x foo/bar ?y]]"), + _ => assert!(false) + } + } + + #[test] + fn test_query_parser_incomplete_edn() { + let input = ".q [:find ?x\r\n"; + let cmd = command(&input).expect("Expected query command"); + match cmd { + Command::Query(edn) => assert_eq!(edn, "[:find ?x\r\n"), + _ => assert!(false) + } + } + + #[test] + fn test_query_parser_empty_edn() { + let input = ".q {}"; + let cmd = command(&input).expect("Expected query command"); + match cmd { + Command::Query(edn) => assert_eq!(edn, "{}"), + _ => assert!(false) + } + } + + #[test] + fn test_query_parser_no_edn() { + let input = ".q "; + let err = command(&input).expect_err("Expected an error"); + assert_eq!(err.to_string(), format!("Invalid command {:?}", input)); + } + + #[test] + fn test_query_parser_invalid_start_char() { + let input = ".q :find ?x"; + let err = command(&input).expect_err("Expected an error"); + assert_eq!(err.to_string(), format!("Invalid command {:?}", input)); + } + + #[test] + fn test_transact_parser_complete_edn() { + let input = ".t [[:db/add \"s\" :db/ident :foo/uuid] [:db/add \"r\" :db/ident :bar/uuid]]"; + let cmd = command(&input).expect("Expected transact command"); + match cmd { + Command::Transact(edn) => assert_eq!(edn, "[[:db/add \"s\" :db/ident :foo/uuid] [:db/add \"r\" :db/ident :bar/uuid]]"), + _ => assert!(false) + } + } + + #[test] + fn test_transact_parser_alt_command() { + let input = ".transact [[:db/add \"s\" :db/ident :foo/uuid] [:db/add \"r\" :db/ident :bar/uuid]]"; + let cmd = command(&input).expect("Expected transact command"); + match cmd { + Command::Transact(edn) => assert_eq!(edn, "[[:db/add \"s\" :db/ident :foo/uuid] [:db/add \"r\" :db/ident :bar/uuid]]"), + _ => assert!(false) + } + } + + #[test] + fn test_transact_parser_incomplete_edn() { + let input = ".t {\r\n"; + let cmd = command(&input).expect("Expected transact command"); + match cmd { + Command::Transact(edn) => assert_eq!(edn, "{\r\n"), + _ => assert!(false) + } + } + + #[test] + fn test_transact_parser_empty_edn() { + let input = ".t {}"; + let cmd = command(&input).expect("Expected transact command"); + match cmd { + Command::Transact(edn) => assert_eq!(edn, "{}"), + _ => assert!(false) + } + } + + #[test] + fn test_transact_parser_no_edn() { + let input = ".t "; + let err = command(&input).expect_err("Expected an error"); + assert_eq!(err.to_string(), format!("Invalid command {:?}", input)); + } + + #[test] + fn test_transact_parser_invalid_start_char() { + let input = ".t :db/add \"s\" :db/ident :foo/uuid"; + let err = command(&input).expect_err("Expected an error"); + assert_eq!(err.to_string(), format!("Invalid command {:?}", input)); + } + + #[test] + fn test_parser_preceeding_trailing_whitespace() { + let input = " .close "; + let cmd = command(&input).expect("Expected close command"); + match cmd { + Command::Close => assert!(true), + _ => assert!(false) + } + } + + #[test] + fn test_command_parser_no_dot() { + let input = "help command1 command2"; + let err = command(&input).expect_err("Expected an error"); + assert_eq!(err.to_string(), format!("Invalid command {:?}", input)); + } + + #[test] + fn test_command_parser_invalid_cmd() { + let input = ".foo command1"; + let err = command(&input).expect_err("Expected an error"); + assert_eq!(err.to_string(), format!("Invalid command {:?}", input)); + } +} diff --git a/tools/cli/src/mentat_cli/errors.rs b/tools/cli/src/mentat_cli/errors.rs new file mode 100644 index 000000000..8e095ab34 --- /dev/null +++ b/tools/cli/src/mentat_cli/errors.rs @@ -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) + } + } +} diff --git a/tools/cli/src/mentat_cli/input.rs b/tools/cli/src/mentat_cli/input.rs index 267b3cf41..2a614e5ad 100644 --- a/tools/cli/src/mentat_cli/input.rs +++ b/tools/cli/src/mentat_cli/input.rs @@ -8,21 +8,34 @@ // CONDITIONS OF ANY KIND, either express or implied. See the License for the // specific language governing permissions and limitations under the License. -use std::io::{self, stdin, BufRead, BufReader}; +use std::io::stdin; use linefeed::Reader; use linefeed::terminal::DefaultTerminal; use self::InputResult::*; +use command_parser::{ + Command, + command +}; + +use errors as cli; + +/// Starting prompt +const DEFAULT_PROMPT: &'static str = "mentat=> "; +/// Prompt when further input is being read +// TODO: Should this actually reflect the current open brace? +const MORE_PROMPT: &'static str = "mentat.> "; + /// Possible results from reading input from `InputReader` #[derive(Clone, Debug)] pub enum InputResult { - /// rusti command as input; (name, rest of line) - Command(String, Option), + /// mentat command as input; (name, rest of line) + MetaCommand(Command), /// An empty line Empty, - /// Needs more input; i.e. there is an unclosed delimiter + /// Needs more input More, /// End of file reached Eof, @@ -32,6 +45,7 @@ pub enum InputResult { pub struct InputReader { buffer: String, reader: Option>, + in_process_cmd: Option, } impl InputReader { @@ -48,6 +62,7 @@ impl InputReader { InputReader{ buffer: String::new(), reader: r, + in_process_cmd: None, } } @@ -59,28 +74,53 @@ impl InputReader { /// Reads a single command, item, or statement from `stdin`. /// Returns `More` if further input is required for a complete result. /// 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) -> Result { + let prompt = if self.in_process_cmd.is_some() { MORE_PROMPT } else { DEFAULT_PROMPT }; let line = match self.read_line(prompt) { Some(s) => s, - None => return Eof, + None => return Ok(Eof), }; self.buffer.push_str(&line); if self.buffer.is_empty() { - return Empty; + return Ok(Empty); } self.add_history(&line); - let res = More; - - match res { - More => (), - _ => self.buffer.clear(), + // if we have a command in process (i.e. in incomplete query or transaction), + // then we already know which type of command it is and so we don't need to parse the + // command again, only the content, which we do later. + // Therefore, we add the newly read in line to the existing command args. + // If there is no in process command, we parse the read in line as a new command. + let cmd = match &self.in_process_cmd { + &Some(Command::Query(ref args)) => { + Command::Query(args.clone() + " " + &line) + }, + &Some(Command::Transact(ref args)) => { + Command::Transact(args.clone() + " " + &line) + }, + _ => { + try!(command(&self.buffer)) + } }; - res + match cmd { + Command::Query(_) | + Command::Transact(_) if !cmd.is_complete() => { + // a query or transact is complete if it contains a valid edn. + // if the command is not complete, ask for more from the repl and remember + // which type of command we've found here. + self.in_process_cmd = Some(cmd); + Ok(More) + }, + _ => { + self.buffer.clear(); + self.in_process_cmd = None; + Ok(InputResult::MetaCommand(cmd)) + } + } } fn read_line(&mut self, prompt: &str) -> Option { @@ -88,7 +128,7 @@ impl InputReader { Some(ref mut r) => { r.set_prompt(prompt); r.read_line().ok().and_then(|line| line) - } + }, None => self.read_stdin() } } diff --git a/tools/cli/src/mentat_cli/lib.rs b/tools/cli/src/mentat_cli/lib.rs index 41d196fda..bcc7c71c7 100644 --- a/tools/cli/src/mentat_cli/lib.rs +++ b/tools/cli/src/mentat_cli/lib.rs @@ -7,18 +7,32 @@ // 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. + #![crate_name = "mentat_cli"] #[macro_use] extern crate log; +#[macro_use] extern crate lazy_static; +#[macro_use] extern crate error_chain; +extern crate combine; extern crate env_logger; extern crate getopts; extern crate linefeed; +extern crate rusqlite; + +extern crate mentat; +extern crate edn; +extern crate mentat_query; +extern crate mentat_core; +extern crate mentat_db; use getopts::Options; +pub mod command_parser; +pub mod store; pub mod input; pub mod repl; +pub mod errors; pub fn run() -> i32 { env_logger::init().unwrap(); @@ -26,7 +40,10 @@ pub fn run() -> i32 { let args = std::env::args().collect::>(); let mut opts = Options::new(); + opts.optopt("d", "", "The path to a database to open", "DATABASE"); opts.optflag("h", "help", "Print this help message and exit"); + opts.optmulti("q", "query", "Execute a query on startup. Queries are executed after any transacts.", "QUERY"); + opts.optmulti("t", "transact", "Execute a transact on startup. Transacts are executed before queries.", "TRANSACT"); opts.optflag("v", "version", "Print version and exit"); let matches = match opts.parse(&args[1..]) { @@ -41,13 +58,42 @@ pub fn run() -> i32 { print_version(); return 0; } + if matches.opt_present("help") { print_usage(&args[0], &opts); return 0; } - let mut repl = repl::Repl::new(); - repl.run(); + let mut last_arg: Option<&str> = None; + let cmds:Vec = args.iter().filter_map(|arg| { + match last_arg { + Some("-d") => { + last_arg = None; + Some(command_parser::Command::Open(arg.clone())) + }, + Some("-q") => { + last_arg = None; + Some(command_parser::Command::Query(arg.clone())) + }, + Some("-t") => { + last_arg = None; + Some(command_parser::Command::Transact(arg.clone())) + }, + Some(_) | + None => { + last_arg = Some(&arg); + None + }, + } + }).collect(); + + let repl = repl::Repl::new(); + if repl.is_ok() { + repl.unwrap().run(Some(cmds)); + + } else { + println!("{}", repl.err().unwrap()); + } 0 } diff --git a/tools/cli/src/mentat_cli/repl.rs b/tools/cli/src/mentat_cli/repl.rs index d7b95f581..9f84060c3 100644 --- a/tools/cli/src/mentat_cli/repl.rs +++ b/tools/cli/src/mentat_cli/repl.rs @@ -7,63 +7,192 @@ // 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. -use input::{InputReader}; -use input::InputResult::{Command, Empty, More, Eof}; -/// Starting prompt -const DEFAULT_PROMPT: &'static str = "mentat=> "; -/// Prompt when further input is being read -const MORE_PROMPT: &'static str = "mentat.> "; -/// Prompt when a `.block` command is in effect -const BLOCK_PROMPT: &'static str = "mentat+> "; +use std::collections::HashMap; + +use mentat::query::QueryResults; +use mentat_core::TypedValue; + +use command_parser::{ + Command, + HELP_COMMAND, + OPEN_COMMAND, + LONG_QUERY_COMMAND, + SHORT_QUERY_COMMAND, + LONG_TRANSACT_COMMAND, + SHORT_TRANSACT_COMMAND, +}; +use input::InputReader; +use input::InputResult::{ + MetaCommand, + Empty, + More, + Eof +}; +use store::{ + Store, + db_output_name +}; + +lazy_static! { + static ref COMMAND_HELP: HashMap<&'static str, &'static str> = { + let mut map = HashMap::new(); + map.insert(HELP_COMMAND, "Show help for commands."); + map.insert(OPEN_COMMAND, "Open a database at path."); + map.insert(LONG_QUERY_COMMAND, "Execute a query against the current open database."); + map.insert(SHORT_QUERY_COMMAND, "Shortcut for `.query`. Execute a query against the current open database."); + map.insert(LONG_TRANSACT_COMMAND, "Execute a transact against the current open database."); + map.insert(SHORT_TRANSACT_COMMAND, "Shortcut for `.transact`. Execute a transact against the current open database."); + map + }; +} /// Executes input and maintains state of persistent items. pub struct Repl { + store: Store } impl Repl { /// Constructs a new `Repl`. - pub fn new() -> Repl { - Repl{} + pub fn new() -> Result { + let store = Store::new(None).map_err(|e| e.to_string())?; + Ok(Repl{ + store: store, + }) } - /// Runs the REPL interactively. - pub fn run(&mut self) { - let mut more = false; + pub fn run(&mut self, startup_commands: Option>) { let mut input = InputReader::new(); + if let Some(cmds) = startup_commands { + for command in cmds.iter() { + println!("{}", command.output()); + self.handle_command(command.clone()); + } + } + loop { - let res = input.read_input(if more { MORE_PROMPT } else { DEFAULT_PROMPT }); - // let res = if self.read_block { - // self.read_block = false; - // input.read_block_input(BLOCK_PROMPT) - // } else { - // input.read_input(if more { MORE_PROMPT } else { DEFAULT_PROMPT }) - // }; + let res = input.read_input(); match res { - Command(name, args) => { - debug!("read command: {} {:?}", name, args); - - more = false; - self.handle_command(name, args); + Ok(MetaCommand(cmd)) => { + debug!("read command: {:?}", cmd); + self.handle_command(cmd); }, - Empty => (), - More => { more = true; }, - Eof => { + Ok(Empty) | + Ok(More) => (), + Ok(Eof) => { if input.is_tty() { println!(""); } break; - } - }; + }, + Err(e) => println!("{}", e.to_string()), + } } } /// Runs a single command input. - fn handle_command(&mut self, cmd: String, args: Option) { - println!("{:?} {:?}", cmd, args); + fn handle_command(&mut self, cmd: Command) { + match cmd { + Command::Help(args) => self.help_command(args), + Command::Open(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::Query(query) => self.execute_query(query), + Command::Transact(transaction) => self.execute_transact(transaction), + } + } + + fn help_command(&self, args: Vec) { + if args.is_empty() { + for (cmd, msg) in COMMAND_HELP.iter() { + println!(".{} - {}", cmd, msg); + } + } else { + for mut arg in args { + if arg.chars().nth(0).unwrap() == '.' { + arg.remove(0); + } + let msg = COMMAND_HELP.get(arg.as_str()); + if msg.is_some() { + println!(".{} - {}", arg, msg.unwrap()); + } else { + println!("Unrecognised command {}", arg); + } + } + } + } + + pub fn execute_query(&self, query: String) { + let results = match self.store.query(query){ + Result::Ok(vals) => { + vals + }, + Result::Err(err) => return println!("{:?}.", err), + }; + + if results.is_empty() { + println!("No results found.") + } + + let mut output:String = String::new(); + match results { + QueryResults::Scalar(Some(val)) => { + output.push_str(&self.typed_value_as_string(val) ); + }, + QueryResults::Tuple(Some(vals)) => { + for val in vals { + output.push_str(&format!("{}\t", self.typed_value_as_string(val))); + } + }, + QueryResults::Coll(vv) => { + for val in vv { + output.push_str(&format!("{}\n", self.typed_value_as_string(val))); + } + }, + QueryResults::Rel(vvv) => { + for vv in vvv { + for v in vv { + output.push_str(&format!("{}\t", self.typed_value_as_string(v))); + } + output.push_str("\n"); + } + }, + _ => output.push_str(&format!("No results found.")) + } + println!("\n{}", output); + } + + pub fn execute_transact(&mut self, transaction: String) { + match self.store.transact(transaction) { + Result::Ok(report) => println!("{:?}", report), + Result::Err(err) => println!("{:?}.", err), + } + } + + fn typed_value_as_string(&self, value: TypedValue) -> String { + match value { + TypedValue::Boolean(b) => if b { "true".to_string() } else { "false".to_string() }, + TypedValue::Double(d) => format!("{}", d), + TypedValue::Instant(i) => format!("{}", i), + TypedValue::Keyword(k) => format!("{}", k), + TypedValue::Long(l) => format!("{}", l), + TypedValue::Ref(r) => format!("{}", r), + TypedValue::String(s) => format!("{:?}", s.to_string()), + TypedValue::Uuid(u) => format!("{}", u), + } } } diff --git a/tools/cli/src/mentat_cli/store.rs b/tools/cli/src/mentat_cli/store.rs new file mode 100644 index 000000000..bc49443c6 --- /dev/null +++ b/tools/cli/src/mentat_cli/store.rs @@ -0,0 +1,61 @@ +// Copyright 2017 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. + +use rusqlite; + +use errors as cli; + +use mentat::{ + new_connection, +}; +use mentat::query::QueryResults; + +use mentat::conn::Conn; +use mentat_db::types::TxReport; + +pub struct Store { + handle: rusqlite::Connection, + conn: Conn, + pub db_name: String, +} + +pub fn db_output_name(db_name: &String) -> String { + if db_name.is_empty() { "in-memory db".to_string() } else { db_name.clone() } +} + +impl Store { + pub fn new(database: Option) -> Result { + let db_name = database.unwrap_or("".to_string()); + + let mut handle = try!(new_connection(&db_name)); + let conn = try!(Conn::connect(&mut handle)); + Ok(Store { handle, conn, db_name }) + } + + pub fn open(&mut self, database: Option) -> Result<(), cli::Error> { + self.db_name = database.unwrap_or("".to_string()); + self.handle = try!(new_connection(&self.db_name)); + self.conn = try!(Conn::connect(&mut self.handle)); + Ok(()) + } + + pub fn close(&mut self) -> Result<(), cli::Error> { + self.db_name = "".to_string(); + self.open(None) + } + + pub fn query(&self, query: String) -> Result { + Ok(try!(self.conn.q_once(&self.handle, &query, None))) + } + + pub fn transact(&mut self, transaction: String) -> Result { + Ok(try!(self.conn.transact(&mut self.handle, &transaction))) + } +} From ecc926086a4d9fa56f1ea598477eaff10ec8b609 Mon Sep 17 00:00:00 2001 From: Emily Toop Date: Thu, 22 Jun 2017 12:52:43 +0100 Subject: [PATCH 4/7] Exit CLI (#457) (#484) r-rnewman * Implement exit command for cli tool * Address review comments r=rnewman * Include exit commands in help --- tools/cli/src/mentat_cli/command_parser.rs | 77 ++++++++++++++++++---- tools/cli/src/mentat_cli/repl.rs | 30 ++++++--- 2 files changed, 87 insertions(+), 20 deletions(-) diff --git a/tools/cli/src/mentat_cli/command_parser.rs b/tools/cli/src/mentat_cli/command_parser.rs index aa81e4d71..36d4912f6 100644 --- a/tools/cli/src/mentat_cli/command_parser.rs +++ b/tools/cli/src/mentat_cli/command_parser.rs @@ -13,7 +13,6 @@ use combine::{ eof, look_ahead, many1, - parser, satisfy, sep_end_by, token, @@ -29,8 +28,6 @@ use combine::combinator::{ try }; -use combine::primitives::Consumed; - use errors as cli; use edn; @@ -42,14 +39,17 @@ pub static LONG_QUERY_COMMAND: &'static str = &"query"; pub static SHORT_QUERY_COMMAND: &'static str = &"q"; pub static LONG_TRANSACT_COMMAND: &'static str = &"transact"; pub static SHORT_TRANSACT_COMMAND: &'static str = &"t"; +pub static LONG_EXIT_COMMAND: &'static str = &"exit"; +pub static SHORT_EXIT_COMMAND: &'static str = &"e"; #[derive(Clone, Debug, Eq, PartialEq)] pub enum Command { - Transact(String), - Query(String), + Close, + Exit, Help(Vec), Open(String), - Close, + Query(String), + Transact(String), } impl Command { @@ -65,7 +65,8 @@ impl Command { }, &Command::Help(_) | &Command::Open(_) | - &Command::Close => true + &Command::Close | + &Command::Exit => true } } @@ -86,6 +87,9 @@ impl Command { &Command::Close => { format!(".{}", CLOSE_COMMAND) }, + &Command::Exit => { + format!(".{}", LONG_EXIT_COMMAND) + }, } } } @@ -112,18 +116,29 @@ pub fn command(s: &str) -> Result { } Ok(Command::Open(args[0].clone())) }); + + let no_arg_parser = || arguments() + .skip(spaces()) + .skip(eof()); let close_parser = string(CLOSE_COMMAND) - .with(arguments()) - .skip(spaces()) - .skip(eof()) + .with(no_arg_parser()) .map(|args| { - if args.len() > 0 { + if !args.is_empty() { bail!(cli::ErrorKind::CommandParse(format!("Unrecognized argument {:?}", args[0])) ); } Ok(Command::Close) }); + let exit_parser = try(string(LONG_EXIT_COMMAND)).or(try(string(SHORT_EXIT_COMMAND))) + .with(no_arg_parser()) + .map(|args| { + if !args.is_empty() { + bail!(cli::ErrorKind::CommandParse(format!("Unrecognized argument {:?}", args[0])) ); + } + Ok(Command::Exit) + }); + let edn_arg_parser = || spaces() .with(look_ahead(string("[").or(string("{"))) .with(many1::, _>(try(any()))) @@ -146,10 +161,11 @@ pub fn command(s: &str) -> Result { spaces() .skip(token('.')) - .with(choice::<[&mut Parser>; 5], _> + .with(choice::<[&mut Parser>; 6], _> ([&mut try(help_parser), &mut try(open_parser), &mut try(close_parser), + &mut try(exit_parser), &mut try(query_parser), &mut try(transact_parser)])) .parse(s) @@ -294,6 +310,43 @@ mod tests { } } + #[test] + fn test_exit_parser_with_args() { + let input = ".exit arg1"; + let err = command(&input).expect_err("Expected an error"); + assert_eq!(err.to_string(), format!("Invalid command {:?}", input)); + } + + #[test] + fn test_exit_parser_no_args() { + let input = ".exit"; + let cmd = command(&input).expect("Expected exit command"); + match cmd { + Command::Exit => assert!(true), + _ => assert!(false) + } + } + + #[test] + fn test_exit_parser_no_args_trailing_whitespace() { + let input = ".exit "; + let cmd = command(&input).expect("Expected exit command"); + match cmd { + Command::Exit => assert!(true), + _ => assert!(false) + } + } + + #[test] + fn test_exit_parser_short_command() { + let input = ".e"; + let cmd = command(&input).expect("Expected exit command"); + match cmd { + Command::Exit => assert!(true), + _ => assert!(false) + } + } + #[test] fn test_query_parser_complete_edn() { let input = ".q [:find ?x :where [?x foo/bar ?y]]"; diff --git a/tools/cli/src/mentat_cli/repl.rs b/tools/cli/src/mentat_cli/repl.rs index 9f84060c3..5c3cb790e 100644 --- a/tools/cli/src/mentat_cli/repl.rs +++ b/tools/cli/src/mentat_cli/repl.rs @@ -8,7 +8,10 @@ // CONDITIONS OF ANY KIND, either express or implied. See the License for the // specific language governing permissions and limitations under the License. -use std::collections::HashMap; +use std::collections::HashMap; +use std::process; + +use error_chain::ChainedError; use mentat::query::QueryResults; use mentat_core::TypedValue; @@ -21,6 +24,8 @@ use command_parser::{ SHORT_QUERY_COMMAND, LONG_TRANSACT_COMMAND, SHORT_TRANSACT_COMMAND, + LONG_EXIT_COMMAND, + SHORT_EXIT_COMMAND, }; use input::InputReader; use input::InputResult::{ @@ -37,6 +42,8 @@ use store::{ lazy_static! { static ref COMMAND_HELP: HashMap<&'static str, &'static str> = { let mut map = HashMap::new(); + map.insert(LONG_EXIT_COMMAND, "Close the current database and exit the REPL."); + map.insert(SHORT_EXIT_COMMAND, "Shortcut for `.exit`. Close the current database and exit the REPL."); map.insert(HELP_COMMAND, "Show help for commands."); map.insert(OPEN_COMMAND, "Open a database at path."); map.insert(LONG_QUERY_COMMAND, "Execute a query against the current open database."); @@ -103,18 +110,25 @@ impl Repl { 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.close(), Command::Query(query) => self.execute_query(query), Command::Transact(transaction) => self.execute_transact(transaction), + Command::Exit => { + self.close(); + println!("Exiting..."); + process::exit(0); + } } } + fn close(&mut self) { + 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.display()) + }; + } + fn help_command(&self, args: Vec) { if args.is_empty() { for (cmd, msg) in COMMAND_HELP.iter() { From a9c05ba9b7365331f4ff83562e99306dde608ab3 Mon Sep 17 00:00:00 2001 From: Emily Toop Date: Fri, 23 Jun 2017 15:03:13 +0100 Subject: [PATCH 5/7] Show schema of current DB (#487) --- tools/cli/src/mentat_cli/command_parser.rs | 51 ++++++++++++++++++++-- tools/cli/src/mentat_cli/repl.rs | 10 +++++ tools/cli/src/mentat_cli/store.rs | 11 ++++- 3 files changed, 66 insertions(+), 6 deletions(-) diff --git a/tools/cli/src/mentat_cli/command_parser.rs b/tools/cli/src/mentat_cli/command_parser.rs index 36d4912f6..5d9f85682 100644 --- a/tools/cli/src/mentat_cli/command_parser.rs +++ b/tools/cli/src/mentat_cli/command_parser.rs @@ -37,6 +37,7 @@ pub static OPEN_COMMAND: &'static str = &"open"; pub static CLOSE_COMMAND: &'static str = &"close"; pub static LONG_QUERY_COMMAND: &'static str = &"query"; pub static SHORT_QUERY_COMMAND: &'static str = &"q"; +pub static SCHEMA_COMMAND: &'static str = &"schema"; pub static LONG_TRANSACT_COMMAND: &'static str = &"transact"; pub static SHORT_TRANSACT_COMMAND: &'static str = &"t"; pub static LONG_EXIT_COMMAND: &'static str = &"exit"; @@ -49,6 +50,7 @@ pub enum Command { Help(Vec), Open(String), Query(String), + Schema, Transact(String), } @@ -66,7 +68,8 @@ impl Command { &Command::Help(_) | &Command::Open(_) | &Command::Close | - &Command::Exit => true + &Command::Exit | + &Command::Schema => true } } @@ -83,13 +86,16 @@ impl Command { }, &Command::Open(ref args) => { format!(".{} {}", OPEN_COMMAND, args) - } + }, &Command::Close => { format!(".{}", CLOSE_COMMAND) }, &Command::Exit => { format!(".{}", LONG_EXIT_COMMAND) }, + &Command::Schema => { + format!(".{}", SCHEMA_COMMAND) + }, } } } @@ -129,6 +135,15 @@ pub fn command(s: &str) -> Result { } Ok(Command::Close) }); + + let schema_parser = string(SCHEMA_COMMAND) + .with(no_arg_parser()) + .map(|args| { + if !args.is_empty() { + bail!(cli::ErrorKind::CommandParse(format!("Unrecognized argument {:?}", args[0])) ); + } + Ok(Command::Schema) + }); let exit_parser = try(string(LONG_EXIT_COMMAND)).or(try(string(SHORT_EXIT_COMMAND))) .with(no_arg_parser()) @@ -161,12 +176,13 @@ pub fn command(s: &str) -> Result { spaces() .skip(token('.')) - .with(choice::<[&mut Parser>; 6], _> + .with(choice::<[&mut Parser>; 7], _> ([&mut try(help_parser), &mut try(open_parser), &mut try(close_parser), &mut try(exit_parser), &mut try(query_parser), + &mut try(schema_parser), &mut try(transact_parser)])) .parse(s) .unwrap_or((Err(cli::ErrorKind::CommandParse(format!("Invalid command {:?}", s)).into()), "")).0 @@ -323,7 +339,7 @@ mod tests { let cmd = command(&input).expect("Expected exit command"); match cmd { Command::Exit => assert!(true), - _ => assert!(false) + _ => assert!(false) } } @@ -346,6 +362,33 @@ mod tests { _ => assert!(false) } } + + #[test] + fn test_schema_parser_with_args() { + let input = ".schema arg1"; + let err = command(&input).expect_err("Expected an error"); + assert_eq!(err.to_string(), format!("Invalid command {:?}", input)); + } + + #[test] + fn test_schema_parser_no_args() { + let input = ".schema"; + let cmd = command(&input).expect("Expected schema command"); + match cmd { + Command::Schema => assert!(true), + _ => assert!(false) + } + } + + #[test] + fn test_schema_parser_no_args_trailing_whitespace() { + let input = ".schema "; + let cmd = command(&input).expect("Expected schema command"); + match cmd { + Command::Schema => assert!(true), + _ => assert!(false) + } + } #[test] fn test_query_parser_complete_edn() { diff --git a/tools/cli/src/mentat_cli/repl.rs b/tools/cli/src/mentat_cli/repl.rs index 5c3cb790e..acf1d208f 100644 --- a/tools/cli/src/mentat_cli/repl.rs +++ b/tools/cli/src/mentat_cli/repl.rs @@ -22,6 +22,7 @@ use command_parser::{ OPEN_COMMAND, LONG_QUERY_COMMAND, SHORT_QUERY_COMMAND, + SCHEMA_COMMAND, LONG_TRANSACT_COMMAND, SHORT_TRANSACT_COMMAND, LONG_EXIT_COMMAND, @@ -48,6 +49,7 @@ lazy_static! { map.insert(OPEN_COMMAND, "Open a database at path."); map.insert(LONG_QUERY_COMMAND, "Execute a query against the current open database."); map.insert(SHORT_QUERY_COMMAND, "Shortcut for `.query`. Execute a query against the current open database."); + map.insert(SCHEMA_COMMAND, "Output the schema for the current open database."); map.insert(LONG_TRANSACT_COMMAND, "Execute a transact against the current open database."); map.insert(SHORT_TRANSACT_COMMAND, "Shortcut for `.transact`. Execute a transact against the current open database."); map @@ -112,6 +114,14 @@ impl Repl { }, Command::Close => self.close(), Command::Query(query) => self.execute_query(query), + Command::Schema => { + let edn = self.store.fetch_schema(); + match edn.to_pretty(120) { + Ok(s) => println!("{}", s), + Err(e) => println!("{}", e) + }; + + } Command::Transact(transaction) => self.execute_transact(transaction), Command::Exit => { self.close(); diff --git a/tools/cli/src/mentat_cli/store.rs b/tools/cli/src/mentat_cli/store.rs index bc49443c6..9bfc1e8de 100644 --- a/tools/cli/src/mentat_cli/store.rs +++ b/tools/cli/src/mentat_cli/store.rs @@ -10,11 +10,14 @@ use rusqlite; +use edn; + use errors as cli; use mentat::{ new_connection, }; + use mentat::query::QueryResults; use mentat::conn::Conn; @@ -52,10 +55,14 @@ impl Store { } pub fn query(&self, query: String) -> Result { - Ok(try!(self.conn.q_once(&self.handle, &query, None))) + Ok(self.conn.q_once(&self.handle, &query, None)?) } pub fn transact(&mut self, transaction: String) -> Result { - Ok(try!(self.conn.transact(&mut self.handle, &transaction))) + Ok(self.conn.transact(&mut self.handle, &transaction)?) + } + + pub fn fetch_schema(&self) -> edn::Value { + self.conn.current_schema().to_edn_value() } } From 9932a98c6f6ee15a329a7999b6dc1d36a79a76b7 Mon Sep 17 00:00:00 2001 From: Emily Toop Date: Wed, 28 Jun 2017 16:58:29 -0700 Subject: [PATCH 6/7] show attributes of current database --- tools/cli/src/mentat_cli/command_parser.rs | 47 +++++++++++++++++++++- tools/cli/src/mentat_cli/repl.rs | 12 +++++- tools/cli/src/mentat_cli/store.rs | 17 ++++++++ 3 files changed, 72 insertions(+), 4 deletions(-) diff --git a/tools/cli/src/mentat_cli/command_parser.rs b/tools/cli/src/mentat_cli/command_parser.rs index 5d9f85682..131117bf6 100644 --- a/tools/cli/src/mentat_cli/command_parser.rs +++ b/tools/cli/src/mentat_cli/command_parser.rs @@ -38,6 +38,7 @@ pub static CLOSE_COMMAND: &'static str = &"close"; pub static LONG_QUERY_COMMAND: &'static str = &"query"; pub static SHORT_QUERY_COMMAND: &'static str = &"q"; pub static SCHEMA_COMMAND: &'static str = &"schema"; +pub static ATTRIBUTES_COMMAND: &'static str = &"attributes"; pub static LONG_TRANSACT_COMMAND: &'static str = &"transact"; pub static SHORT_TRANSACT_COMMAND: &'static str = &"t"; pub static LONG_EXIT_COMMAND: &'static str = &"exit"; @@ -51,6 +52,7 @@ pub enum Command { Open(String), Query(String), Schema, + Attributes, Transact(String), } @@ -69,7 +71,8 @@ impl Command { &Command::Open(_) | &Command::Close | &Command::Exit | - &Command::Schema => true + &Command::Schema | + &Command::Attributes => true } } @@ -96,6 +99,9 @@ impl Command { &Command::Schema => { format!(".{}", SCHEMA_COMMAND) }, + &Command::Attributes => { + format!(".{}", ATTRIBUTES_COMMAND) + }, } } } @@ -144,6 +150,15 @@ pub fn command(s: &str) -> Result { } Ok(Command::Schema) }); + + let attributes_parser = string(ATTRIBUTES_COMMAND) + .with(no_arg_parser()) + .map(|args| { + if !args.is_empty() { + bail!(cli::ErrorKind::CommandParse(format!("Unrecognized argument {:?}", args[0])) ); + } + Ok(Command::Attributes) + }); let exit_parser = try(string(LONG_EXIT_COMMAND)).or(try(string(SHORT_EXIT_COMMAND))) .with(no_arg_parser()) @@ -176,13 +191,14 @@ pub fn command(s: &str) -> Result { spaces() .skip(token('.')) - .with(choice::<[&mut Parser>; 7], _> + .with(choice::<[&mut Parser>; 8], _> ([&mut try(help_parser), &mut try(open_parser), &mut try(close_parser), &mut try(exit_parser), &mut try(query_parser), &mut try(schema_parser), + &mut try(attributes_parser), &mut try(transact_parser)])) .parse(s) .unwrap_or((Err(cli::ErrorKind::CommandParse(format!("Invalid command {:?}", s)).into()), "")).0 @@ -390,6 +406,33 @@ mod tests { } } + #[test] + fn test_attributes_parser_with_args() { + let input = ".attributes arg1"; + let err = command(&input).expect_err("Expected an error"); + assert_eq!(err.to_string(), format!("Invalid command {:?}", input)); + } + + #[test] + fn test_attributes_parser_no_args() { + let input = ".attributes"; + let cmd = command(&input).expect("Expected attributes command"); + match cmd { + Command::Attributes => assert!(true), + _ => assert!(false) + } + } + + #[test] + fn test_attributes_parser_no_args_trailing_whitespace() { + let input = ".attributes "; + let cmd = command(&input).expect("Expected attributes command"); + match cmd { + Command::Attributes => assert!(true), + _ => assert!(false) + } + } + #[test] fn test_query_parser_complete_edn() { let input = ".q [:find ?x :where [?x foo/bar ?y]]"; diff --git a/tools/cli/src/mentat_cli/repl.rs b/tools/cli/src/mentat_cli/repl.rs index acf1d208f..e4acc9e70 100644 --- a/tools/cli/src/mentat_cli/repl.rs +++ b/tools/cli/src/mentat_cli/repl.rs @@ -27,6 +27,7 @@ use command_parser::{ SHORT_TRANSACT_COMMAND, LONG_EXIT_COMMAND, SHORT_EXIT_COMMAND, + ATTRIBUTES_COMMAND, }; use input::InputReader; use input::InputResult::{ @@ -43,6 +44,7 @@ use store::{ lazy_static! { static ref COMMAND_HELP: HashMap<&'static str, &'static str> = { let mut map = HashMap::new(); + map.insert(ATTRIBUTES_COMMAND, "Output the attributes for the schema in the current open database."); map.insert(LONG_EXIT_COMMAND, "Close the current database and exit the REPL."); map.insert(SHORT_EXIT_COMMAND, "Shortcut for `.exit`. Close the current database and exit the REPL."); map.insert(HELP_COMMAND, "Show help for commands."); @@ -120,8 +122,14 @@ impl Repl { Ok(s) => println!("{}", s), Err(e) => println!("{}", e) }; - - } + }, + Command::Attributes => { + let edn = self.store.fetch_attributes(); + match edn.to_pretty(120) { + Ok(s) => println!("{}", s), + Err(e) => println!("{}", e) + }; + }, Command::Transact(transaction) => self.execute_transact(transaction), Command::Exit => { self.close(); diff --git a/tools/cli/src/mentat_cli/store.rs b/tools/cli/src/mentat_cli/store.rs index 9bfc1e8de..f59a17adb 100644 --- a/tools/cli/src/mentat_cli/store.rs +++ b/tools/cli/src/mentat_cli/store.rs @@ -62,7 +62,24 @@ impl Store { Ok(self.conn.transact(&mut self.handle, &transaction)?) } + // the schema is the entire schema of the store including structure used to describe the store. pub fn fetch_schema(&self) -> edn::Value { self.conn.current_schema().to_edn_value() } + + // the attributes are the specific attributes added to the schema for this particular store. + pub fn fetch_attributes(&self) -> edn::Value { + let schema = self.conn.current_schema(); + + edn::Value::Vector((&schema.schema_map).iter() + .filter_map(|(entid, attribute)| { + if let Some(ident) = schema.get_ident(*entid) { + if !ident.namespace.starts_with("db") { + return Some(attribute.to_edn_value(Some(ident.clone()))); + } + } + return None; + }) + .collect()) + } } From eb09f9a5d1b1d9fb1dde76ae83749bcccb043c7a Mon Sep 17 00:00:00 2001 From: Emily Toop Date: Thu, 29 Jun 2017 13:35:09 -0700 Subject: [PATCH 7/7] Simply no argument parsing --- tools/cli/src/mentat_cli/command_parser.rs | 44 ++++++---------------- 1 file changed, 12 insertions(+), 32 deletions(-) diff --git a/tools/cli/src/mentat_cli/command_parser.rs b/tools/cli/src/mentat_cli/command_parser.rs index 131117bf6..31089ba94 100644 --- a/tools/cli/src/mentat_cli/command_parser.rs +++ b/tools/cli/src/mentat_cli/command_parser.rs @@ -128,46 +128,26 @@ pub fn command(s: &str) -> Result { } Ok(Command::Open(args[0].clone())) }); - - let no_arg_parser = || arguments() - .skip(spaces()) - .skip(eof()); let close_parser = string(CLOSE_COMMAND) - .with(no_arg_parser()) - .map(|args| { - if !args.is_empty() { - bail!(cli::ErrorKind::CommandParse(format!("Unrecognized argument {:?}", args[0])) ); - } - Ok(Command::Close) - }); + .skip(spaces()) + .skip(eof()) + .map(|_| Ok(Command::Close) ); let schema_parser = string(SCHEMA_COMMAND) - .with(no_arg_parser()) - .map(|args| { - if !args.is_empty() { - bail!(cli::ErrorKind::CommandParse(format!("Unrecognized argument {:?}", args[0])) ); - } - Ok(Command::Schema) - }); + .skip(spaces()) + .skip(eof()) + .map(|_| Ok(Command::Schema)); let attributes_parser = string(ATTRIBUTES_COMMAND) - .with(no_arg_parser()) - .map(|args| { - if !args.is_empty() { - bail!(cli::ErrorKind::CommandParse(format!("Unrecognized argument {:?}", args[0])) ); - } - Ok(Command::Attributes) - }); + .skip(spaces()) + .skip(eof()) + .map(|_| Ok(Command::Attributes)); let exit_parser = try(string(LONG_EXIT_COMMAND)).or(try(string(SHORT_EXIT_COMMAND))) - .with(no_arg_parser()) - .map(|args| { - if !args.is_empty() { - bail!(cli::ErrorKind::CommandParse(format!("Unrecognized argument {:?}", args[0])) ); - } - Ok(Command::Exit) - }); + .skip(spaces()) + .skip(eof()) + .map(|_| Ok(Command::Exit) ); let edn_arg_parser = || spaces() .with(look_ahead(string("[").or(string("{")))