Compare commits
8 commits
master
...
fluffyemil
Author | SHA1 | Date | |
---|---|---|---|
|
7b923e6826 | ||
|
29ac9cdcac | ||
|
2b6398be55 | ||
|
edb5755b1e | ||
|
6ba3fc9f06 | ||
|
4627a2f993 | ||
|
b76dd74108 | ||
|
86a25b68e1 |
11 changed files with 1196 additions and 3 deletions
|
@ -11,7 +11,7 @@ version = "0.4.0"
|
|||
build = "build/version.rs"
|
||||
|
||||
[workspace]
|
||||
members = []
|
||||
members = ["tools/cli"]
|
||||
|
||||
[build-dependencies]
|
||||
rustc_version = "0.1.7"
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
45
tools/cli/Cargo.toml
Normal file
45
tools/cli/Cargo.toml
Normal file
|
@ -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"
|
16
tools/cli/src/bin/mentat_cli.rs
Normal file
16
tools/cli/src/bin/mentat_cli.rs
Normal file
|
@ -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);
|
||||
}
|
524
tools/cli/src/mentat_cli/command_parser.rs
Normal file
524
tools/cli/src/mentat_cli/command_parser.rs
Normal file
|
@ -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<String>),
|
||||
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<Command, cli::Error> {
|
||||
let arguments = || sep_end_by::<Vec<_>, _, _>(many1(satisfy(|c: char| !c.is_whitespace())), many1::<Vec<_>, _>(space())).expected("arguments");
|
||||
|
||||
let help_parser = string(HELP_COMMAND)
|
||||
.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::<Vec<_>, _>(try(any())))
|
||||
.and_then(|args| -> Result<String, cli::Error> {
|
||||
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<Input = _, Output = Result<Command, cli::Error>>; 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<String> = 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<String> = 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));
|
||||
}
|
||||
}
|
36
tools/cli/src/mentat_cli/errors.rs
Normal file
36
tools/cli/src/mentat_cli/errors.rs
Normal file
|
@ -0,0 +1,36 @@
|
|||
// Copyright 2016 Mozilla
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use
|
||||
// this file except in compliance with the License. You may obtain a copy of the
|
||||
// License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
// Unless required by applicable law or agreed to in writing, software distributed
|
||||
// under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
||||
// CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations under the License.
|
||||
|
||||
#![allow(dead_code)]
|
||||
|
||||
use rusqlite;
|
||||
|
||||
use mentat::errors as mentat;
|
||||
|
||||
error_chain! {
|
||||
types {
|
||||
Error, ErrorKind, ResultExt, Result;
|
||||
}
|
||||
|
||||
foreign_links {
|
||||
Rusqlite(rusqlite::Error);
|
||||
}
|
||||
|
||||
links {
|
||||
MentatError(mentat::Error, mentat::ErrorKind);
|
||||
}
|
||||
|
||||
errors {
|
||||
CommandParse(message: String) {
|
||||
description("An error occured parsing the entered command")
|
||||
display("{}", message)
|
||||
}
|
||||
}
|
||||
}
|
157
tools/cli/src/mentat_cli/input.rs
Normal file
157
tools/cli/src/mentat_cli/input.rs
Normal file
|
@ -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<Reader<DefaultTerminal>>,
|
||||
in_process_cmd: Option<Command>,
|
||||
}
|
||||
|
||||
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<InputResult, cli::Error> {
|
||||
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<String> {
|
||||
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<String> {
|
||||
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() {
|
||||
}
|
||||
}
|
121
tools/cli/src/mentat_cli/lib.rs
Normal file
121
tools/cli/src/mentat_cli/lib.rs
Normal file
|
@ -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::<Vec<_>>();
|
||||
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<command_parser::Command> = 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() {
|
||||
}
|
||||
}
|
226
tools/cli/src/mentat_cli/repl.rs
Normal file
226
tools/cli/src/mentat_cli/repl.rs
Normal file
|
@ -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<Repl, String> {
|
||||
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<Vec<Command>>) {
|
||||
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<String>) {
|
||||
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() {
|
||||
}
|
||||
}
|
68
tools/cli/src/mentat_cli/store.rs
Normal file
68
tools/cli/src/mentat_cli/store.rs
Normal file
|
@ -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<String>) -> Result<Store, cli::Error> {
|
||||
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<String>) -> 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<QueryResults, cli::Error> {
|
||||
Ok(self.conn.q_once(&self.handle, &query, None)?)
|
||||
}
|
||||
|
||||
pub fn transact(&mut self, transaction: String) -> Result<TxReport, cli::Error> {
|
||||
Ok(self.conn.transact(&mut self.handle, &transaction)?)
|
||||
}
|
||||
|
||||
pub fn fetch_schema(&self) -> edn::Value {
|
||||
self.conn.current_schema().to_edn_value()
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue