boxcap 0.1.0

Common project structure for creating rust projects
Documentation
#![warn(missing_docs)]
//! Boxcap is a type of project structure that focuses on a multilayered approach
//! The application layer is responsible for managing the human hardware interface, as well as initalizing code
//! the main component of the application layer is the dispatcher. The dispatcher resgisters services and spawns
//! them their own thread.
//! The logger is another component of the
//!

use chrono::Local;
use std::collections::{HashMap, VecDeque};
use std::error::Error;
use std::fs::{self, OpenOptions};
use std::io::Write;
use std::path::Path;
use std::sync::mpsc::{Receiver, Sender};

use crate::services::{map_comm_pairs, Service};
use std::thread;

pub mod services;

/// Common structure that messages between threads take
pub struct Message {
    to: String,
    author: String,
    content: String,
}

impl Message {
    /// constructs a new message object from a string
    pub fn new(name: &str, to: &str, content: &str) -> Message {
        Message {
            to: to.to_string(),
            author: name.to_string(),
            content: content.to_string(),
        }
    }
}

/// Mediary between services that manages the use of the tx, rx pair between services
pub struct Messenger {
    ids: HashMap<String, (Sender<Message>, Receiver<Message>)>,
}

impl Messenger {
    /// constructs a new messanger object
    pub fn new() -> Self {
        Messenger {
            ids: HashMap::new(),
        }
    }

    /// Assign an id to this sender
    /// How do i know which service i send data to in the future? if this id process
    /// is abstracted i wont know how to send on client side
    pub fn set_comm_pair(
        &mut self,
        name: String,
        comm_pair: (Sender<Message>, Receiver<Message>),
    ) -> () {
        self.ids.insert(name, comm_pair);
    }

    /// routes a message from its source to its destination
    pub fn route(&self, msg: Message) -> Result<(), Box<dyn Error>> {
        if let Some((tx, _)) = self.ids.get(&msg.to) {
            // Route the message
            tx.send(msg).unwrap();
            Ok(())
        } else {
            Err(format!("ID not found in pairs: {}", msg.to).into()) // Return an error if the ID is not found
        }
    }
}

/// The Logger struct behaves as the software component responsible for logging.
pub struct Logger {
    name: String,
    rx: Option<Receiver<Message>>,
    tx: Option<Sender<Message>>,
    log_path: String,
}

impl Logger {
    /// Constructs a new logger
    pub fn new() -> Logger {
        Logger {
            name: "logger".to_string(),
            log_path: "./logs/log".to_string(),
            tx: None,
            rx: None,
        }
    }

    /// Sets the path for the logs to go. Default is ./logs/log.
    /// Allows method chaining and updates the logger instance.
    pub fn set_path(mut self, path: &str) -> Self {
        self.log_path = format!("./logs/{}", path);
        self
    }

    /// Function to allow the logger to write a log to the specified file.
    /// Returns Ok(()) if the log was successfully written, Err(Box<dyn Error>) otherwise.
    pub fn log(&self, message: &str) -> Result<(), Box<dyn Error>> {
        // Ensure the log directory exists
        let log_file_path = Path::new(&self.log_path);
        if let Some(parent) = log_file_path.parent() {
            if !parent.exists() {
                fs::create_dir_all(parent)?;
            }
        }

        // Open the log file in append mode, create it if it does not exist
        let mut file = OpenOptions::new()
            .append(true)
            .create(true)
            .open(&self.log_path)?;

        // Get the current timestamp
        let timestamp = Local::now().format("%Y-%m-%d %H:%M:%S").to_string();

        // Write the log message to the file with the timestamp
        writeln!(file, "{} - {}", timestamp, message)?;

        Ok(())
    }
}

impl Service for Logger {
    /// Run the particular service
    fn run(&self) -> Result<(), Box<dyn Error>> {
        loop {
            if let Some(rx) = &self.rx {
                if let Ok(msg) = rx.try_recv() {
                    println!("Logger received msg: {}", msg.content);
                    if let Err(e) = self.log(&msg.content) {
                        eprintln!("Logger errored with error: {}", e);
                    }
                }
            }
        }
    }

