diff --git a/rust/.gitignore b/rust/.gitignore index a9d37c5..28c01ff 100644 --- a/rust/.gitignore +++ b/rust/.gitignore @@ -1,2 +1,4 @@ target Cargo.lock +test* +log.db* diff --git a/rust/Cargo.toml b/rust/Cargo.toml index f380c67..667e205 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -4,7 +4,12 @@ version = "0.1.0" edition = "2021" [dependencies] +anyhow = "1.0" +chrono = "0.4.41" clap = { version = "4.5.0", features = ["derive"] } -dirs = "5.0.1" ctrlc = "3.4" +dirs = "5.0.1" +sqlx = { version = "0.8", features = [ "runtime-tokio", "sqlite", "chrono" ] } +thiserror = "2.0.12" +tokio = { version = "1.42.0", features = ["full", "rt"] } diff --git a/rust/src/log_handler.rs b/rust/src/log_handler.rs new file mode 100644 index 0000000..8f4cf2c --- /dev/null +++ b/rust/src/log_handler.rs @@ -0,0 +1,155 @@ +use chrono::{DateTime, Local}; +use sqlx::{ + migrate::MigrateDatabase, + query, + query_as, + Row, + sqlite::{ + SqlitePool, + SqlitePoolOptions, + }, + Sqlite, +}; + +const URL_DATABASE: &str = "log.db"; +const TABLE_TESTS: &str = "tests"; +const TABLE_LOGS: &str = "logs"; +const TEST_NONE: &str = "none"; + +#[derive(Debug, Clone, thiserror::Error)] +pub enum Error { + #[error("Error in crate `sqlx`: {0}")] + Internal(String), +} + +#[derive(Clone, Debug, sqlx::FromRow)] +pub struct LogRecord { + log_id: i32, + /// timestamp of the logs creation + log_time: DateTime, + /// logs affiliated test + test_name: String, + /// log message + log_message: String, +} + +#[derive(Clone, Debug, sqlx::FromRow)] +pub struct TestRecord { + /// name of test + test_name: String, + /// time test is scheduled to start + test_time: DateTime, +} + +#[derive(Clone, Debug)] +pub struct Logger { + database: SqlitePool, +} + +impl Logger { + pub async fn setup() -> Result { + match Sqlite::database_exists(URL_DATABASE).await { + Ok(true) => {} + Ok(false) => { + println!("created `{}`", URL_DATABASE); + _ = Sqlite::create_database(URL_DATABASE).await; + } + Err(error) => { + return Err(Error::Internal(error.to_string())); + } + } + let db = match SqlitePool::connect(URL_DATABASE).await { + Ok(value) => value, + Err(error) => return Err(Error::Internal(error.to_string())), + }; + match query(&format!( + "CREATE TABLE IF NOT EXISTS {TABLE_TESTS} ( + test_name VARCHAR(255) NOT NULL UNIQUE, + test_time INTEGER DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (test_name) + )" + )).execute(&db).await { + Ok(_) => {} + Err(error) => { return Err(Error::Internal(error.to_string())); } + }; + match query(&format!( + "CREATE TABLE IF NOT EXISTS {TABLE_LOGS} ( + log_id INTEGER NOT NULL UNIQUE, + log_time INTEGER DEFAULT CURRENT_TIMESTAMP, + test_name VARCHAR(255) NOT NULL, + log_message VARCHAR(2047), + PRIMARY KEY (log_id), + FOREIGN KEY (test_name) REFERENCES {TABLE_TESTS}(test_name) + );" + )).execute(&db).await { + Ok(_) => {} + Err(error) => { return Err(Error::Internal(error.to_string())); } + }; + match query(&format!( + "INSERT OR IGNORE INTO {TABLE_TESTS} (test_name) VALUES('{TEST_NONE}');" + )).execute(&db).await { + Ok(_) => {} + Err(error) => { return Err(Error::Internal(error.to_string())); } + }; + + Ok(Self{database: db}) + } + + pub async fn query(&self, _query_: &str) -> Result<(), Error> { + match query(_query_).execute(&self.database).await { + Err(error) => return Err(Error::Internal(error.to_string())), + _ => Ok(()), + } + } + + pub async fn query_for_test(&self, _query_: &str) -> Result, Error> { + match query_as(_query_).fetch_all(&self.database).await { + Ok(records) => Ok(records), + Err(error) => return Err(Error::Internal(error.to_string())), + } + } + + pub async fn query_for_log(&self, query: &str) -> Result, Error> { + match query_as(query).fetch_all(&self.database).await { + Ok(records) => Ok(records), + Err(error) => return Err(Error::Internal(error.to_string())), + } + } + + pub async fn get_test(&self, name: Option<&str>) -> Result, Error> { + match name { + Some(value) => { + self.query_for_test(&format!("SELECT * FROM {TABLE_TESTS} WHERE test_name = '{value}';")).await + } + None => { + self.query_for_test(&format!("SELECT * FROM {TABLE_TESTS};")).await + } + } + } + + // TODO: figure out time datatype + pub async fn add_test(&self, name: &str, time: &str) -> Result<(), Error> { + self.query(&format!("INSERT INTO {TABLE_TESTS} (test_name, test_time) VALUES('{name}', '{time}');")).await + } + + pub async fn remove_test(&self, name: &str) -> Result<(), Error> { + self.query(&format!("DELETE FROM {TABLE_TESTS} WHERE test_name = '{name}';")).await + } + + pub async fn get_logs_by_testname(&self, name: String) -> Result, Error> { + self.query_for_log(&format!("SELECT * FROM {TABLE_LOGS} WHERE test_name = '{name}';")).await + } + + pub async fn get_logs_by_id(&self, start: usize, stop: Option) -> Result, Error> { + let end = match stop { + Some(value) => value, + None => start, + }; + self.query_for_log(&format!("SELECT * FROM {TABLE_LOGS} WHERE log_id BETWEEN {start} AND {end};")).await + } + + pub async fn add_log(&self, test: &str, message: &str) -> Result<(), Error> { + println!("parameters: {test}, {message}"); + self.query(&format!("INSERT INTO {TABLE_LOGS} (test_name, log_message) VALUES('{test}', '{message}');")).await + } +} diff --git a/rust/src/main.rs b/rust/src/main.rs index 0a6be73..1bffef6 100644 --- a/rust/src/main.rs +++ b/rust/src/main.rs @@ -1,70 +1,102 @@ #![allow(dead_code, unused)] -use std::{ - io::{stdin, stdout, Error, Result, Write}, - sync::{ - atomic::{AtomicBool, Ordering}, Arc - }, +mod log_handler; +mod log_structures; + +use chrono::{ + DateTime, Local, +}; +use clap::{Args, Parser, Subcommand}; +use sqlx::{ + database, migrate::MigrateDatabase, query, query_as, sqlite::{ + SqlitePool, + SqlitePoolOptions, + }, Row, Sqlite }; -fn main() -> Result<()> { - let running = Arc::new(AtomicBool::new(true)); - let signal_running = running.clone(); - let mut input: String = String::new(); - let mut length: usize; - let mut tokens: Vec<&str>; +use log_handler::Logger; - ctrlc::set_handler(move || { - signal_running.swap(false, Ordering::SeqCst); - }); +const DATABASE_URL: &str = "./test.db"; +const NAME_LOG_TABLE: &str = "logs"; +const NAME_TEST_TABLE: &str = "tests"; - while running.load(Ordering::SeqCst) { - print!("> "); - _ = stdout().flush(); - length = match stdin().read_line(&mut input) { - Ok(value) => value, - Err(error) => { - println!("\nError: {:#?}\n", error); - continue; +#[derive(Parser, Debug)] +#[clap(author, version, about, long_about=None)] +struct Arguments { + /// subcommand + #[command(subcommand)] + pub action: Action, +} + +#[allow(non_camel_case_types)] +#[derive(Debug, Clone, Subcommand)] +pub enum Action { + /// perform action on logs + #[command(subcommand)] + log(SubAction), + + // /// perform action on tests + // test(SubAction), +} + +#[allow(non_camel_case_types)] +#[derive(Debug, Clone, Subcommand)] +pub enum SubAction { + add(AddArgs), + get(GetArgs), + remove(RemoveArgs), + show(ShowArgs), +} + +#[derive(Debug, Clone, Args)] +pub struct AddArgs { + test: String, + message: String, +} + +#[derive(Debug, Clone, Args)] +pub struct GetArgs { + id: usize, +} + +#[derive(Debug, Clone, Args)] +pub struct RemoveArgs { + range_start: usize, + range_stop: usize, +} + +#[derive(Debug, Clone, Args)] +pub struct ShowArgs { + range_start: Option, + range_stop: Option, +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let args = Arguments::try_parse()?; + let logger = Logger::setup().await.unwrap(); + let read_logger = logger.clone(); + + match args.action { + Action::log(subaction) => { + match subaction { + SubAction::add(subargs) => { + logger.add_log(&subargs.test, &subargs.message).await.unwrap(); + } + SubAction::get(subargs) => { + let record = logger.query_for_log(&format!("DELETE FROM logs WHERE log_id = {} RETURNING log_id, log_time, test_name, log_message;", subargs.id)).await.unwrap(); + println!("record: {:#?}", record); + } + SubAction::remove(subargs) => { + // remove_log(&db, subargs.range_start, subargs.range_stop).await?; + } + SubAction::show(subargs) => { + let records = read_logger.query_for_log(&format!("SELECT * FROM logs;")).await.unwrap(); + println!("records: {:#?}", records); + } } - }; - - if 0 == length { - continue; } - - input = input.trim_matches('\n').to_owned(); - tokens = input.splitn(2,[' ']).collect(); - if tokens.len() != 2 { - continue; - } - - let cmd = tokens.remove(0); - let data = tokens.remove(0); - - if cmd == "raw" { - println!("{:02x?}", data.as_bytes()); - } else if cmd == "str" { - println!("{}", data); - } - - input.clear(); } Ok(()) } - -// NOTE: not required, but might be fun to implement at some point -// fn tokenise(input: &str, separator: Vec<&str>) -> Vec { -// let separator = separator.join(""); -// let mut tokens: Vec = Vec::new(); -// let mut quoted = false; -// -// for char in input.chars() { -// input.len -// if separator.contains(char) { -// } -// } -// -// tokens -// } diff --git a/rust/test.txt b/rust/test.txt deleted file mode 100644 index e302652..0000000 --- a/rust/test.txt +++ /dev/null @@ -1,192 +0,0 @@ -DeriveInput { - attrs: [], - vis: Visibility::Public( - Pub, - ), - ident: Ident { - ident: "Settings", - span: #0 bytes(1363..1371), - }, - generics: Generics { - lt_token: None, - params: [], - gt_token: None, - where_clause: None, - }, - data: Data::Struct { - struct_token: Struct, - fields: Fields::Named { - brace_token: Brace, - named: [ - Field { - attrs: [ - Attribute { - pound_token: Pound, - style: AttrStyle::Outer, - bracket_token: Bracket, - meta: Meta::Path { - leading_colon: None, - segments: [ - PathSegment { - ident: Ident { - ident: "nested_config", - span: #0 bytes(1380..1393), - }, - arguments: PathArguments::None, - }, - ], - }, - }, - ], - vis: Visibility::Public( - Pub, - ), - mutability: FieldMutability::None, - ident: Some( - Ident { - ident: "general", - span: #0 bytes(1403..1410), - }, - ), - colon_token: Some( - Colon, - ), - ty: Type::Path { - qself: None, - path: Path { - leading_colon: None, - segments: [ - PathSegment { - ident: Ident { - ident: "GeneralSettings", - span: #0 bytes(1412..1427), - }, - arguments: PathArguments::None, - }, - ], - }, - }, - }, - Comma, - Field { - attrs: [ - Attribute { - pound_token: Pound, - style: AttrStyle::Outer, - bracket_token: Bracket, - meta: Meta::Path { - leading_colon: None, - segments: [ - PathSegment { - ident: Ident { - ident: "nested_config", - span: #0 bytes(1435..1448), - }, - arguments: PathArguments::None, - }, - ], - }, - }, - ], - vis: Visibility::Public( - Pub, - ), - mutability: FieldMutability::None, - ident: Some( - Ident { - ident: "format", - span: #0 bytes(1458..1464), - }, - ), - colon_token: Some( - Colon, - ), - ty: Type::Path { - qself: None, - path: Path { - leading_colon: None, - segments: [ - PathSegment { - ident: Ident { - ident: "FormatSettings", - span: #0 bytes(1466..1480), - }, - arguments: PathArguments::None, - }, - ], - }, - }, - }, - Comma, - Field { - attrs: [ - Attribute { - pound_token: Pound, - style: AttrStyle::Outer, - bracket_token: Bracket, - meta: Meta::Path { - leading_colon: None, - segments: [ - PathSegment { - ident: Ident { - ident: "nested_config", - span: #0 bytes(1488..1501), - }, - arguments: PathArguments::None, - }, - ], - }, - }, - Attribute { - pound_token: Pound, - style: AttrStyle::Outer, - bracket_token: Bracket, - meta: Meta::Path { - leading_colon: None, - segments: [ - PathSegment { - ident: Ident { - ident: "no_config", - span: #0 bytes(1509..1518), - }, - arguments: PathArguments::None, - }, - ], - }, - }, - ], - vis: Visibility::Public( - Pub, - ), - mutability: FieldMutability::None, - ident: Some( - Ident { - ident: "styles", - span: #0 bytes(1528..1534), - }, - ), - colon_token: Some( - Colon, - ), - ty: Type::Path { - qself: None, - path: Path { - leading_colon: None, - segments: [ - PathSegment { - ident: Ident { - ident: "StyleSettings", - span: #0 bytes(1536..1549), - }, - arguments: PathArguments::None, - }, - ], - }, - }, - }, - Comma, - ], - }, - semi_token: None, - }, -} \ No newline at end of file