use crate::compiler::TargetValue;
use crate::compiler::TimeZone;
use crate::compiler::runtime::Runtime;
use crate::compiler::state::{RuntimeState, TypeState};
use crate::compiler::{CompileConfig, Function, VrlRuntime, compile_with_state};
use crate::diagnostic::Formatter;
use crate::owned_metadata_path;
use crate::value::Secrets;
use crate::value::Value;
use indoc::indoc;
use prettytable::{Cell, Row, Table, format};
use regex::Regex;
use rustyline::{
Context, Editor, Helper,
completion::Completer,
error::ReadlineError,
highlight::{CmdKind, Highlighter, MatchingBracketHighlighter},
hint::{Hinter, HistoryHinter},
history::MemHistory,
validate::{self, ValidationResult, Validator},
};
use std::borrow::Cow::{self, Borrowed, Owned};
use std::collections::BTreeMap;
use std::rc::Rc;
use std::sync::LazyLock;
static ERRORS: LazyLock<Vec<String>> = LazyLock::new(|| {
[
100, 101, 102, 103, 104, 105, 106, 107, 108, 110, 203, 204, 205, 206, 207, 208, 209, 300,
301, 302, 303, 304, 305, 306, 307, 308, 309, 310, 311, 312, 313, 314, 400, 401, 402, 403,
601, 620, 630, 640, 650, 651, 652, 660, 701,
]
.iter()
.map(std::string::ToString::to_string)
.collect()
});
const DOCS_URL: &str = "https://vector.dev/docs/reference/vrl";
const ERRORS_URL_ROOT: &str = "https://errors.vrl.dev";
const RESERVED_TERMS: &[&str] = &[
"next",
"prev",
"exit",
"quit",
"help",
"help functions",
"help funcs",
"help fs",
"help docs",
];
pub(crate) struct Repl {
quiet: bool,
objects: Vec<TargetValue>,
timezone: TimeZone,
vrl_runtime: VrlRuntime,
stdlib_functions: Vec<Box<dyn Function>>,
}
impl Repl {
pub(crate) fn new(
quiet: bool,
objects: Vec<TargetValue>,
timezone: TimeZone,
vrl_runtime: VrlRuntime,
stdlib_functions: Vec<Box<dyn Function>>,
) -> Self {
Self {
quiet,
objects,
timezone,
vrl_runtime,
stdlib_functions,
}
}
pub(crate) fn run(self) -> Result<(), ReadlineError> {
let Self {
quiet,
mut objects,
timezone,
vrl_runtime,
stdlib_functions,
} = self;
let stdlib_functions = Rc::new(stdlib_functions);
let mut index = 0;
let func_docs_regex = Regex::new(r"^help\sdocs\s(\w{1,})$").unwrap();
let error_docs_regex = Regex::new(r"^help\serror\s(\w{1,})$").unwrap();
let mut state = TypeState::default();
let mut rt = Runtime::new(RuntimeState::default());
let mut rl = Editor::<ReplHelper, MemHistory>::new()?;
rl.set_helper(Some(ReplHelper::new(stdlib_functions.clone())));
#[allow(clippy::print_stdout)]
if !quiet {
println!("{BANNER_TEXT}");
}
loop {
let readline = rl.readline("$ ");
match readline.as_deref() {
Ok(line) if line == "exit" || line == "quit" => break,
Ok("help") => Self::print_help_text(),
Ok(line)
if line == "help functions" || line == "help funcs" || line == "help fs" =>
{
Self::print_function_list(&stdlib_functions);
}
Ok("help docs") => Self::open_url(DOCS_URL),
Ok(line) if error_docs_regex.is_match(line) => {
Self::show_error_docs(line, &error_docs_regex);
}
Ok(line) if func_docs_regex.is_match(line) => {
Self::show_func_docs(line, &func_docs_regex, &stdlib_functions);
}
Ok(line) => {
rl.add_history_entry(line)?;
let command = match line {
"next" => {
if index < objects.len()
&& objects.last().map(|x| &x.value) != Some(&Value::Null)
{
index = index.saturating_add(1);
}
if index == objects.len() {
objects.push(TargetValue {
value: Value::Null,
metadata: Value::Object(BTreeMap::new()),
secrets: Secrets::new(),
});
}
"."
}
"prev" => {
index = index.saturating_sub(1);
if objects.last().map(|x| &x.value) == Some(&Value::Null) {
let _last = objects.pop();
}
"."
}
"" => continue,
_ => line,
};
let result = Self::resolve(
objects.get_mut(index).expect("object should exist"),
&mut rt,
command,
&mut state,
timezone,
vrl_runtime,
&stdlib_functions,
);
let string = match result {
Ok(v) => v.to_string(),
Err(v) => v.clone(),
};
#[allow(clippy::print_stdout)]
{
println!("{string}\n");
}
}
Err(ReadlineError::Interrupted | ReadlineError::Eof) => break,
Err(err) => {
#[allow(clippy::print_stdout)]
{
println!("unable to read line: {err}");
}
break;
}
}
}
Ok(())
}
fn resolve(
target: &mut TargetValue,
runtime: &mut Runtime,
program: &str,
state: &mut TypeState,
timezone: TimeZone,
vrl_runtime: VrlRuntime,
stdlib_functions: &[Box<dyn Function>],
) -> Result<Value, String> {
let mut config = CompileConfig::default();
config.set_read_only_path(owned_metadata_path!("vector"), true);
config.disable_unused_expression_check();
let program = match compile_with_state(program, stdlib_functions, state, config) {
Ok(result) => result.program,
Err(diagnostics) => {
return Err(Formatter::new(program, diagnostics).colored().to_string());
}
};
*state = program.final_type_info().state;
match vrl_runtime {
VrlRuntime::Ast => runtime
.resolve(target, &program, &timezone)
.map_err(|err| err.to_string()),
}
}
fn print_function_list(funcs: &[Box<dyn Function>]) {
let table_format = *format::consts::FORMAT_NO_LINESEP_WITH_TITLE;
let num_columns = 3;
let mut func_table = Table::new();
func_table.set_format(table_format);
funcs
.chunks(num_columns)
.map(|funcs| {
let mut ids: Vec<Cell> = Vec::new();
for n in 0..num_columns {
if let Some(v) = funcs.get(n) {
ids.push(Cell::new(v.identifier()));
}
}
func_table.add_row(Row::new(ids));
})
.for_each(drop);
func_table.printstd();
}
fn print_help_text() {
#[allow(clippy::print_stdout)]
{
println!("{HELP_TEXT}");
}
}
fn open_url(url: &str) {
if let Err(err) = webbrowser::open(url) {
#[allow(clippy::print_stdout)]
{
println!(
"couldn't open default web browser: {err}\n\
you can access the desired documentation at {url}"
);
}
}
}
fn show_func_docs(line: &str, pattern: &Regex, funcs: &[Box<dyn Function>]) {
let matches = pattern.captures(line).unwrap();
let func_name = matches.get(1).unwrap().as_str();
if funcs.iter().any(|f| f.identifier() == func_name) {
let func_url = format!("{DOCS_URL}/functions/#{func_name}");
Self::open_url(&func_url);
} else {
#[allow(clippy::print_stdout)]
{
println!("function name {func_name} not recognized");
}
}
}
fn show_error_docs(line: &str, pattern: &Regex) {
let matches = pattern.captures(line).unwrap();
let error_code = matches.get(1).unwrap().as_str();
if ERRORS.iter().any(|e| e == error_code) {
let error_code_url = format!("{ERRORS_URL_ROOT}/{error_code}");
Self::open_url(&error_code_url);
} else {
#[allow(clippy::print_stdout)]
{
println!("error code {error_code} not recognized");
}
}
}
}
struct ReplHelper {
highlighter: MatchingBracketHighlighter,
history_hinter: HistoryHinter,
colored_prompt: String,
hints: Vec<&'static str>,
stdlib_functions: Rc<Vec<Box<dyn Function>>>,
}
impl ReplHelper {
fn new(stdlib_functions: Rc<Vec<Box<dyn Function>>>) -> Self {
let hints = stdlib_functions
.iter()
.map(|f| f.identifier())
.chain(RESERVED_TERMS.iter().copied())
.collect();
Self {
highlighter: MatchingBracketHighlighter::new(),
history_hinter: HistoryHinter {},
colored_prompt: "$ ".to_owned(),
hints,
stdlib_functions,
}
}
}
impl Helper for ReplHelper {}
impl Completer for ReplHelper {
type Candidate = String;
}
impl Hinter for ReplHelper {
type Hint = String;
fn hint(&self, line: &str, pos: usize, ctx: &Context<'_>) -> Option<String> {
if pos < line.len() {
return None;
}
if let Some(hist) = self.history_hinter.hint(line, pos, ctx) {
return Some(hist);
}
self.hints.iter().find_map(|hint| {
if pos > 0 && hint.starts_with(&line[..pos]) {
Some(String::from(&hint[pos..]))
} else {
None
}
})
}
}
impl Highlighter for ReplHelper {
fn highlight_prompt<'b, 's: 'b, 'p: 'b>(
&'s self,
prompt: &'p str,
default: bool,
) -> Cow<'b, str> {
if default {
Borrowed(&self.colored_prompt)
} else {
Borrowed(prompt)
}
}
fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> {
Owned("\x1b[1m".to_owned() + hint + "\x1b[m")
}
fn highlight<'l>(&self, line: &'l str, pos: usize) -> Cow<'l, str> {
self.highlighter.highlight(line, pos)
}
fn highlight_char(&self, line: &str, pos: usize, forced: CmdKind) -> bool {
self.highlighter.highlight_char(line, pos, forced)
}
}
impl Validator for ReplHelper {
fn validate(
&self,
ctx: &mut validate::ValidationContext,
) -> rustyline::Result<ValidationResult> {
Ok(validate_input(ctx.input(), &self.stdlib_functions))
}
fn validate_while_typing(&self) -> bool {
false
}
}
fn validate_input(input: &str, stdlib_functions: &[Box<dyn Function>]) -> ValidationResult {
let state = TypeState::default();
let mut config = CompileConfig::default();
config.disable_unused_expression_check();
match compile_with_state(input, stdlib_functions, &state, config) {
Err(diagnostics) => {
let error = Formatter::new(input, diagnostics).to_string();
if error.contains("syntax error") && error.contains("unexpected end of program") {
ValidationResult::Incomplete
} else {
ValidationResult::Valid(None)
}
}
Ok(..) => ValidationResult::Valid(None),
}
}
const HELP_TEXT: &str = indoc! {r#"
VRL REPL commands:
help functions Display a list of currently available VRL functions (aliases: ["help funcs", "help fs"])
help docs Navigate to the VRL docs on the Vector website
help docs <func> Navigate to the VRL docs for the specified function
help error <code> Navigate to the docs for a specific error code
next Load the next object or create a new one
prev Load the previous object
exit Terminate the program
"#};
const BANNER_TEXT: &str = indoc! {"
> VVVVVVVV VVVVVVVVRRRRRRRRRRRRRRRRR LLLLLLLLLLL
> V::::::V V::::::VR::::::::::::::::R L:::::::::L
> V::::::V V::::::VR::::::RRRRRR:::::R L:::::::::L
> V::::::V V::::::VRR:::::R R:::::RLL:::::::LL
> V:::::V V:::::V R::::R R:::::R L:::::L
> V:::::V V:::::V R::::R R:::::R L:::::L
> V:::::V V:::::V R::::RRRRRR:::::R L:::::L
> V:::::V V:::::V R:::::::::::::RR L:::::L
> V:::::V V:::::V R::::RRRRRR:::::R L:::::L
> V:::::V V:::::V R::::R R:::::R L:::::L
> V:::::V:::::V R::::R R:::::R L:::::L
> V:::::::::V R::::R R:::::R L:::::L LLLLLL
> V:::::::V RR:::::R R:::::RLL:::::::LLLLLLLLL:::::L
> V:::::V R::::::R R:::::RL::::::::::::::::::::::L
> V:::V R::::::R R:::::RL::::::::::::::::::::::L
> VVV RRRRRRRR RRRRRRRLLLLLLLLLLLLLLLLLLLLLLLL
>
> VECTOR REMAP LANGUAGE
>
>
> Welcome!
>
> The CLI is running in REPL (Read-eval-print loop) mode.
>
> To run the CLI in regular mode, add a program to your command.
>
> VRL REPL commands:
> help Learn more about VRL
> next Load the next object or create a new one
> prev Load the previous object
> exit Terminate the program
>
> Any other value is resolved to a VRL expression.
>
> Try it out now by typing `.` and hitting [enter] to see the result.
"};
#[cfg(test)]
mod tests {
use std::sync::atomic::{AtomicU32, Ordering};
use super::*;
fn is_valid(result: &ValidationResult) -> bool {
matches!(result, ValidationResult::Valid(_))
}
fn is_incomplete(result: &ValidationResult) -> bool {
matches!(result, ValidationResult::Incomplete)
}
#[test]
fn test_validate_complete_expression() {
let result = validate_input(". = 42", &crate::stdlib::all());
assert!(is_valid(&result));
}
#[test]
fn test_validate_incomplete_expression() {
let result = validate_input("if true {", &crate::stdlib::all());
assert!(is_incomplete(&result));
}
#[test]
fn test_validate_type_error_is_valid() {
let result = validate_input(r#"1 + "string""#, &crate::stdlib::all());
assert!(is_valid(&result));
}
#[test]
fn function_is_not_run_during_validation() {
use crate::compiler::prelude::*;
static RUN_COUNTER: AtomicU32 = AtomicU32::new(0u32);
#[derive(Clone, Copy, Debug)]
pub struct Once;
impl Function for Once {
fn identifier(&self) -> &'static str {
"once"
}
fn usage(&self) -> &'static str {
""
}
fn category(&self) -> &'static str {
Category::Number.as_ref()
}
fn return_kind(&self) -> u16 {
kind::NULL
}
fn parameters(&self) -> &'static [Parameter] {
&[]
}
fn compile(
&self,
_state: &state::TypeState,
_ctx: &mut FunctionCompileContext,
_arguments: ArgumentList,
) -> Compiled {
Ok(OnceFn {}.as_expr())
}
fn examples(&self) -> &'static [Example] {
&[]
}
}
#[derive(Clone, Debug)]
struct OnceFn;
impl FunctionExpression for OnceFn {
fn resolve(&self, _ctx: &mut Context) -> Resolved {
RUN_COUNTER.fetch_add(1, Ordering::SeqCst);
Ok(Value::Null)
}
fn type_def(&self, _state: &state::TypeState) -> TypeDef {
Kind::null().into()
}
}
let result = validate_input("once()", &[Box::from(Once)]);
assert!(is_valid(&result));
assert_eq!(RUN_COUNTER.load(Ordering::SeqCst), 0);
}
}