feat(cli): add validate command (#2241)

This commit is contained in:
David Bottiau 2023-07-10 10:36:35 +02:00 committed by GitHub
parent 602ffda466
commit 676327f781
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 262 additions and 9 deletions

125
Cargo.lock generated
View file

@ -240,6 +240,15 @@ dependencies = [
"version_check", "version_check",
] ]
[[package]]
name = "aho-corasick"
version = "0.7.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "aho-corasick" name = "aho-corasick"
version = "1.0.2" version = "1.0.2"
@ -378,6 +387,21 @@ version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711"
[[package]]
name = "assert_fs"
version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f070617a68e5c2ed5d06ee8dd620ee18fb72b99f6c094bed34cf8ab07c875b48"
dependencies = [
"anstyle",
"doc-comment",
"globwalk",
"predicates",
"predicates-core",
"predicates-tree",
"tempfile",
]
[[package]] [[package]]
name = "async-channel" name = "async-channel"
version = "1.8.0" version = "1.8.0"
@ -829,6 +853,16 @@ dependencies = [
"alloc-stdlib", "alloc-stdlib",
] ]
[[package]]
name = "bstr"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6798148dccfbff0fae41c7574d2fa8f1ef3492fba0face179de5d8d447d67b05"
dependencies = [
"memchr",
"serde",
]
[[package]] [[package]]
name = "bumpalo" name = "bumpalo"
version = "3.13.0" version = "3.13.0"
@ -1362,6 +1396,12 @@ version = "1.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c1bba4f227a4a53d12b653f50ca7bf10c9119ae2aba56aff9e0338b5c98f36a" checksum = "8c1bba4f227a4a53d12b653f50ca7bf10c9119ae2aba56aff9e0338b5c98f36a"
[[package]]
name = "difflib"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8"
[[package]] [[package]]
name = "digest" name = "digest"
version = "0.10.7" version = "0.10.7"
@ -1403,6 +1443,12 @@ dependencies = [
"urlencoding", "urlencoding",
] ]
[[package]]
name = "doc-comment"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
[[package]] [[package]]
name = "earcutr" name = "earcutr"
version = "0.4.2" version = "0.4.2"
@ -1900,6 +1946,30 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
[[package]]
name = "globset"
version = "0.4.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "029d74589adefde59de1a0c4f4732695c32805624aec7b68d91503d4dba79afc"
dependencies = [
"aho-corasick 0.7.20",
"bstr",
"fnv",
"log",
"regex",
]
[[package]]
name = "globwalk"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc"
dependencies = [
"bitflags 1.3.2",
"ignore",
"walkdir",
]
[[package]] [[package]]
name = "grpcio" name = "grpcio"
version = "0.8.3" version = "0.8.3"
@ -2227,6 +2297,23 @@ dependencies = [
"unicode-normalization", "unicode-normalization",
] ]
[[package]]
name = "ignore"
version = "0.4.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbe7873dab538a9a44ad79ede1faf5f30d49f9a5c883ddbab48bce81b64b7492"
dependencies = [
"globset",
"lazy_static",
"log",
"memchr",
"regex",
"same-file",
"thread_local",
"walkdir",
"winapi-util",
]
[[package]] [[package]]
name = "imbl" name = "imbl"
version = "2.0.0" version = "2.0.0"
@ -3211,6 +3298,34 @@ version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
[[package]]
name = "predicates"
version = "3.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09963355b9f467184c04017ced4a2ba2d75cbcb4e7462690d388233253d4b1a9"
dependencies = [
"anstyle",
"difflib",
"itertools 0.10.5",
"predicates-core",
]
[[package]]
name = "predicates-core"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b794032607612e7abeb4db69adb4e33590fa6cf1149e95fd7cb00e634b92f174"
[[package]]
name = "predicates-tree"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "368ba315fb8c5052ab692e68a0eefec6ec57b23a36959c14496f0b0df2c0cecf"
dependencies = [
"predicates-core",
"termtree",
]
[[package]] [[package]]
name = "prettyplease" name = "prettyplease"
version = "0.1.25" version = "0.1.25"
@ -3650,7 +3765,7 @@ version = "1.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f" checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick 1.0.2",
"memchr", "memchr",
"regex-syntax 0.7.2", "regex-syntax 0.7.2",
] ]
@ -4477,10 +4592,12 @@ name = "surreal"
version = "1.0.0-beta.9" version = "1.0.0-beta.9"
dependencies = [ dependencies = [
"argon2", "argon2",
"assert_fs",
"base64 0.21.2", "base64 0.21.2",
"bytes", "bytes",
"clap 4.3.10", "clap 4.3.10",
"futures 0.3.28", "futures 0.3.28",
"glob",
"http", "http",
"hyper", "hyper",
"ipnet", "ipnet",
@ -4797,6 +4914,12 @@ dependencies = [
"windows-sys 0.48.0", "windows-sys 0.48.0",
] ]
[[package]]
name = "termtree"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76"
[[package]] [[package]]
name = "test-log" name = "test-log"
version = "0.2.12" version = "0.2.12"

View file

@ -38,6 +38,7 @@ base64 = "0.21.2"
bytes = "1.4.0" bytes = "1.4.0"
clap = { version = "4.3.10", features = ["env", "derive", "wrap_help", "unicode"] } clap = { version = "4.3.10", features = ["env", "derive", "wrap_help", "unicode"] }
futures = "0.3.28" futures = "0.3.28"
glob = "0.3.1"
http = "0.2.9" http = "0.2.9"
hyper = "0.14.27" hyper = "0.14.27"
ipnet = "2.8.0" ipnet = "2.8.0"
@ -49,30 +50,31 @@ reqwest = { version = "0.11.18", features = ["blocking"] }
rustyline = { version = "11.0.0", features = ["derive"] } rustyline = { version = "11.0.0", features = ["derive"] }
serde = { version = "1.0.164", features = ["derive"] } serde = { version = "1.0.164", features = ["derive"] }
serde_cbor = "0.11.2" serde_cbor = "0.11.2"
serde_pack = { version = "1.1.1", package = "rmp-serde" }
serde_json = "1.0.99" serde_json = "1.0.99"
serde_pack = { version = "1.1.1", package = "rmp-serde" }
surrealdb = { path = "lib", features = ["protocol-http", "protocol-ws", "rustls"] } surrealdb = { path = "lib", features = ["protocol-http", "protocol-ws", "rustls"] }
tempfile = "3.6.0" tempfile = "3.6.0"
thiserror = "1.0.40" thiserror = "1.0.40"
tokio = { version = "1.29.1", features = ["macros", "signal"] } tokio = { version = "1.29.1", features = ["macros", "signal"] }
tokio-util = { version = "0.7.8", features = ["io"] } tokio-util = { version = "0.7.8", features = ["io"] }
uuid = { version = "1.4.0", features = ["serde", "js", "v4", "v7"] }
tracing = "0.1" tracing = "0.1"
tracing-opentelemetry = "0.18.0" tracing-opentelemetry = "0.18.0"
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
urlencoding = "2.1.2" urlencoding = "2.1.2"
uuid = { version = "1.4.0", features = ["serde", "js", "v4", "v7"] }
warp = { version = "0.3.5", features = ["compression", "tls", "websocket"] } warp = { version = "0.3.5", features = ["compression", "tls", "websocket"] }
[target.'cfg(unix)'.dependencies] [target.'cfg(unix)'.dependencies]
nix = "0.26.2" nix = "0.26.2"
[dev-dependencies] [dev-dependencies]
rcgen = "0.10.0" assert_fs = "1.0.13"
tonic = "0.8.3"
opentelemetry-proto = { version = "0.1.0", features = ["gen-tonic", "traces", "build-server"] } opentelemetry-proto = { version = "0.1.0", features = ["gen-tonic", "traces", "build-server"] }
rcgen = "0.10.0"
serial_test = "2.0.0" serial_test = "2.0.0"
tokio-stream = { version = "0.1", features = ["net"] }
temp-env = "0.3.4" temp-env = "0.3.4"
tokio-stream = { version = "0.1", features = ["net"] }
tonic = "0.8.3"
[package.metadata.deb] [package.metadata.deb]
maintainer-scripts = "pkg/deb/" maintainer-scripts = "pkg/deb/"

View file

@ -8,10 +8,10 @@ mod sql;
#[cfg(feature = "has-storage")] #[cfg(feature = "has-storage")]
mod start; mod start;
mod upgrade; mod upgrade;
mod validate;
pub(crate) mod validator; pub(crate) mod validator;
mod version; mod version;
use self::upgrade::UpgradeCommandArguments;
use crate::cnf::LOGO; use crate::cnf::LOGO;
use backup::BackupCommandArguments; use backup::BackupCommandArguments;
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
@ -24,6 +24,8 @@ use sql::SqlCommandArguments;
#[cfg(feature = "has-storage")] #[cfg(feature = "has-storage")]
use start::StartCommandArguments; use start::StartCommandArguments;
use std::process::ExitCode; use std::process::ExitCode;
use upgrade::UpgradeCommandArguments;
use validate::ValidateCommandArguments;
use version::VersionCommandArguments; use version::VersionCommandArguments;
const INFO: &str = " const INFO: &str = "
@ -71,6 +73,8 @@ enum Commands {
visible_alias = "isready" visible_alias = "isready"
)] )]
IsReady(IsReadyCommandArguments), IsReady(IsReadyCommandArguments),
#[command(about = "Validate SurrealQL query files")]
Validate(ValidateCommandArguments),
} }
pub async fn init() -> ExitCode { pub async fn init() -> ExitCode {
@ -85,6 +89,7 @@ pub async fn init() -> ExitCode {
Commands::Upgrade(args) => upgrade::init(args).await, Commands::Upgrade(args) => upgrade::init(args).await,
Commands::Sql(args) => sql::init(args).await, Commands::Sql(args) => sql::init(args).await,
Commands::IsReady(args) => isready::init(args).await, Commands::IsReady(args) => isready::init(args).await,
Commands::Validate(args) => validate::init(args).await,
}; };
if let Err(e) = output { if let Err(e) = output {
error!("{}", e); error!("{}", e);

62
src/cli/validate.rs Normal file
View file

@ -0,0 +1,62 @@
use crate::err::Error;
use clap::Args;
use glob::glob;
use std::io::{Error as IoError, ErrorKind};
use surrealdb::sql::parse;
#[derive(Args, Debug)]
pub struct ValidateCommandArguments {
#[arg(help = "Glob pattern for the files to validate")]
#[arg(default_value = "**/*.surql")]
pattern: String,
}
pub async fn init(args: ValidateCommandArguments) -> Result<(), Error> {
let ValidateCommandArguments {
pattern,
} = args;
let entries = match glob(&pattern) {
Ok(entries) => entries,
Err(error) => {
eprintln!("Error parsing glob pattern {pattern}: {error}");
return Err(Error::Io(IoError::new(
ErrorKind::Other,
format!("Error parsing glob pattern {pattern}: {error}"),
)));
}
};
let mut has_entries = false;
for entry in entries.flatten() {
let file_content = tokio::fs::read_to_string(entry.clone()).await?;
let parse_result = parse(&file_content);
match parse_result {
Ok(_) => {
println!("{}: OK", entry.display());
}
Err(error) => {
println!("{}: KO", entry.display());
eprintln!("{error}");
return Err(crate::err::Error::from(error));
}
}
has_entries = true;
}
if !has_entries {
eprintln!("No files found for pattern {pattern}");
return Err(Error::Io(IoError::new(
ErrorKind::NotFound,
format!("No files found for pattern {pattern}"),
)));
}
Ok(())
}

View file

@ -1,6 +1,7 @@
mod cli_integration { mod cli_integration {
// cargo test --package surreal --bin surreal --no-default-features --features storage-mem --test cli -- cli_integration --nocapture // cargo test --package surreal --bin surreal --no-default-features --features storage-mem --test cli -- cli_integration --nocapture
use assert_fs::prelude::{FileTouch, FileWriteStr, PathChild};
use rand::{thread_rng, Rng}; use rand::{thread_rng, Rng};
use serial_test::serial; use serial_test::serial;
use std::fs; use std::fs;
@ -50,8 +51,7 @@ mod cli_integration {
} }
} }
/// Run the CLI with the given args fn run_internal<P: AsRef<Path>>(args: &str, current_dir: Option<P>) -> Child {
fn run(args: &str) -> Child {
let mut path = std::env::current_exe().unwrap(); let mut path = std::env::current_exe().unwrap();
assert!(path.pop()); assert!(path.pop());
if path.ends_with("deps") { if path.ends_with("deps") {
@ -62,6 +62,9 @@ mod cli_integration {
path.push(format!("{}{}", env!("CARGO_PKG_NAME"), std::env::consts::EXE_SUFFIX)); path.push(format!("{}{}", env!("CARGO_PKG_NAME"), std::env::consts::EXE_SUFFIX));
let mut cmd = Command::new(path); let mut cmd = Command::new(path);
if let Some(dir) = current_dir {
cmd.current_dir(&dir);
}
cmd.stdin(Stdio::piped()); cmd.stdin(Stdio::piped());
cmd.stdout(Stdio::piped()); cmd.stdout(Stdio::piped());
cmd.stderr(Stdio::piped()); cmd.stderr(Stdio::piped());
@ -71,6 +74,16 @@ mod cli_integration {
} }
} }
/// Run the CLI with the given args
fn run(args: &str) -> Child {
run_internal::<String>(args, None)
}
/// Run the CLI with the given args inside a temporary directory
fn run_in_dir<P: AsRef<Path>>(args: &str, current_dir: P) -> Child {
run_internal(args, Some(current_dir))
}
fn tmp_file(name: &str) -> String { fn tmp_file(name: &str) -> String {
let path = Path::new(env!("OUT_DIR")).join(name); let path = Path::new(env!("OUT_DIR")).join(name);
path.to_string_lossy().into_owned() path.to_string_lossy().into_owned()
@ -290,4 +303,52 @@ mod cli_integration {
let output = server.kill().output().unwrap_err(); let output = server.kill().output().unwrap_err();
assert!(output.contains("Started web server"), "couldn't start web server: {output}"); assert!(output.contains("Started web server"), "couldn't start web server: {output}");
} }
#[test]
#[serial]
fn validate_found_no_files() {
let temp_dir = assert_fs::TempDir::new().unwrap();
temp_dir.child("file.txt").touch().unwrap();
assert!(run_in_dir("validate", &temp_dir).output().is_err());
}
#[test]
#[serial]
fn validate_succeed_for_valid_surql_files() {
let temp_dir = assert_fs::TempDir::new().unwrap();
let statement_file = temp_dir.child("statement.surql");
statement_file.touch().unwrap();
statement_file.write_str("CREATE thing:success;").unwrap();
assert!(run_in_dir("validate", &temp_dir).output().is_ok());
}
#[test]
#[serial]
fn validate_failed_due_to_invalid_glob_pattern() {
let temp_dir = assert_fs::TempDir::new().unwrap();
const WRONG_GLOB_PATTERN: &str = "**/*{.txt";
let args = format!("validate \"{}\"", WRONG_GLOB_PATTERN);
assert!(run_in_dir(&args, &temp_dir).output().is_err());
}
#[test]
#[serial]
fn validate_failed_due_to_invalid_surql_files_syntax() {
let temp_dir = assert_fs::TempDir::new().unwrap();
let statement_file = temp_dir.child("statement.surql");
statement_file.touch().unwrap();
statement_file.write_str("CREATE $thing WHERE value = '';").unwrap();
assert!(run_in_dir("validate", &temp_dir).output().is_err());
}
} }