trisync 0.1.3

A friendly CLI Tool for automating synchronization of multiple TRIRIGA environment by using the OM API
Documentation
use crate::repo;
use dialoguer::console::Style;
use dialoguer::{theme::ColorfulTheme, Confirm, Input, Select, MultiSelect};
use serde::{Deserialize, Serialize};
use serde_yaml;
use std::env::current_dir;
use std::error::Error;
use std::fs::File;
use std::io::Write;
use std::path::Path;
use std::time::{SystemTime, UNIX_EPOCH};

use std::fmt::Display;

#[derive(Debug, Serialize, Deserialize, Clone)]
/// TRIRIGA OM Object Parameters
pub struct TririgaOMObject {
	pub object_type: String,
	pub object_name:String,
	pub module_name: String,
	pub bo_name: Option<String>
}

#[derive(Debug, Serialize, Deserialize, Clone)]
/// TRIRIGA Configuration
pub struct TririgaConfig {
	pub om_path: String,
	pub url: String,
	pub user: String,
	pub password: String,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
/// Application Configuration, including a list of OM objects to keep track of
pub struct ApplicationConfig {
	pub package_name: String,
	pub package_description: String,
	pub created_at: u128,
	pub remote_url: String,
	pub objects: Vec<TririgaOMObject>
}



impl ApplicationConfig  {
	/// Parse Application config from a yaml file
	pub fn from_yaml(path: &str) -> Result<ApplicationConfig, Box<dyn std::error::Error>> {
		let file = std::fs::File::open(path).expect("Could not open Application Configuration file. You might want to run `init` command first");
		let config: ApplicationConfig =
			serde_yaml::from_reader(file).expect("Could not parse config file. The file might be in wrong format. Consider deleting it and retry.");
		Ok(config)
	}

	/// Save config to yaml file
	pub fn to_yaml(config: &ApplicationConfig, path:&str) -> Result<(), Box<dyn Error>> {
		let deserialized_config = serde_yaml::to_string(config)?;
		let path = Path::new(path);
		let mut file = File::create(&path)?;
		file.write_all(&deserialized_config.as_bytes())?;

		Ok(())
	}

	/// Display a multi select dialog to delete objects from OM list. Return the list of deleted objects
	pub fn delete_tririga_om_objects_dialog(&mut self) -> Result<Vec<TririgaOMObject>, Box<dyn Error>> {
		let theme: ColorfulTheme = ColorfulTheme {
			values_style: Style::new().yellow().dim(),
			..ColorfulTheme::default()
		};
		let objects_to_be_deleted_index = MultiSelect::with_theme(&theme).with_prompt("Objects to be removed (Use Spacebar to select, Enter to validate)").items(&self.objects).interact()?;
		// Collect deleted objects to return
		let objects_to_be_deleted = objects_to_be_deleted_index.iter().map(|i| self.objects[*i].clone()).collect();
		// Delete selected objects from config
		for i in objects_to_be_deleted_index {
			self.objects.remove(i);
		}
		Ok(objects_to_be_deleted)
	}
	
	pub fn init(path: &str) -> Result<Option<(bool, ApplicationConfig)>, Box<dyn Error>> {
		let theme: ColorfulTheme = ColorfulTheme {
			values_style: Style::new().yellow().dim(),
			..ColorfulTheme::default()
		};
		let file_path = Path::new(path);
		if file_path.exists() {
			if !Confirm::with_theme(&theme)
				.with_prompt("Application configuration file already exists. Overwrite?")
				.default(false)
				.interact()?
			{
				match Self::from_yaml(path) {
					Ok(c) => return Ok(Some((false, c))),
					Err(e) => panic!("Failed to read existing application config, {}", e),
				}
			} else {
				Self::init_dialog(path)
			}
		} else {
			println!("Application Config file does not exists. Creating one");
			match File::create(path) {
				Ok(_) => Self::init_dialog(path),
				Err(e) => panic!("Could not create application config file, {}", e),
			}
		}
	}

	fn init_dialog(path: &str) -> Result<Option<(bool, ApplicationConfig)>, Box<dyn Error>> {
		let theme: ColorfulTheme = ColorfulTheme {
			values_style: Style::new().yellow().dim(),
			..ColorfulTheme::default()
		};
		let current_time = SystemTime::now();
		let current_time_millis = current_time
			.duration_since(UNIX_EPOCH)
			.expect("Time went backwards")
			.as_millis();

		if !Confirm::with_theme(&theme)
			.with_prompt("Do you want to continue")
			.default(true)
			.interact()?
		{
			return Ok(None);
		}


		let repo_url =
			repo::Repo::new(&current_dir()?.to_str().unwrap()).find_remote_origin_url();
		let remote_url = match repo_url {
			Some(u) => {
				Input::with_theme(&theme)
					.with_prompt("Github repo URL")
					.default(u)
					.allow_empty(false)
					.show_default(true)
					.interact()?
			}
			None => {
				Input::with_theme(&theme)
					.with_prompt("Github repo URL")
					.allow_empty(false)
					.interact()?
			}
		};

		let package_name = Input::with_theme(&theme)
		.with_prompt("OM package name (max length: 42 chracters)")
		.validate_with(|s: &String| -> Result<(), &str> {
			let character_count = s.chars().count();
			if character_count > 42 {
				Err("You cannot give a name lengthening more than 42 characters")
			} else {
				Ok(())
			}
		})
		.interact()?;

		let package_description = Input::with_theme(&theme)
		.with_prompt("Package description")
		.default("Created by CLI Tool".parse().unwrap())
		.interact()?;

		let config = ApplicationConfig {
			created_at: current_time_millis,
			remote_url,
			objects: [].to_vec(),
			package_name,
			package_description
		};


		Self::to_yaml(&config, path)?;

		Ok(Some((true, config)))
	}
}

impl TririgaConfig {
	pub fn from_yaml(path: &str) -> Result<TririgaConfig, Box<dyn std::error::Error>> {
		let file = std::fs::File::open(path).expect("Could not open TRIRIGA Configuration file. You might want to run `init` command first");
		let config: TririgaConfig =
			serde_yaml::from_reader(file).expect("Could not parse TRIRIGA config file. The file might be in wrong format. Consider deleting it and retry.");
		Ok(config)
	}

