Compare commits
13 commits
master
...
fluffyemil
Author | SHA1 | Date | |
---|---|---|---|
|
223a53cf9a | ||
|
831f7032ce | ||
|
401abde971 | ||
|
ced479c466 | ||
|
3a8e3c7e78 | ||
|
f9a9aca2f5 | ||
|
b6152cfd4b | ||
|
7e42aafcc0 | ||
|
de616a4f62 | ||
|
66c402b205 | ||
|
46fc1615fb | ||
|
5a6c3f6598 | ||
|
9b30a2c0a7 |
15 changed files with 1663 additions and 4 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;
|
||||
|
|
|
@ -245,6 +245,7 @@ impl SchemaTypeChecking for Schema {
|
|||
// Ref coerces a little: we interpret some things depending on the schema as a Ref.
|
||||
(&ValueType::Ref, TypedValue::Long(x)) => Ok(TypedValue::Ref(x)),
|
||||
(&ValueType::Ref, TypedValue::Keyword(ref x)) => self.require_entid(&x).map(|entid| TypedValue::Ref(entid)),
|
||||
(&ValueType::Instant, TypedValue::Instant(x)) => Ok(TypedValue::Instant(x)),
|
||||
// Otherwise, we have a type mismatch.
|
||||
(value_type, _) => bail!(ErrorKind::BadEDNValuePair(value.clone(), value_type.clone())),
|
||||
}
|
||||
|
|
38
fixtures/movie.edn
Normal file
38
fixtures/movie.edn
Normal file
|
@ -0,0 +1,38 @@
|
|||
[
|
||||
{:db/ident :movie/title
|
||||
:db/valueType :db.type/string
|
||||
:db/cardinality :db.cardinality/one}
|
||||
|
||||
{:db/ident :movie/year
|
||||
:db/valueType :db.type/long
|
||||
:db/cardinality :db.cardinality/one}
|
||||
|
||||
{:db/ident :movie/director
|
||||
:db/valueType :db.type/ref
|
||||
:db/cardinality :db.cardinality/many}
|
||||
|
||||
{:db/ident :movie/sequel
|
||||
:db/valueType :db.type/ref
|
||||
:db/cardinality :db.cardinality/one}
|
||||
|
||||
{:db/ident :movie/cast
|
||||
:db/valueType :db.type/ref
|
||||
:db/cardinality :db.cardinality/many}
|
||||
|
||||
{:db/ident :person/name
|
||||
:db/valueType :db.type/string
|
||||
:db/unique :db.unique/identity
|
||||
:db/cardinality :db.cardinality/one}
|
||||
|
||||
{:db/ident :person/born
|
||||
:db/valueType :db.type/instant
|
||||
:db/cardinality :db.cardinality/one}
|
||||
|
||||
{:db/ident :person/death
|
||||
:db/valueType :db.type/instant
|
||||
:db/cardinality :db.cardinality/one}
|
||||
|
||||
{:db/ident :movie/trivia
|
||||
:db/valueType :db.type/string
|
||||
:db/cardinality :db.cardinality/many}
|
||||
]
|
376
fixtures/movie_data.edn
Normal file
376
fixtures/movie_data.edn
Normal file
|
@ -0,0 +1,376 @@
|
|||
[
|
||||
{:db/id "james-cameron"
|
||||
:person/name "James Cameron"
|
||||
:person/born #inst "1954-08-16T00:00:00Z"}
|
||||
|
||||
{:db/id "arnold-schwarzenegger"
|
||||
:person/name "Arnold Schwarzenegger"
|
||||
:person/born #inst "1947-07-30T00:00:00Z"}
|
||||
|
||||
{:db/id "linda-hamilton"
|
||||
:person/name "Linda Hamilton"
|
||||
:person/born #inst "1956-09-26T00:00:00Z"}
|
||||
|
||||
{:db/id "michael-biehn"
|
||||
:person/name "Michael Biehn"
|
||||
:person/born #inst "1956-07-31T00:00:00Z"}
|
||||
|
||||
{:db/id "ted-kotcheff"
|
||||
:person/name "Ted Kotcheff"
|
||||
:person/born #inst "1931-04-07T00:00:00Z"}
|
||||
|
||||
{:db/id "sylvester-stallone"
|
||||
:person/name "Sylvester Stallone"
|
||||
:person/born #inst "1946-07-06T00:00:00Z"}
|
||||
|
||||
{:db/id "richard-crenna"
|
||||
:person/name "Richard Crenna"
|
||||
:person/born #inst "1926-11-30T00:00:00.000Z"
|
||||
:person/death #inst "2003-01-17T00:00:00.000Z"}
|
||||
|
||||
{:db/id "brian-dennehy"
|
||||
:person/name "Brian Dennehy"
|
||||
:person/born #inst "1938-07-09T00:00:00.000Z"}
|
||||
|
||||
{:db/id "john-mctiernan"
|
||||
:person/name "John McTiernan"
|
||||
:person/born #inst "1951-01-08T00:00:00.000Z"}
|
||||
|
||||
{:db/id "elpidia-carrillo"
|
||||
:person/name "Elpidia Carrillo"
|
||||
:person/born #inst "1961-08-16T00:00:00.000Z"}
|
||||
|
||||
{:db/id "carl-weathers"
|
||||
:person/name "Carl Weathers"
|
||||
:person/born #inst "1948-01-14T00:00:00.000Z"}
|
||||
|
||||
{:db/id "richard-donner"
|
||||
:person/name "Richard Donner"
|
||||
:person/born #inst "1930-04-24T00:00:00.000Z"}
|
||||
|
||||
{:db/id "mel-gibson"
|
||||
:person/name "Mel Gibson"
|
||||
:person/born #inst "1956-01-03T00:00:00.000Z"}
|
||||
|
||||
{:db/id "danny-glover"
|
||||
:person/name "Danny Glover"
|
||||
:person/born #inst "1946-07-22T00:00:00.000Z"}
|
||||
|
||||
{:db/id "gary-busey"
|
||||
:person/name "Gary Busey"
|
||||
:person/born #inst "1944-07-29T00:00:00.000Z"}
|
||||
|
||||
{:db/id "paul-verhoeven"
|
||||
:person/name "Paul Verhoeven"
|
||||
:person/born #inst "1938-07-18T00:00:00.000Z"}
|
||||
|
||||
{:db/id "peter-weller"
|
||||
:person/name "Peter Weller"
|
||||
:person/born #inst "1947-06-24T00:00:00.000Z"}
|
||||
|
||||
{:db/id "nancy-allen"
|
||||
:person/name "Nancy Allen"
|
||||
:person/born #inst "1950-06-24T00:00:00.000Z"}
|
||||
|
||||
{:db/id "ronny-cox"
|
||||
:person/name "Ronny Cox"
|
||||
:person/born #inst "1938-07-23T00:00:00.000Z"}
|
||||
|
||||
{:db/id "mark-l-lester"
|
||||
:person/name "Mark L. Lester"
|
||||
:person/born #inst "1946-11-26T00:00:00.000Z"}
|
||||
|
||||
{:db/id "rae-dawn-chong"
|
||||
:person/name "Rae Dawn Chong"
|
||||
:person/born #inst "1961-02-28T00:00:00.000Z"}
|
||||
|
||||
{:db/id "alyssa-milano"
|
||||
:person/name "Alyssa Milano"
|
||||
:person/born #inst "1972-12-19T00:00:00.000Z"}
|
||||
|
||||
{:db/id "bruce-willis"
|
||||
:person/name "Bruce Willis"
|
||||
:person/born #inst "1955-03-19T00:00:00.000Z"}
|
||||
|
||||
{:db/id "alan-rickman"
|
||||
:person/name "Alan Rickman"
|
||||
:person/born #inst "1946-02-21T00:00:00.000Z"}
|
||||
|
||||
{:db/id "alexander-godunov"
|
||||
:person/name "Alexander Godunov"
|
||||
:person/born #inst "1949-11-28T00:00:00.000Z"
|
||||
:person/death #inst "1995-05-18T00:00:00.000Z"}
|
||||
|
||||
{:db/id "robert-patrick"
|
||||
:person/name "Robert Patrick"
|
||||
:person/born #inst "1958-11-05T00:00:00.000Z"}
|
||||
|
||||
{:db/id "edward-furlong"
|
||||
:person/name "Edward Furlong"
|
||||
:person/born #inst "1977-08-02T00:00:00.000Z"}
|
||||
|
||||
{:db/id "jonathan-mostow"
|
||||
:person/name "Jonathan Mostow"
|
||||
:person/born #inst "1961-11-28T00:00:00.000Z"}
|
||||
|
||||
{:db/id "nick-stahl"
|
||||
:person/name "Nick Stahl"
|
||||
:person/born #inst "1979-12-05T00:00:00.000Z"}
|
||||
|
||||
{:db/id "claire-danes"
|
||||
:person/name "Claire Danes"
|
||||
:person/born #inst "1979-04-12T00:00:00.000Z"}
|
||||
|
||||
{:db/id "george-p-cosmatos"
|
||||
:person/name "George P. Cosmatos"
|
||||
:person/born #inst "1941-01-04T00:00:00.000Z"
|
||||
:person/death #inst "2005-04-19T00:00:00.000Z"}
|
||||
|
||||
{:db/id "charles-napier"
|
||||
:person/name "Charles Napier"
|
||||
:person/born #inst "1936-04-12T00:00:00.000Z"
|
||||
:person/death #inst "2011-10-05T00:00:00.000Z"}
|
||||
|
||||
{:db/id "peter-macdonald"
|
||||
:person/name "Peter MacDonald"}
|
||||
|
||||
{:db/id "marc-de-jonge"
|
||||
:person/name "Marc de Jonge"
|
||||
:person/born #inst "1949-02-16T00:00:00.000Z"
|
||||
:person/death #inst "1996-06-06T00:00:00.000Z"}
|
||||
|
||||
{:db/id "stephen-hopkins"
|
||||
:person/name "Stephen Hopkins"}
|
||||
|
||||
{:db/id "ruben-blades"
|
||||
:person/name "Ruben Blades"
|
||||
:person/born #inst "1948-07-16T00:00:00.000Z"}
|
||||
|
||||
{:db/id "joe-pesci"
|
||||
:person/name "Joe Pesci"
|
||||
:person/born #inst "1943-02-09T00:00:00.000Z"}
|
||||
|
||||
{:db/id "ridley-scott"
|
||||
:person/name "Ridley Scott"
|
||||
:person/born #inst "1937-11-30T00:00:00.000Z"}
|
||||
|
||||
{:db/id "tom-skerritt"
|
||||
:person/name "Tom Skerritt"
|
||||
:person/born #inst "1933-08-25T00:00:00.000Z"}
|
||||
|
||||
{:db/id "sigourney-weaver"
|
||||
:person/name "Sigourney Weaver"
|
||||
:person/born #inst "1949-10-08T00:00:00.000Z"}
|
||||
|
||||
{:db/id "veronica-cartwright"
|
||||
:person/name "Veronica Cartwright"
|
||||
:person/born #inst "1949-04-20T00:00:00.000Z"}
|
||||
|
||||
{:db/id "carrie-henn"
|
||||
:person/name "Carrie Henn"}
|
||||
|
||||
{:db/id "george-miller"
|
||||
:person/name "George Miller"
|
||||
:person/born #inst "1945-03-03T00:00:00.000Z"}
|
||||
|
||||
{:db/id "steve-bisley"
|
||||
:person/name "Steve Bisley"
|
||||
:person/born #inst "1951-12-26T00:00:00.000Z"}
|
||||
|
||||
{:db/id "joanne-samuel"
|
||||
:person/name "Joanne Samuel"}
|
||||
|
||||
{:db/id "michael-preston"
|
||||
:person/name "Michael Preston"
|
||||
:person/born #inst "1938-05-14T00:00:00.000Z"}
|
||||
|
||||
{:db/id "bruce-spence"
|
||||
:person/name "Bruce Spence"
|
||||
:person/born #inst "1945-09-17T00:00:00.000Z"}
|
||||
|
||||
{:db/id "george-ogilvie"
|
||||
:person/name "George Ogilvie"
|
||||
:person/born #inst "1931-03-05T00:00:00.000Z"}
|
||||
|
||||
{:db/id "tina-turner"
|
||||
:person/name "Tina Turner"
|
||||
:person/born #inst "1939-11-26T00:00:00.000Z"}
|
||||
|
||||
{:db/id "sophie-marceau"
|
||||
:person/name "Sophie Marceau"
|
||||
:person/born #inst "1966-11-17T00:00:00.000Z"}
|
||||
|
||||
{:db/id "terminator-3"
|
||||
:movie/title "Terminator 3: Rise of the Machines"
|
||||
:movie/year 2003
|
||||
:movie/director "jonathan-mostow"
|
||||
:movie/cast ["arnold-schwarzenegger"
|
||||
"nick-stahl"
|
||||
"claire-danes"]}
|
||||
|
||||
{:db/id "terminator-2"
|
||||
:movie/title "Terminator 2: Judgment Day"
|
||||
:movie/year 1991
|
||||
:movie/director "james-cameron"
|
||||
:movie/cast ["arnold-schwarzenegger"
|
||||
"linda-hamilton"
|
||||
"robert-patrick"
|
||||
"edward-furlong"]
|
||||
:movie/sequel "terminator-3"}
|
||||
|
||||
{:db/id "the-terminator"
|
||||
:movie/title "The Terminator"
|
||||
:movie/year 1984
|
||||
:movie/director "james-cameron"
|
||||
:movie/cast ["arnold-schwarzenegger"
|
||||
"linda-hamilton"
|
||||
"michael-biehn"]
|
||||
:movie/sequel "terminator-2"}
|
||||
|
||||
{:db/id "rambo-3"
|
||||
:movie/title "Rambo III"
|
||||
:movie/year 1988
|
||||
:movie/director "peter-macdonald"
|
||||
:movie/cast ["sylvester-stallone"
|
||||
"richard-crenna"
|
||||
"marc-de-jonge"]}
|
||||
|
||||
{:db/id "rambo-2"
|
||||
:movie/title "Rambo: First Blood Part II"
|
||||
:movie/year 1985
|
||||
:movie/director "george-p-cosmatos"
|
||||
:movie/cast ["sylvester-stallone"
|
||||
"richard-crenna"
|
||||
"charles-napier"]
|
||||
:movie/sequel "rambo-3"}
|
||||
|
||||
{:db/id "first-blood"
|
||||
:movie/title "First Blood"
|
||||
:movie/year 1982
|
||||
:movie/director "ted-kotcheff"
|
||||
:movie/cast ["sylvester-stallone"
|
||||
"richard-crenna"
|
||||
"brian-dennehy"]
|
||||
:movie/sequel "rambo-2"}
|
||||
|
||||
{:db/id "predator-2"
|
||||
:movie/title "Predator 2"
|
||||
:movie/year 1990
|
||||
:movie/director "stephen-hopkins"
|
||||
:movie/cast ["danny-glover"
|
||||
"gary-busey"
|
||||
"ruben-blades"]}
|
||||
|
||||
{:db/id "predator"
|
||||
:movie/title "Predator"
|
||||
:movie/year 1987
|
||||
:movie/director "john-mctiernan"
|
||||
:movie/cast ["arnold-schwarzenegger"
|
||||
"elpidia-carrillo"
|
||||
"carl-weathers"]
|
||||
:movie/sequel "predator-2"}
|
||||
|
||||
{:db/id "lethal-weapon-3"
|
||||
:movie/title "Lethal Weapon 3"
|
||||
:movie/year 1992
|
||||
:movie/director "richard-donner"
|
||||
:movie/cast ["mel-gibson"
|
||||
"danny-glover"
|
||||
"joe-pesci"]}
|
||||
|
||||
{:db/id "lethal-weapon-2"
|
||||
:movie/title "Lethal Weapon 2"
|
||||
:movie/year 1989
|
||||
:movie/director "richard-donner"
|
||||
:movie/cast ["mel-gibson"
|
||||
"danny-glover"
|
||||
"joe-pesci"]
|
||||
:movie/sequel "lethal-weapon-3"}
|
||||
|
||||
{:db/id "lethal-weapon"
|
||||
:movie/title "Lethal Weapon"
|
||||
:movie/year 1987
|
||||
:movie/director "richard-donner"
|
||||
:movie/cast ["mel-gibson"
|
||||
"danny-glover"
|
||||
"gary-busey"]
|
||||
:movie/sequel "lethal-weapon-2"}
|
||||
|
||||
{:db/id "robocop"
|
||||
:movie/title "RoboCop"
|
||||
:movie/year 1987
|
||||
:movie/director "paul-verhoeven"
|
||||
:movie/cast ["peter-weller"
|
||||
"nancy-allen"
|
||||
"ronny-cox"]}
|
||||
|
||||
{:db/id "commando"
|
||||
:movie/title "Commando"
|
||||
:movie/year 1985
|
||||
:movie/director "mark-l-lester"
|
||||
:movie/cast ["arnold-schwarzenegger"
|
||||
"alyssa-milano"
|
||||
"rae-dawn-chong"]
|
||||
:movie/trivia "In 1986, a sequel was written with an eye to having
|
||||
John McTiernan direct. Schwarzenegger wasn't interested in reprising
|
||||
the role. The script was then reworked with a new central character,
|
||||
eventually played by Bruce Willis, and became Die Hard"}
|
||||
|
||||
{:db/id "die-hard"
|
||||
:movie/title "Die Hard"
|
||||
:movie/year 1988
|
||||
:movie/director "john mctiernan"
|
||||
:movie/cast [ "bruce-willis"
|
||||
"alan-rickman"
|
||||
"alexander-godunov"]}
|
||||
|
||||
{:db/id "aliens"
|
||||
:movie/title "Aliens"
|
||||
:movie/year 1986
|
||||
:movie/director "james-cameron"
|
||||
:movie/cast ["sigourney-weaver"
|
||||
"carrie-henn"
|
||||
"michael-biehn"]}
|
||||
|
||||
{:db/id "alien"
|
||||
:movie/title "Alien"
|
||||
:movie/year 1979
|
||||
:movie/director "ridley-scott"
|
||||
:movie/cast ["tom-skerritt"
|
||||
"sigourney-weaver"
|
||||
"veronica-cartwright"]
|
||||
:movie/sequel "aliens"}
|
||||
|
||||
{:db/id "mad-max-3"
|
||||
:movie/title "Mad Max Beyond Thunderdome"
|
||||
:movie/year 1985
|
||||
:movie/director ["george-miller"
|
||||
"george-ogilvie"]
|
||||
:movie/cast ["mel-gibson"
|
||||
"tina-turner"]}
|
||||
|
||||
{:db/id "mad-max-2"
|
||||
:movie/title "Mad Max 2"
|
||||
:movie/year 1981
|
||||
:movie/director "george-miller"
|
||||
:movie/cast ["mel-gibson"
|
||||
"michael-preston"
|
||||
"bruce-spence"]
|
||||
:movie/sequel "mad-max-3"}
|
||||
|
||||
{:db/id "mad-max"
|
||||
:movie/title "Mad Max"
|
||||
:movie/year 1979
|
||||
:movie/director "george-miller"
|
||||
:movie/cast ["mel-gibson"
|
||||
"steve-bisley"
|
||||
"joanne-samuel"]
|
||||
:movie/sequel "mad-max-2"}
|
||||
|
||||
{:db/id "braveheart"
|
||||
:movie/title "Braveheart"
|
||||
:movie/year 1995
|
||||
:movie/director "mel-gibson"
|
||||
:movie/cast ["mel-gibson"
|
||||
"sophie-marceau"]}
|
||||
]
|
|
@ -116,6 +116,29 @@ impl QueryResults {
|
|||
&FindRel(_) => Box::new(|| QueryResults::Rel(vec![])),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_pretty(&self) -> Result<String> {
|
||||
use QueryResults::*;
|
||||
match self {
|
||||
&Scalar(Some(ref val)) => {
|
||||
Ok(val.value_type().to_edn_value().to_pretty(120).unwrap_or(String::new()) )
|
||||
},
|
||||
&Tuple(Some(ref vals)) => {
|
||||
Ok(vals.iter().map(|val| format!("{}\t", val.value_type().to_edn_value().to_pretty(120).unwrap_or(String::new()))).collect::<Vec<String>>().into_iter().collect::<String>())
|
||||
},
|
||||
&Coll(ref vals) => {
|
||||
Ok(vals.iter().map(|val| format!("{}\n", val.value_type().to_edn_value().to_pretty(120).unwrap_or(String::new()))).collect::<Vec<String>>().into_iter().collect::<String>())
|
||||
},
|
||||
&Rel(ref valsvec) => {
|
||||
let mut output = String::new();
|
||||
for vals in valsvec {
|
||||
output.push_str(&format!("{}\n", &vals.iter().map(|val| format!("{}\t", val.value_type().to_edn_value().to_pretty(120).unwrap_or(String::new()))).collect::<Vec<String>>().into_iter().collect::<String>()));
|
||||
}
|
||||
Ok(output)
|
||||
},
|
||||
_ => Ok("No results found.".to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type Index = i32; // See rusqlite::RowIndex.
|
||||
|
@ -496,4 +519,4 @@ pub fn query_projection(query: &AlgebraicQuery) -> CombinedProjection {
|
|||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
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]
|
||||
combine = "2.2.2"
|
||||
getopts = "0.2"
|
||||
env_logger = "0.3"
|
||||
error-chain = "0.8.1"
|
||||
lazy_static = "0.2.2"
|
||||
linefeed = "0.1"
|
||||
log = "0.3"
|
||||
tempfile = "1.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"
|
15
tools/cli/src/bin/mentat_cli.rs
Normal file
15
tools/cli/src/bin/mentat_cli.rs
Normal file
|
@ -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);
|
||||
}
|
546
tools/cli/src/mentat_cli/command_parser.rs
Normal file
546
tools/cli/src/mentat_cli/command_parser.rs
Normal file
|
@ -0,0 +1,546 @@
|
|||
// 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 LONG_TRANSACT_COMMAND: &'static str = &"transact";
|
||||
pub static SHORT_TRANSACT_COMMAND: &'static str = &"t";
|
||||
pub static READ_COMMAND: &'static str = &"read";
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum Command {
|
||||
Transact(String),
|
||||
Query(String),
|
||||
Help(Vec<String>),
|
||||
Open(String),
|
||||
Read(Vec<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, Option<edn::ParseError>) {
|
||||
match self {
|
||||
&Command::Query(ref args) |
|
||||
&Command::Transact(ref args) => {
|
||||
let r = edn::parse::value(&args);
|
||||
(r.is_ok(), r.err())
|
||||
},
|
||||
&Command::Help(_) |
|
||||
&Command::Open(_) |
|
||||
&Command::Close |
|
||||
&Command::Read(_) => (true, None)
|
||||
}
|
||||
}
|
||||
|
||||
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::Read(ref args) => {
|
||||
format!(".{} {:?}", READ_COMMAND, args)
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 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::<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))
|
||||
});
|
||||
|
||||
let read_parser = string(READ_COMMAND)
|
||||
.with(spaces())
|
||||
.with(arguments())
|
||||
.map(|args| {
|
||||
// strip quotes from file paths.
|
||||
// not sure how to map this and still throw the error so doing it the old fashioned way
|
||||
let mut files = Vec::with_capacity(args.len());
|
||||
for arg in args.iter() {
|
||||
let start_char = arg.chars().nth(0);
|
||||
match start_char {
|
||||
Some('"') |
|
||||
Some('\'') => {
|
||||
let separator = start_char.unwrap();
|
||||
if arg.ends_with(separator) {
|
||||
files.push(arg.split(separator).collect::<Vec<&str>>().into_iter().collect());
|
||||
} else {
|
||||
return Err(cli::ErrorKind::CommandParse(format!("Unrecognized argument {}", arg)).into());
|
||||
}
|
||||
},
|
||||
_ => files.push(arg.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
// check that we have at least one argument
|
||||
if args.len() == 0 {
|
||||
return Err(cli::ErrorKind::CommandParse("Missing required argument".to_string()).into());
|
||||
}
|
||||
Ok(Command::Read(files.clone()))
|
||||
});
|
||||
|
||||
spaces()
|
||||
.skip(token('.'))
|
||||
.with(choice::<[&mut Parser<Input = _, Output = Result<Command, cli::Error>>; 6], _>
|
||||
([&mut try(help_parser),
|
||||
&mut try(open_parser),
|
||||
&mut try(close_parser),
|
||||
&mut try(query_parser),
|
||||
&mut try(transact_parser),
|
||||
&mut try(read_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_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_read_parser_single_arg_no_quotes() {
|
||||
let input = ".read arg1";
|
||||
let cmd = command(&input).expect("Expected read command");
|
||||
match cmd {
|
||||
Command::Read(files) => assert_eq!(files, vec!["arg1"]),
|
||||
_ => assert!(false)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_read_parser_single_arg_quotes() {
|
||||
let input = r#".read "arg1""#;
|
||||
let cmd = command(&input).expect("Expected read command");
|
||||
match cmd {
|
||||
Command::Read(files) => assert_eq!(files, vec!["arg1"]),
|
||||
_ => assert!(false)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_read_parser_single_arg_file_extension() {
|
||||
let input = ".read arg1.edn";
|
||||
let cmd = command(&input).expect("Expected read command");
|
||||
match cmd {
|
||||
Command::Read(files) => assert_eq!(files, vec!["arg1.edn"]),
|
||||
_ => assert!(false)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_read_parser_single_arg_file_path() {
|
||||
let input = ".read ~/path/to/data/arg1.edn";
|
||||
let cmd = command(&input).expect("Expected read command");
|
||||
match cmd {
|
||||
Command::Read(files) => assert_eq!(files, vec!["~/path/to/data/arg1.edn"]),
|
||||
_ => assert!(false)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_read_parser_single_arg_file_path_quotes() {
|
||||
let input = r#".read "~/path/to/data/arg1.edn""#;
|
||||
let cmd = command(&input).expect("Expected read command");
|
||||
match cmd {
|
||||
Command::Read(files) => assert_eq!(files, vec!["~/path/to/data/arg1.edn"]),
|
||||
_ => assert!(false)
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn test_read_parser_single_arg_bad_quotes() {
|
||||
let input = r#".read "~/path/to/data/arg1.edn"#;
|
||||
let err = command(&input).expect_err("Expected an error");
|
||||
assert_eq!(err.to_string(), "Unrecognized argument \"~/path/to/data/arg1.edn");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_read_parser_multiple_args() {
|
||||
let input = ".read ~/path/to/data/arg1.edn ~/path/to/schema/arg2.edn";
|
||||
let cmd = command(&input).expect("Expected read command");
|
||||
match cmd {
|
||||
Command::Read(files) => assert_eq!(files, vec!["~/path/to/data/arg1.edn", "~/path/to/schema/arg2.edn"]),
|
||||
_ => assert!(false)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_read_parser_multiple_args_mix_quotes() {
|
||||
let input = r#".read "~/path/to/data/arg1.edn" '~/path/to/schema/arg2.edn'"#;
|
||||
let cmd = command(&input).expect("Expected read command");
|
||||
match cmd {
|
||||
Command::Read(files) => assert_eq!(files, vec!["~/path/to/data/arg1.edn", "~/path/to/schema/arg2.edn"]),
|
||||
_ => assert!(false)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_read_parser_no_args() {
|
||||
let input = ".read";
|
||||
let err = command(&input).expect_err("Expected an error");
|
||||
assert_eq!(err.to_string(), "Missing required argument");
|
||||
}
|
||||
|
||||
#[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));
|
||||
}
|
||||
}
|
43
tools/cli/src/mentat_cli/errors.rs
Normal file
43
tools/cli/src/mentat_cli/errors.rs
Normal file
|
@ -0,0 +1,43 @@
|
|||
// 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;
|
||||
use edn;
|
||||
|
||||
error_chain! {
|
||||
types {
|
||||
Error, ErrorKind, ResultExt, Result;
|
||||
}
|
||||
|
||||
foreign_links {
|
||||
EdnParseError(edn::ParseError);
|
||||
Rusqlite(rusqlite::Error);
|
||||
}
|
||||
|
||||
links {
|
||||
MentatError(mentat::Error, mentat::ErrorKind);
|
||||
}
|
||||
|
||||
errors {
|
||||
CommandParse(message: String) {
|
||||
description("An error occured parsing the entered command")
|
||||
display("{}", message)
|
||||
}
|
||||
|
||||
FileError(filename: String, message: String) {
|
||||
description("An error occured while reading file")
|
||||
display("Unable to open file {}: {}", filename, message)
|
||||
}
|
||||
}
|
||||
}
|
206
tools/cli/src/mentat_cli/input.rs
Normal file
206
tools/cli/src/mentat_cli/input.rs
Normal file
|
@ -0,0 +1,206 @@
|
|||
// 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 std::io::BufReader;
|
||||
use std::io::BufRead;
|
||||
use std::fs::File;
|
||||
use std::path::Path;
|
||||
|
||||
use self::InputResult::*;
|
||||
|
||||
use edn;
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
pub fn read_file(&mut self, filename: &String) -> Result<Vec<InputResult>, cli::Error> {
|
||||
let path = Path::new(&filename);
|
||||
let display = path.display();
|
||||
|
||||
let f = match File::open(&path) {
|
||||
Err(err) => bail!(cli::ErrorKind::FileError(display.to_string(), err.to_string())),
|
||||
Ok(file) => file,
|
||||
};
|
||||
|
||||
let mut buffer = String::new();
|
||||
|
||||
let file = BufReader::new(&f);
|
||||
let mut cmd_err: Option<edn::ParseError> = None;
|
||||
|
||||
let mut results: Vec<InputResult> = Vec::new();
|
||||
for line in file.lines() {
|
||||
let l = line.unwrap();
|
||||
println!("{}", l);
|
||||
buffer.push_str(&l);
|
||||
let cmd = Command::Transact(buffer.to_string());
|
||||
let (is_complete, err) = cmd.is_complete();
|
||||
if is_complete {
|
||||
results.push(InputResult::MetaCommand(cmd));
|
||||
buffer.clear();
|
||||
cmd_err = None;
|
||||
} else {
|
||||
cmd_err = err;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if let Some(err) = cmd_err {
|
||||
bail!(err);
|
||||
}
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
/// 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)
|
||||
},
|
||||
_ => {
|
||||
let res = command(&self.buffer);
|
||||
match res {
|
||||
Ok(cmd) => cmd,
|
||||
Err(err) => {
|
||||
self.buffer.clear();
|
||||
bail!(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let (is_complete, _) = cmd.is_complete();
|
||||
if is_complete {
|
||||
self.buffer.clear();
|
||||
self.in_process_cmd = None;
|
||||
Ok(InputResult::MetaCommand(cmd))
|
||||
} else {
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
}
|
||||
}
|
126
tools/cli/src/mentat_cli/lib.rs
Normal file
126
tools/cli/src/mentat_cli/lib.rs
Normal file
|
@ -0,0 +1,126 @@
|
|||
// 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.optopt("q", "query", "Execute a query on startup.", "QUERY");
|
||||
opts.optopt("r", "read", "Read in file at path and execute transact for each edn", "READ");
|
||||
opts.optopt("t", "transact", "Execute a transact on startup.", "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("-r") => {
|
||||
last_arg = None;
|
||||
Some(command_parser::Command::Read(vec![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() {
|
||||
}
|
||||
}
|
179
tools/cli/src/mentat_cli/repl.rs
Normal file
179
tools/cli/src/mentat_cli/repl.rs
Normal file
|
@ -0,0 +1,179 @@
|
|||
// 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 error_chain::ChainedError;
|
||||
|
||||
use command_parser::{
|
||||
Command,
|
||||
HELP_COMMAND,
|
||||
OPEN_COMMAND,
|
||||
LONG_QUERY_COMMAND,
|
||||
SHORT_QUERY_COMMAND,
|
||||
LONG_TRANSACT_COMMAND,
|
||||
SHORT_TRANSACT_COMMAND,
|
||||
READ_COMMAND,
|
||||
};
|
||||
use input::InputReader;
|
||||
use input::InputResult;
|
||||
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.insert(READ_COMMAND, "Read a file containing one or more complete edn transact statements. Transacts each edn in turn.");
|
||||
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();
|
||||
|
||||
let mut unexecuted_commands: Vec<InputResult> = Vec::new();
|
||||
|
||||
if let Some(cmds) = startup_commands {
|
||||
unexecuted_commands.append(&mut cmds.into_iter().map(|c| MetaCommand(c.clone())).collect());
|
||||
}
|
||||
|
||||
|
||||
loop {
|
||||
let res = if unexecuted_commands.len() > 0 { Ok(unexecuted_commands.remove(0)) } else { input.read_input() };
|
||||
|
||||
match res {
|
||||
Ok(MetaCommand(Command::Read(files))) => {
|
||||
for file in files {
|
||||
let file_res = input.read_file(&file);
|
||||
if let Ok(commands) = file_res {
|
||||
unexecuted_commands.append(&mut commands.clone());
|
||||
} else {
|
||||
println!("{:?}", file_res.err());
|
||||
}
|
||||
}
|
||||
},
|
||||
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.display()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.display())
|
||||
};
|
||||
},
|
||||
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.display())
|
||||
};
|
||||
},
|
||||
Command::Query(query) => self.execute_query(query),
|
||||
Command::Transact(transaction) => self.execute_transact(transaction),
|
||||
Command::Read(_) => (),
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn execute_query(&self, query: String) {
|
||||
let results = match self.store.query(query){
|
||||
Result::Ok(vals) => {
|
||||
vals
|
||||
},
|
||||
Result::Err(err) => return println!("{}.", err.display()),
|
||||
};
|
||||
|
||||
if results.is_empty() {
|
||||
return println!("No results found.")
|
||||
}
|
||||
if let Ok(output) = results.to_pretty() {
|
||||
println!("\n{}", output);
|
||||
}
|
||||
}
|
||||
|
||||
fn execute_transact(&mut self, transaction: String) {
|
||||
match self.store.transact(transaction) {
|
||||
Result::Ok(report) => println!("{:?}", report),
|
||||
Result::Err(err) => println!("{}.", err.display()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn it_works() {
|
||||
}
|
||||
}
|
61
tools/cli/src/mentat_cli/store.rs
Normal file
61
tools/cli/src/mentat_cli/store.rs
Normal file
|
@ -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<String>) -> Result<Store, cli::Error> {
|
||||
let db_name = database.unwrap_or("".to_string());
|
||||
|
||||
let mut handle = new_connection(&db_name)?;
|
||||
let conn = 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 = new_connection(&self.db_name)?;
|
||||
self.conn = 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)?)
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue