ginst 0.1.2

A generic installation tool, able to read and execute instructions from a json file.
//! # Program
//!
//! Crate with structs representing programs and common operations for said programs

pub mod steps;

use std::process::Command;
use crate::distro::get_dist;
use json::JsonValue::{self, Null};
use steps::InstructionSet;

/// Struct indicating the programs installation status
#[derive(Default, Debug, PartialEq, Clone, Copy)]
pub enum Status {
    Installed,
    #[default] Missing,
}

/// Struct representing a program 
#[derive(Default, Debug, Clone)]
pub struct Program {
    pub status: Status,
    pub name: String,
    installation: InstructionSet,
    configuration: InstructionSet,
    pub dependencies: ProgramCollection,
}

impl Program {
    fn is_installed(&self) -> bool {
        self.status == Status::Installed && self.dependencies.are_installed()
    }

    /// Checks if a program is installed using the `command -v` command.
    fn check(&self) -> Status {
        /* Performs a check if the program is installed */
        let status = Command::new("command")
                        .arg("-v")
                        .arg(&self.name)
                        .status()
                        .expect("Failed to execute.");

        if status.success() {
            Status::Installed
        } else {
            Status::Missing
        }
    }

    pub fn has_configuration_steps(&self) -> bool {
        !self.configuration.is_empty() && self.configuration.len() != 0
    }

    pub fn has_installation_steps(&self) -> bool {
        !self.installation.is_empty() && self.installation.len() != 0
    }

    pub fn has_dependencies(&self) -> bool {
        !self.dependencies.is_empty() && self.dependencies.len() != 0
    }

    /// Executes installation instructions for the current distro (uses get_dist())
    pub fn install(&self) {
        if self.is_installed() {
            println!("{} is already installed", self.name);
            return;
        }

        let current_dist = get_dist();
        if self.has_installation_steps() {
            // omg this is so nice
            let installation_steps = self.installation.for_dist(current_dist.clone());
            if let Some(steps) = installation_steps {
                steps.execute();
            } else {
                println!("No installation instructions for '{}' given", current_dist);
            }
        } else {
            println!("No installation instructions for program '{}' given.", self.name);
        }
    }

    /// Executes configuration instructions for the current distro (uses get_dist())
    pub fn configure(&self) {
        let current_dist = get_dist();
        if self.has_configuration_steps() {
            // omg this is so nice
            let configuration_steps = self.configuration.for_dist(current_dist.clone());
            if let Some(steps) = configuration_steps {
                steps.execute();
            } else {
                println!("No configuration instructions for '{}' given", current_dist);
            }
        } else {
            println!("No configuration instructions for program '{}' given.", self.name);
        }
    }

    pub fn print(&self, indent_level: u8) {
        self.print_status();
        self.dependencies.print_statuses(indent_level + 1);
    }

    pub fn print_status(&self) {
        if self.is_installed() {
            println!("[✓] {}", self.name);
        } else {
            println!("[⤫] {}", self.name);
        }
    }
}

/// A collection of programs with some utilie
#[derive(Default, Debug, Clone)]
pub struct ProgramCollection {
    pub programs: Vec<Program>
}

impl ProgramCollection {

    pub fn len(&self) -> usize {
        self.programs.len()
    }

    pub fn is_empty(&self) -> bool {
        self.programs.is_empty() && self.programs.len() == 0
    }

    pub fn are_installed(&self) -> bool {
        if !self.is_empty() {
            for val in self.programs.clone().iter_mut().map(|d| d.is_installed()) {
                if !val {
                    return false;
                }
            }
        }

        true
    }

    pub fn print_statuses(&self, indent_level: u8) {
        for program in self.programs.clone() {
            for _ in 0..indent_level {
                print!("  "); // indent by 1 block
            }
            program.print(indent_level);
        }
    }

    pub fn install_missing(&self) {
        for prog in self.programs.clone() {
            prog.install();
        }
    }