	pub fn to_yaml(config: &TririgaConfig, path: &str) -> Result<(), Box<dyn Error>> {
		let deserialized_config = serde_yaml::to_string(config)?;
		let path = Path::new(path);
		let mut file = File::create(&path)?;
		file.write_all(&deserialized_config.as_bytes())?;

		Ok(())
	}


	pub fn init(path: &str) -> Result<Option<TririgaConfig>, Box<dyn Error>> {
		let theme: ColorfulTheme = ColorfulTheme {
			values_style: Style::new().yellow().dim(),
			..ColorfulTheme::default()
		};
		let file_path = Path::new(path);
		if file_path.exists() {
			if !Confirm::with_theme(&theme)
				.with_prompt("TRIRIGA Configuration file already exists. Overwrite? If yes, a new package will be generated automatically")
				.default(false)
				.interact()?
			{
				match Self::from_yaml(path) {
					Ok(c) => return Ok(Some( c)),
					Err(e) => panic!("Failed to read existing TRIRIGA config, {}", e),
				}
			} else {
				Self::init_dialog(path)
			}
		} else {
			println!("TRIRIGA Config file does not exists. Creating one");
			match File::create(path) {
				Ok(_) => Self::init_dialog(path),
				Err(e) => panic!("Could not create TRIRIGA config file, {}", e),
			}
		}
	}

	fn init_dialog(path: &str) -> Result<Option<TririgaConfig>, Box<dyn Error>> {
		let theme: ColorfulTheme = ColorfulTheme {
			values_style: Style::new().yellow().dim(),
			..ColorfulTheme::default()
		};
		if !Confirm::with_theme(&theme)
			.with_prompt("Do you want to continue")
			.default(true)
			.interact()?
		{
			return Ok(None);
		}


		let tririga_url = Input::with_theme(&theme)
			.with_prompt("Tririga URL")
			.default("http://localhost:8008".parse().unwrap())
			.interact()?;

		let tririga_username = Input::with_theme(&theme)
			.with_prompt("Tririga username")
			.default("system".parse().unwrap())
			.interact()?;
		let tririga_password = Input::with_theme(&theme)
			.with_prompt("Tririga password")
			.default("admin".parse().unwrap())
			.interact()?;

		let tririga_om_path = Input::with_theme(&theme).with_prompt("Tririga OM path (mapping to '/root/ibm/tririga/userfiles/ObjectMigration/UploadsWithImport/' | Only use absolute path)").
		validate_with(|p: &String| -> Result<(), &str> {
			match std::env::consts::OS {
				"windows" => {
					if p.chars().nth(0).unwrap().is_alphabetic() {
						Ok(())
					} else {
						Err("Path must be absolute (begins with an uppercase character)")
					}
				}
				_ => {
					if p.chars().nth(0).unwrap() == '/' {
						Ok(())
					} else {
						Err("Path must be absolute (begins with '/')")
					}
				}
			}
			
		})
		.interact()?;

		let config = TririgaConfig {
				om_path: tririga_om_path,
				url: tririga_url,
				user: tririga_username,
				password: tririga_password,
		};
		Self::to_yaml(&config, path)?;

		Ok(Some(config))
	}
}

impl TririgaOMObject {
	/// Display an interactive dialog and create a object based on user inputs
	pub fn from_dialog() -> Result<Self, Box<dyn Error>> {
		let theme: ColorfulTheme = ColorfulTheme {
			values_style: Style::new().yellow().dim(),
			..ColorfulTheme::default()
		};
		let tririga_om_object_types: Vec<&str> = vec!["MODULE", "BO", "QUERY", "GUI", "WF", "SCORECARD", "LIST", "PORTAL", "PORTALSEC", "ASSOCIATION", "DOC", "RECDATASTR", "GRPDATASTR", "GUISTYLE", "TOKEN", "NAVCOLLECTION", "NAVITEM", "ALTFORMLIST", "CALENDARSET"];

		let object_type = Select::with_theme(&theme).with_prompt("Object type (Use arrow keys to select an item from list)").default(0).items(&tririga_om_object_types).interact()?;
		let object_name = Input::with_theme(&theme).with_prompt("Object name").allow_empty(false).interact()?;
		let module_name = Input::with_theme(&theme).with_prompt("Module Name").allow_empty(false).interact()?;
		let bo_name = Input::with_theme(&theme).with_prompt("BO Name").allow_empty(true).interact()?;

		Ok(Self{
			object_name, 
			object_type: tririga_om_object_types[object_type].to_owned(),
			module_name,
			bo_name: Some(bo_name)
		})
	}
}

impl Display for TririgaOMObject {
	fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
		write!(fmt, "[{}] Object name: {}. Module name: {}. BO Name: {}", self.object_type, self.object_name, self.module_name, self.bo_name.as_ref().unwrap_or(&"Not set".to_string()))
	}
}