    /// sends a message to
    fn send(&self, msg: Message) -> Result<(), Box<dyn Error>> {
        if let Some(tx) = &self.tx {
            tx.send(msg)?
        }
        Ok(())
    }

    /// The dispactcher will keep track of everyones ID
    fn identify(&self) -> String {
        return self.name.clone();
    }

    ///
    fn set_comm_pair(&mut self, tx: Sender<Message>, rx: Receiver<Message>) -> () {
        self.tx = Some(tx);
        self.rx = Some(rx);
    }
}

/// Dispatcher struct that holds a collection of services
pub struct Dispatcher {
    service_ids: HashMap<String, u8>,
    services: VecDeque<Box<dyn Service>>,
    logger: Logger,
}

impl Dispatcher {
    /// construct a new dispatcher
    pub fn new() -> Dispatcher {
        Dispatcher {
            service_ids: HashMap::new(),
            services: VecDeque::new(),
            logger: Logger::new(),
        }
    }

    /// Register a particular service to this dispatcher
    pub fn register(&mut self, service: Box<dyn Service>) {
        self.services.push_front(service);
    }

    /// Spawn threads for all the services and let them run
    pub fn start(mut self) {
        let mut handles = vec![];
        let mut messenger = Messenger::new();
        while !self.services.is_empty() {
            if let Some(mut service) = self.services.pop_front() {
                map_comm_pairs(&mut service, &mut messenger);

                handles.push(thread::spawn(move || {
                    if let Err(e) = service.run() {
                        // Handle the error, e.g., log it
                        println!("Service run failed: {}", e);
                    }
                }));
            }
        }

        'check_message: loop {
            for (_, rx) in messenger.ids.values() {
                if let Ok(msg) = rx.try_recv() {
                    match messenger.route(msg) {
                        Ok(_) => {}
                        Err(e) => {
                            eprintln!("Dispatcher messager caught the following error: {e}");
                            break 'check_message;
                        }
                    };
                }
            }
        }

        for handle in handles {
            handle.join().unwrap();
        }
    }

    /// Displays the current mapping of IDs
    pub fn log_table(&self) -> Result<(), Box<dyn Error>> {
        let table_state = format!("{:?}", self.service_ids);
        self.logger.log(&table_state)
    }
}

/// The testing suite for the library can be seen here
#[cfg(test)]
mod tests {

    use super::*;
    use crate::services::TestService;
    use std::{fs, path::Path};

    fn clean() -> Result<(), Box<dyn std::error::Error>> {
        // Define the path to the logs directory
        let log_dir_path = Path::new("logs/tests");

        // Check if the directory exists
        if log_dir_path.exists() {
            // Remove the directory and its contents
            fs::remove_dir_all(log_dir_path)?;
            println!("Successfully removed {}", log_dir_path.display());
        } else {
            println!("Directory {} does not exist.", log_dir_path.display());
        }

        Ok(())
    }

    #[test]
    fn test_dispatcher() {
        let mut d = Dispatcher::new();

        d.register(Box::new(Logger::new().set_path("tests/test_dispatcher")));
        d.register(Box::new(TestService::new("test")));

        d.start();

        clean().expect("FAILED TO CLEAN");
    }

    #[tokio::test]
    async fn test_dispatch_db() {
        let mut d = Dispatcher::new();

        let db_type = services::database::DatabaseType::LOCAL;
        let db = match services::Database::new("test", "test", db_type).await {
            Ok(db) => db,
            Err(e) => panic!("test_dispatch_db: {}", e),
        };

        let tt = Box::new(TestService::new("test_service"));

        assert_eq!(db.identify(), db.name);
        assert_eq!(tt.identify(), "test_service");

        d.register(Box::new(db));
        d.register(tt);

        d.start();
    }

    #[test]
    fn test_log() {
        let logger = Logger::new();
        assert!(
            logger.log("This is a log from the test_log test").is_ok(),
            "Logging failed"
        );
        clean().expect("FAILED TO CLEAN");
    }
    #[test]
    fn test_custom_log() {
        let logger = Logger::new().set_path("/tests/custom_logs");
        assert!(
            logger.log("This is a log from the test_log test").is_ok(),
            "Logging failed"
        );
        clean().expect("FAILED TO CLEAN");
    }
}