use chrono::{DateTime, Local}; use sqlx::{ migrate::MigrateDatabase, query, query_as, sqlite::{SqlitePool, SqlitePoolOptions}, Row, Sqlite, }; const URL_DATABASE: &str = "log.db"; pub const TABLE_TESTS: &str = "tests"; pub const TABLE_TYPES: &str = "log_types"; pub const TABLE_LOGS: &str = "logs"; pub const TEST_NONE: &str = "none"; const TYPE_INFORMATION: &str = "information"; const TYPE_WARNING: &str = "warning"; const TYPE_ERROR: &str = "error"; #[derive(Debug, Clone, thiserror::Error)] pub enum Error { #[error("Error in crate `sqlx`: {0}")] Internal(String), #[error("failed to convert `{0}` to LogType")] LogTypeConversion(String), } #[derive(Clone, Debug, sqlx::FromRow)] pub struct LogRecord { log_id: i32, /// timestamp of the logs creation log_time: DateTime, /// type of log (information, error..) log_type: LogType, /// 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())), }; let logger = Self { database: db }; logger .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) )" )) .await?; logger .query(&format!( "INSERT OR IGNORE INTO {TABLE_TESTS} (test_name) VALUES('{TEST_NONE}');" )) .await?; logger .query(&format!( "CREATE TABLE IF NOT EXISTS {TABLE_TYPES} ( log_type VARCHAR(255) NOT NULL UNIQUE, PRIMARY KEY (log_type) )" )) .await?; logger .query(&format!( "INSERT OR IGNORE INTO {TABLE_TYPES} (log_type) VALUES('{TYPE_INFORMATION}');" )) .await?; logger .query(&format!( "INSERT OR IGNORE INTO {TABLE_TYPES} (log_type) VALUES('{TYPE_WARNING}');" )) .await?; logger .query(&format!( "INSERT OR IGNORE INTO {TABLE_TYPES} (log_type) VALUES('{TYPE_ERROR}');" )) .await?; logger .query(&format!( "CREATE TABLE IF NOT EXISTS {TABLE_LOGS} ( log_id INTEGER NOT NULL UNIQUE, log_time INTEGER DEFAULT CURRENT_TIMESTAMP, log_type VARCHAR(255) NOT NULL, test_name VARCHAR(255) NOT NULL, log_message VARCHAR(2047), PRIMARY KEY (log_id), FOREIGN KEY (log_type) REFERENCES {TABLE_TYPES}(log_type), FOREIGN KEY (test_name) REFERENCES {TABLE_TESTS}(test_name) );" )) .await?; Ok(logger) } 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_tests(&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 } } } pub async fn add_test(&self, name: &str, time: DateTime) -> Result<(), Error> { self.query(&format!( "INSERT INTO {TABLE_TESTS} (test_name, test_time) VALUES('{name}', '{}');", time.timestamp() )) .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, log_type: LogType, test: &str, message: &str) -> Result<(), Error> { let log_type = log_type.as_str(); self.query(&format!( "INSERT INTO {TABLE_LOGS} (log_type, test_name, log_message) VALUES('{log_type}', '{test}', '{message}');" )) .await } } #[derive(Clone, Debug, PartialEq, sqlx::Type)] #[sqlx(rename_all = "lowercase")] pub enum LogType { Information, Warning, Error, } impl LogType { pub fn as_str(&self) -> &'static str { match self { Self::Information => TYPE_INFORMATION, Self::Warning => TYPE_WARNING, Self::Error => TYPE_ERROR, } } pub fn from_str(input: &str) -> Result { match input.to_lowercase().as_str() { TYPE_INFORMATION => Ok(Self::Information), TYPE_WARNING => Ok(Self::Warning), TYPE_ERROR => Ok(Self::Error), unrecognised => Err(Error::LogTypeConversion(unrecognised.to_owned())), } } }