csvsql 0.1.4

SQL like engine that works on CSV file.
Documentation
use std::io::{self, BufRead};
use std::io::{IsTerminal, Write};

use itertools::Itertools;
use reedline::{
    ColumnarMenu, DefaultCompleter, DefaultPrompt, DefaultPromptSegment, Emacs, ExampleHighlighter,
    FileBackedHistory, KeyCode, KeyModifiers, MenuBuilder, Reedline, ReedlineEvent, ReedlineMenu,
    Signal, ValidationResult, Validator, default_emacs_keybindings,
};

use crate::engine::Engine;
use crate::error::CvsSqlError;
use crate::outputer::Outputer;

struct EolValidator {}
impl Validator for EolValidator {
    fn validate(&self, line: &str) -> ValidationResult {
        if line.ends_with("\\") {
            ValidationResult::Incomplete
        } else {
            ValidationResult::Complete
        }
    }
}
pub fn work_on_console(
    engine: &Engine,
    no_console: bool,
    outputer: &mut dyn Outputer,
) -> Result<(), CvsSqlError> {
    if io::stdout().is_terminal()
        && io::stdin().is_terminal()
        && io::stderr().is_terminal()
        && !no_console
    {
        use_readline(engine, outputer)
    } else {
        stdout(engine, outputer)
    }
}
struct ReadlineRepl {
    editor: Reedline,
}
impl ReplOutputer for ReadlineRepl {
    fn get_commands(&mut self, prompt: &str) -> Result<Option<String>, CvsSqlError> {
        let left_prompt = DefaultPromptSegment::Basic(prompt.to_string());
        let right_prompt = DefaultPromptSegment::Empty;
        let prompt = DefaultPrompt::new(left_prompt, right_prompt);
        let sig = self.editor.read_line(&prompt)?;
        match sig {
            Signal::Success(command) => Ok(Some(command)),
            Signal::CtrlD | Signal::CtrlC => Ok(None),
        }
    }
}
fn use_readline(engine: &Engine, outputer: &mut dyn Outputer) -> Result<(), CvsSqlError> {
    let mut line_editor = Reedline::create();
    if let Some(config_dir) = dirs::config_dir() {
        let history = config_dir.join("csvsql").join(".history");
        if let Ok(history) = FileBackedHistory::with_file(150, history) {
            line_editor = line_editor.with_history(Box::new(history));
        }
    };
    let commands_to_complete = vec![
        "SELECT".into(),
        "UPDATE".into(),
        "DELETE".into(),
        "INSERT".into(),
        "FROM".into(),
        "WHERE".into(),
        "GROUP BY".into(),
        "HAVING".into(),
        "LIMIT".into(),
        "CREATE".into(),
        "DROP".into(),
        "TEMPORARY".into(),
    ];
    let completer = DefaultCompleter::new_with_wordlen(commands_to_complete, 2);
    let completion_menu = Box::new(ColumnarMenu::default().with_name("completion_menu"));
    let mut keybindings = default_emacs_keybindings();
    keybindings.add_binding(
        KeyModifiers::NONE,
        KeyCode::Tab,
        ReedlineEvent::UntilFound(vec![
            ReedlineEvent::Menu("completion_menu".to_string()),
            ReedlineEvent::MenuNext,
        ]),
    );

    let commands_to_highlight = vec![
        "SELECT".into(),
        "UPDATE".into(),
        "DELETE".into(),
        "INSERT".into(),
        "CREATE".into(),
        "DROP".into(),
    ];
    let highlighter = Box::new(ExampleHighlighter::new(commands_to_highlight));

    let edit_mode = Box::new(Emacs::new(keybindings));
    line_editor = line_editor
        .with_completer(Box::new(completer))
        .with_menu(ReedlineMenu::EngineCompleter(completion_menu))
        .with_edit_mode(edit_mode)
        .with_validator(Box::new(EolValidator {}))
        .with_highlighter(highlighter);
    let mut reeline = ReadlineRepl {
        editor: line_editor,
    };
    repl(engine, outputer, &mut reeline)
}