    pub fn count_missing(&self) -> u8 {
        let mut counter = 0;
        for p in self.programs.clone() {
            if !p.is_installed() {
                counter += 1;
            }
        }
        counter
    }

    pub fn push(&mut self, program: Program) {
        self.programs.push(program);
    }

}

/// Reads parsed json to generate a single program and performs check() to see of the program if
/// installed
pub fn from_json(json_parsed: &JsonValue) -> Program {
    let mut prog: Program = Default::default();

    prog.name = json_parsed["name"].clone().to_string();
    prog.installation = steps::from_json(json_parsed["installation"].clone());
    prog.configuration = steps::from_json(json_parsed["configuration"].clone());
    prog.status = prog.check();
    prog.dependencies = collection_from_json(json_parsed["dependencies"].clone());
    
    prog
}

/// Generates a program collection from parsed json
pub fn collection_from_json(json_parsed: JsonValue) -> ProgramCollection{
    let mut programs: ProgramCollection = Default::default();

    if json_parsed != Null {
        for program in json_parsed["programs"].members() {
            programs.push(from_json(program));
        }
    }
    
    programs
}

#[cfg(test)]
mod tests {
    use json;
    use super::from_json;

    #[test]
    fn test_from_json() {
        let prog = "{
            \"name\": \"topkek\",
            \"installation\": {
                \"*\": [
                    \"echo '#!/bin/bash' >> ~/.local/bin/topkek\",
                    \"echo 'echo hello :D' >> ~/.local/bin/topkek\",
                    \"chmod +x ~/.local/bin/topkek\"
                ]
            },
            \"configuration\": {
                \"*\": [
                    \"~/.local/bin/topkek\"
                ]
            }
        }";
        
        from_json(&json::parse(prog).unwrap());
    }

    #[test]
    #[should_panic]
    fn test_from_json_invalid() {
        // the missing ',' after name
        let prog = "{
            \"name\": \"topkek\"
            \"installation\": {
                \"*\": [
                ]
            }
        }";
        
        from_json(&json::parse(prog).unwrap());
    }


    #[test]
    fn test_has_config() {
        let prog = "{
            \"name\": \"topkek\",
            \"configuration\": {
                \"*\": [
                    \"~/.local/bin/topkek\"
                ]
            }
        }";
        
        let prog = from_json(&json::parse(prog).unwrap());
        assert!(prog.has_configuration_steps());
    }

    #[test]
    fn test_has_no_config() {
        let prog = "{
            \"name\": \"topkek\",
            \"installation\": {
                \"*\": [
                    \"echo '#!/bin/bash' >> ~/.local/bin/topkek\",
                    \"echo 'echo hello :D' >> ~/.local/bin/topkek\",
                    \"chmod +x ~/.local/bin/topkek\"
                ]
            }
        }";
        
        let prog = from_json(&json::parse(prog).unwrap());
        assert!(prog.has_configuration_steps() == false);
    }
    
    #[test]
    fn test_has_install() {
        let prog = "{
            \"name\": \"topkek\",
            \"installation\": {
                \"*\": [
                    \"echo '#!/bin/bash' >> ~/.local/bin/topkek\",
                    \"echo 'echo hello :D' >> ~/.local/bin/topkek\",
                    \"chmod +x ~/.local/bin/topkek\"
                ]
            }
        }";
        
        let prog = from_json(&json::parse(prog).unwrap());
        assert!(prog.has_installation_steps());
    }

    #[test]
    fn test_has_no_install() {
        let prog = "{
            \"name\": \"topkek\"
        }";
        
        let prog = from_json(&json::parse(prog).unwrap());
        assert!(prog.has_installation_steps() == false);
    }

    #[test]
    fn test_is_installed() {
        let prog = "{
            \"name\": \"topkek\"
        }";
        
        let prog = from_json(&json::parse(prog).unwrap());
        assert!(prog.is_installed() == false);
    }
}