diff --git a/Cargo.toml b/Cargo.toml index aa5d66da..fae66bf9 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" diff --git a/build/version.rs b/build/version.rs index 54f8b2a4..6ad73157 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 b2c08fc3..e15148f0 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 new file mode 100644 index 00000000..0958a369 --- /dev/null +++ b/tools/cli/Cargo.toml @@ -0,0 +1,45 @@ +[package] +name = "mentat_cli" +version = "0.0.1" + +[lib] +name = "mentat_cli" +path = "src/mentat_cli/lib.rs" + +[[bin]] +name = "mentat_cli" +doc = false +test = false + +[dependencies] +getopts = "0.2" +env_logger = "0.3" +linefeed = "0.1" +log = "0.3" +tempfile = "1.1" +combine = "2.2.2" +lazy_static = "0.2" +error-chain = { git = "https://github.com/rnewman/error-chain", branch = "rnewman/sync" } + +[dependencies.rusqlite] +version = "0.12" +# 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/bin/mentat_cli.rs b/tools/cli/src/bin/mentat_cli.rs new file mode 100644 index 00000000..ea785edd --- /dev/null +++ b/tools/cli/src/bin/mentat_cli.rs @@ -0,0 +1,16 @@ +// 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/command_parser.rs b/tools/cli/src/mentat_cli/command_parser.rs new file mode 100644 index 00000000..5d9f8568 --- /dev/null +++ b/tools/cli/src/mentat_cli/command_parser.rs @@ -0,0 +1,524 @@ +// 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, + satisfy, + sep_end_by, + token, + Parser +}; +use combine::char::{ + space, + spaces, + string +}; +use combine::combinator::{ + choice, + try +}; + +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 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"; +pub static SHORT_EXIT_COMMAND: &'static str = &"e"; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum Command { + Close, + Exit, + Help(Vec), + Open(String), + Query(String), + Schema, + Transact(String), +} + +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 | + &Command::Exit | + &Command::Schema => 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) + }, + &Command::Exit => { + format!(".{}", LONG_EXIT_COMMAND) + }, + &Command::Schema => { + format!(".{}", SCHEMA_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 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) + }); + + 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()) + .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()))) + .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>; 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 +} + +#[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_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_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() { + 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 00000000..8e095ab3 --- /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 new file mode 100644 index 00000000..2a614e5a --- /dev/null +++ b/tools/cli/src/mentat_cli/input.rs @@ -0,0 +1,157 @@ +// 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::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 { + /// mentat command as input; (name, rest of line) + MetaCommand(Command), + /// An empty line + Empty, + /// Needs more input + More, + /// End of file reached + Eof, +} + +/// Reads input from `stdin` +pub struct InputReader { + buffer: String, + reader: Option>, + in_process_cmd: 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, + in_process_cmd: None, + } + } + + /// 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) -> 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 Ok(Eof), + }; + + self.buffer.push_str(&line); + + if self.buffer.is_empty() { + return Ok(Empty); + } + + self.add_history(&line); + + // 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)) + } + }; + + 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 { + 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 00000000..bcc7c71c --- /dev/null +++ b/tools/cli/src/mentat_cli/lib.rs @@ -0,0 +1,121 @@ +// 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; +#[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(); + + 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..]) { + 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 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 +} + +/// 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 00000000..b7c953bd --- /dev/null +++ b/tools/cli/src/mentat_cli/repl.rs @@ -0,0 +1,226 @@ +// 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::collections::HashMap; +use std::process; + +use mentat::query::QueryResults; +use mentat_core::TypedValue; + +use command_parser::{ + Command, + HELP_COMMAND, + OPEN_COMMAND, + LONG_QUERY_COMMAND, + SHORT_QUERY_COMMAND, + SCHEMA_COMMAND, + LONG_TRANSACT_COMMAND, + SHORT_TRANSACT_COMMAND, + LONG_EXIT_COMMAND, + SHORT_EXIT_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(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."); + 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 + }; +} + +/// Executes input and maintains state of persistent items. +pub struct Repl { + store: Store +} + +impl Repl { + /// Constructs a new `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, 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(); + + match res { + Ok(MetaCommand(cmd)) => { + debug!("read command: {:?}", cmd); + self.handle_command(cmd); + }, + 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: 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 => 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(); + 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) + }; + } + + 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), + } + } +} + +#[cfg(test)] +mod tests { + #[test] + fn it_works() { + } +} diff --git a/tools/cli/src/mentat_cli/store.rs b/tools/cli/src/mentat_cli/store.rs new file mode 100644 index 00000000..9bfc1e8d --- /dev/null +++ b/tools/cli/src/mentat_cli/store.rs @@ -0,0 +1,68 @@ +// 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 edn; + +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(self.conn.q_once(&self.handle, &query, None)?) + } + + pub fn transact(&mut self, transaction: String) -> Result { + Ok(self.conn.transact(&mut self.handle, &transaction)?) + } + + pub fn fetch_schema(&self) -> edn::Value { + self.conn.current_schema().to_edn_value() + } +}