trait ReplOutputer {
    fn get_commands(&mut self, prompt: &str) -> Result<Option<String>, CvsSqlError>;
    fn print_error(&self, err: CvsSqlError) {
        eprintln!("Got error: {err}")
    }
    fn print_output(&self, output: &str) {
        println!("{output}")
    }
}
struct SimpleStdRepl {}
impl ReplOutputer for SimpleStdRepl {
    fn get_commands(&mut self, prompt: &str) -> Result<Option<String>, CvsSqlError> {
        let mut stdout = io::stdout().lock();
        print!("{prompt} > ");
        stdout.flush()?;

        let stdin = io::stdin();
        let validator = EolValidator {};
        let mut command_to_execute = vec![];
        while let Some(line) = stdin.lock().lines().next() {
            let command = line?;
            command_to_execute.push(command.to_string());
            if let ValidationResult::Complete = validator.validate(&command) {
                break;
            }
        }

        if command_to_execute.is_empty() {
            Ok(None)
        } else {
            let command = command_to_execute.iter().join("\n");
            Ok(Some(command))
        }
    }
}

fn stdout(engine: &Engine, outputer: &mut dyn Outputer) -> Result<(), CvsSqlError> {
    let mut std = SimpleStdRepl {};
    repl(engine, outputer, &mut std)
}
fn repl(
    engine: &Engine,
    outputer: &mut dyn Outputer,
    repl: &mut impl ReplOutputer,
) -> Result<(), CvsSqlError> {
    loop {
        match repl.get_commands(&engine.prompt())? {
            None => {
                return Ok(());
            }
            Some(command) => {
                let command = command.replace("\\\n", "\n");

                match engine.execute_commands(&command) {
                    Ok(results) => {
                        for results in results {
                            if let Some(out) = outputer.write(&results)? {
                                repl.print_output(&out);
                            }
                        }
                    }
                    Err(e) => repl.print_error(e),
                }
            }
        }
    }
}

#[cfg(test)]
mod tests {

    use std::cell::RefCell;

    use crate::args::Args;

    use super::*;

    struct TestRepl {
        prompts: Vec<String>,
        errors: RefCell<Vec<CvsSqlError>>,
        outputs: RefCell<Vec<String>>,
        index: usize,
        inputs: Vec<String>,
    }
    impl TestRepl {
        fn new(inputs: Vec<String>) -> Self {
            TestRepl {
                prompts: vec![],
                errors: RefCell::new(vec![]),
                outputs: RefCell::new(vec![]),
                index: 0,
                inputs,
            }
        }
    }
    #[derive(Default)]
    struct TestOutputer {
        results_count: usize,
        results: Vec<String>,
    }
    impl TestOutputer {
        fn new(results: Vec<String>) -> Self {
            Self {
                results_count: 0,
                results,
            }
        }
    }
    impl Outputer for TestOutputer {
        fn write(
            &mut self,
            _: &crate::engine::CommandExecution,
        ) -> Result<Option<String>, CvsSqlError> {
            let ret = self.results.get(self.results_count).cloned();
            self.results_count += 1;
            Ok(ret)
        }
    }

    impl ReplOutputer for TestRepl {
        fn get_commands(&mut self, prompt: &str) -> Result<Option<String>, CvsSqlError> {
            self.prompts.push(prompt.to_string());
            match self.inputs.get(self.index) {
                None => Ok(None),
                Some(str) => {
                    self.index += 1;
                    Ok(Some(str.to_string()))
                }
            }
        }
        fn print_error(&self, err: CvsSqlError) {
            self.errors.borrow_mut().push(err);
        }
        fn print_output(&self, output: &str) {
            self.outputs.borrow_mut().push(output.to_string());
        }
    }

    #[test]
    fn test_repl() -> Result<(), CvsSqlError> {
        let args = Args::default();
        let engine = Engine::try_from(&args)?;
        let mut outputer = TestOutputer::new(vec!["one".into(), "two".into()]);
        let mut test_repl = TestRepl::new(vec![
            "SELECT * FROM \\\n tests.data.artists".into(),
            "SELECT;".into(),
            "START TRANSACTION".into(),
            "ROLLBACK".into(),
        ]);

        repl(&engine, &mut outputer, &mut test_repl)?;

        assert_eq!(outputer.results_count, 3);
        assert_eq!(test_repl.errors.borrow().len(), 1);
        assert_eq!(test_repl.prompts.len(), 5);
        assert_eq!(test_repl.errors.borrow().len(), 1);
        assert_eq!(test_repl.outputs.borrow().len(), 2);

        Ok(())
    }
}