use std::path::PathBuf; use clap::parser::ValuesRef; use clap::{value_parser, Arg, ArgAction, ArgMatches, Command}; use log::{debug, error, info}; use rusqlite::Connection; use serde::Serialize; use simple_logger::SimpleLogger; use db::securities::list_securities; use crate::db::users::{create_user, list_users}; use crate::migration::apply_migrations; use crate::util::{CustomResult, Error}; use crate::webui::web_ui_main; mod db; mod event_processor; mod migration; mod services; mod util; mod webui; mod events; #[derive(Debug, Serialize)] struct Output { status: String, result: T, } impl Output { fn success(shit: T) -> Output { return Output { status: "success".to_string(), result: shit, }; } } trait CommandLineUserLogic { fn create_user( &mut self, conn: &mut Connection, name: &String, public_key: &String, ) -> CustomResult<()>; fn list_user(&mut self, conn: &Connection) -> CustomResult<()>; fn list_securities(&mut self, conn: &Connection) -> CustomResult<()>; fn process_events( &mut self, conn: &mut Connection, files: ValuesRef, ) -> CustomResult<()>; fn run_webui( &mut self, conn: &mut Connection, port: u16, enable_template_autoreload: bool, ) -> CustomResult<()>; } fn verbosity_arg() -> Arg { return Arg::new("verbosity") .short('v') .action(ArgAction::Count) .default_value("0"); } fn get_verbosity(m: &ArgMatches) -> log::LevelFilter { match m.get_one::("verbosity") { None => log::LevelFilter::Error, Some(1) => log::LevelFilter::Warn, Some(2) => log::LevelFilter::Info, Some(3) => log::LevelFilter::Debug, Some(4) => log::LevelFilter::Trace, _ => log::LevelFilter::Error, } } fn db_path_arg() -> Arg { return Arg::new("db-path") .long("db-path") .value_name("FILE") .value_parser(value_parser!(PathBuf)) .default_value("ledger.sqlite"); } fn get_db_path(m: &ArgMatches) -> &PathBuf { return m.get_one::("db-path").unwrap(); } fn new_user_command_line_parser() -> Command { return Command::new("new") .long_flag("new") .about("Create new user.") .arg( Arg::new("name") .long("name") .required(true) .help("name of new user.") .action(ArgAction::Set), ) .arg( Arg::new("public_key") .required(true) .long("public-key") .help("public key of users.") .action(ArgAction::Set), ); } fn ls_user_command_line_parser() -> Command { return Command::new("ls").long_flag("ls").about("List all users."); } fn process_events_command_line_parser() -> Command { Command::new("process") .about("Process events one after the other. In the order received") .arg_required_else_help(true) .arg(clap::arg!( ... "Stuff to add").value_parser(value_parser!(PathBuf))) } fn user_command_line_parser() -> Command { return Command::new("users") .about("Handy command about users.") .subcommand(new_user_command_line_parser()) .subcommand(ls_user_command_line_parser()) .arg_required_else_help(true); } fn event_command_line_parser() -> Command { return Command::new("events") .about("Process and query events files") .subcommand(process_events_command_line_parser()) .arg_required_else_help(true); } fn ls_securities_command_line_parser() -> Command { return Command::new("ls") .long_flag("ls") .about("List all securities."); } fn run_web_ui_command_line_parser() -> Command { return Command::new("run") .arg( Arg::new("port") .long("port") .value_parser(clap::value_parser!(u16).range(2000..)) .action(ArgAction::Set) .default_value("2222"), ) .arg( Arg::new("enable-template-autoreload") .long("enable-template-autoreload") .required(false) .num_args(0) .help("Reload underlying template (for development)"), ) .about("Run web user interface."); } fn securities_command_line_parser() -> Command { return Command::new("securities") .about("Handy command about securities.") .subcommand(ls_securities_command_line_parser()) .arg_required_else_help(true); } fn web_ui_command_line_parser() -> Command { return Command::new("wui") .about("Web user interface.") .subcommand(run_web_ui_command_line_parser()) .arg_required_else_help(true); } fn command_line_parser() -> Command { return Command::new("prog") .arg(verbosity_arg()) .arg(db_path_arg()) .subcommand(event_command_line_parser()) .subcommand(user_command_line_parser()) .subcommand(securities_command_line_parser()) .subcommand(web_ui_command_line_parser()) .arg_required_else_help(true); } fn process_new_user_command_line( conn: &mut Connection, m: &ArgMatches, logic: &mut dyn CommandLineUserLogic, ) -> CustomResult<()> { debug!("Processing new user command line {:?}.", m); let name = m.get_one::("name").unwrap(); let pk = m.get_one::("public_key").unwrap(); logic.create_user(conn, name, pk) } fn process_list_user_command_line( conn: &Connection, m: &ArgMatches, logic: &mut dyn CommandLineUserLogic, ) -> CustomResult<()> { debug!("Processing list users command line {:?}.", m); logic.list_user(conn) } fn process_user_command_line( conn: &mut Connection, m: &ArgMatches, logic: &mut dyn CommandLineUserLogic, ) -> CustomResult<()> { debug!("Processing user command line {:?}.", m); match m.subcommand() { Some(("new", new_user_match)) => process_new_user_command_line(conn, new_user_match, logic), Some(("ls", list_user_match)) => { process_list_user_command_line(conn, list_user_match, logic) } _ => unreachable!(), } } fn process_list_securities_command_line( conn: &Connection, m: &ArgMatches, logic: &mut dyn CommandLineUserLogic, ) -> CustomResult<()> { debug!("Processing list securities command line {:?}.", m); logic.list_securities(conn) } fn process_process_events_command_line( conn: &mut Connection, m: &ArgMatches, logic: &mut dyn CommandLineUserLogic, ) -> CustomResult<()> { debug!("Processing process event command line {:?}.", m); let mut files = m.get_many::("PATH").unwrap(); if files.any(|x| !x.exists()) { error!("Some passed files do not exists. Existing right now."); return Err(Error::UserInputError( "Some passed files do not exists. Existing right now.".to_string(), )); } logic.process_events(conn, files) } fn process_run_wui_command_line( conn: &mut Connection, m: &ArgMatches, logic: &mut dyn CommandLineUserLogic, ) -> CustomResult<()> { debug!("Processing run wui command line {:?}.", m); let port = m.get_one::("port").unwrap_or(&2222); let enable_template_autoreload = m.get_flag("enable-template-autoreload"); logic.run_webui(conn, *port, enable_template_autoreload) } fn process_securities_command_line( conn: &Connection, m: &ArgMatches, logic: &mut dyn CommandLineUserLogic, ) -> CustomResult<()> { debug!("Processing securities command line {:?}.", m); match m.subcommand() { Some(("ls", list_securities_match)) => { process_list_securities_command_line(conn, list_securities_match, logic) } _ => unreachable!(), } } fn process_events_command_line( conn: &mut Connection, m: &ArgMatches, logic: &mut dyn CommandLineUserLogic, ) -> CustomResult<()> { debug!("Processing events command line {:?}.", m); match m.subcommand() { Some(("process", process_events_match)) => { process_process_events_command_line(conn, process_events_match, logic) } _ => unreachable!(), } } fn process_wui_command_line( conn: &mut Connection, m: &ArgMatches, logic: &mut dyn CommandLineUserLogic, ) -> CustomResult<()> { debug!("Processing wui command line {:?}.", m); match m.subcommand() { Some(("run", run_wui_match)) => process_run_wui_command_line(conn, run_wui_match, logic), _ => unreachable!(), } } fn process_command_line( conn: &mut Connection, m: &ArgMatches, logic: &mut dyn CommandLineUserLogic, ) -> CustomResult<()> { debug!("Processing command line {:?}.", m); match m.subcommand() { Some(("users", user_match)) => process_user_command_line(conn, user_match, logic), Some(("securities", securities_match)) => { process_securities_command_line(conn, securities_match, logic) } Some(("events", events_match)) => process_events_command_line(conn, events_match, logic), Some(("wui", wui_match)) => process_wui_command_line(conn, wui_match, logic), _ => { println!("Received un parsable command. {:?}", m.subcommand()); unreachable!(); } } } #[cfg(test)] mod cli_test { use super::*; struct TestCommandLineLogic { list_user_count: usize, create_user_count: usize, list_securities_count: usize, process_events_count: usize, run_webui_count: usize, } impl TestCommandLineLogic { fn new() -> TestCommandLineLogic { return TestCommandLineLogic { list_user_count: 0, create_user_count: 0, list_securities_count: 0, process_events_count: 0, run_webui_count: 0, }; } } impl CommandLineUserLogic for TestCommandLineLogic { fn create_user( &mut self, _conn: &mut Connection, _name: &String, _public_key: &String, ) -> CustomResult<()> { self.create_user_count += 1; Ok(()) } fn list_user(&mut self, _conn: &Connection) -> CustomResult<()> { self.list_user_count += 1; Ok(()) } fn list_securities(&mut self, _conn: &Connection) -> CustomResult<()> { self.list_securities_count += 1; Ok(()) } fn process_events( &mut self, _conn: &mut Connection, _files: ValuesRef, ) -> CustomResult<()> { self.process_events_count += 1; Ok(()) } fn run_webui( &mut self, _conn: &mut Connection, _port: u16, _enable_template_autoreload: bool, ) -> CustomResult<()> { self.run_webui_count += 1; Ok(()) } } #[test] fn verbosity() { { let cmd = Command::new("test").arg(verbosity_arg()); let m = cmd.get_matches_from(vec!["test", "-vv"]); assert_eq!(get_verbosity(&m), log::LevelFilter::Info); } { let cmd = Command::new("test").arg(verbosity_arg()); let m = cmd.get_matches_from(vec!["test"]); assert_eq!(get_verbosity(&m), log::LevelFilter::Error); } } #[test] fn db_path() { { let cmd = Command::new("test").arg(db_path_arg()); let m = cmd.get_matches_from(vec!["test", "--db-path", "/foo/bar"]); let expt = PathBuf::from("/foo/bar"); assert_eq!(get_db_path(&m), &expt); } { let cmd = Command::new("test").arg(db_path_arg()); let m = cmd.get_matches_from(vec!["test"]); let expt = PathBuf::from("ledger.sqlite"); assert_eq!(get_db_path(&m), &expt); } } #[test] fn create_user() { let cmd = command_line_parser(); let m = cmd.get_matches_from(vec![ "test", "users", "new", "--name", "paul", "--public-key", "123abc", ]); let mut logic = TestCommandLineLogic::new(); let mut conn = Connection::open_in_memory().unwrap(); process_command_line(&mut conn, &m, &mut logic); assert_eq!(logic.create_user_count, 1); } #[test] fn list_users() { let cmd = command_line_parser(); let m = cmd.get_matches_from(vec!["test", "users", "ls"]); let mut logic = TestCommandLineLogic::new(); let mut conn = Connection::open_in_memory().unwrap(); process_command_line(&mut conn, &m, &mut logic); assert_eq!(logic.list_user_count, 1); } #[test] fn list_securities() { let cmd = command_line_parser(); let m = cmd.get_matches_from(vec!["test", "securities", "ls"]); let mut logic = TestCommandLineLogic::new(); let mut conn = Connection::open_in_memory().unwrap(); process_command_line(&mut conn, &m, &mut logic); assert_eq!(logic.list_securities_count, 1); } #[test] fn process_events() { let cmd = command_line_parser(); let m = cmd.get_matches_from(vec!["test", "events", "process", "/dev/null"]); let mut logic = TestCommandLineLogic::new(); let mut conn = Connection::open_in_memory().unwrap(); process_command_line(&mut conn, &m, &mut logic); assert_eq!(logic.process_events_count, 1); } #[test] fn run_web_ui() { let cmd = command_line_parser(); let m = cmd.get_matches_from(vec!["test", "wui", "run"]); let mut logic = TestCommandLineLogic::new(); let mut conn = Connection::open_in_memory().unwrap(); process_command_line(&mut conn, &m, &mut logic); assert_eq!(logic.run_webui_count, 1); } } struct ProCommandLineLogic {} impl ProCommandLineLogic { fn new() -> ProCommandLineLogic { return ProCommandLineLogic {}; } } impl CommandLineUserLogic for ProCommandLineLogic { fn create_user( &mut self, conn: &mut Connection, name: &String, public_key: &String, ) -> CustomResult<()> { let tx = conn.transaction().map_err(Error::SqlError)?; create_user(&tx, name, public_key)?; tx.commit().map_err(Error::SqlError) } fn list_user(&mut self, conn: &Connection) -> CustomResult<()> { let users = list_users(conn)?; let out = Output::success(users); println!("{}", serde_json::to_string(&out).unwrap()); Ok(()) } fn list_securities(&mut self, conn: &Connection) -> CustomResult<()> { let securities = list_securities(conn)?; let out = Output::success(securities); println!("{}", serde_json::to_string(&out).unwrap()); Ok(()) } fn process_events( &mut self, conn: &mut Connection, files: ValuesRef, ) -> CustomResult<()> { // process_event_files(conn, files) Ok(()) } fn run_webui( &mut self, conn: &mut Connection, port: u16, enable_template_autoreload: bool, ) -> CustomResult<()> { info!("Serving web-ui on port {:?}", port); web_ui_main(conn, port, enable_template_autoreload) } } fn get_connection(m: &ArgMatches) -> Connection { let path = get_db_path(&m); info!("Sqlite database location is {:?}.", path); return Connection::open(path).unwrap(); } fn set_logger(m: &ArgMatches) { SimpleLogger::new() .with_level(get_verbosity(&m)) .init() .unwrap(); } fn main() { let command = command_line_parser(); let m = command.get_matches(); set_logger(&m); let mut conn = get_connection(&m); apply_migrations(&mut conn).unwrap(); let mut logic = ProCommandLineLogic::new(); process_command_line(&mut conn, &m, &mut logic); }