trisync 0.1.3

A friendly CLI Tool for automating synchronization of multiple TRIRIGA environment by using the OM API
Documentation
use reqwest::{Client, RequestBuilder, StatusCode};
use std::str::FromStr;

use serde::Deserialize;
use std::error::Error;
use std::fs::File;
use std::io::Write;
use std::path::Path;
pub struct Api {
	client: Client,
	api_url: String,
	base_url: String,
	credentials: Credentials,
}
pub struct Credentials {
	pub user: String,
	pub password: String,
}

#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
struct Response {
	package_id: u128,
	package_name: String,
	status: String,
	message: String,
}
#[derive(Debug, PartialEq, Eq)]
pub enum PackageStatus {
	New,
	ExportPending,
	Exported,
	ExportFailed,
	ImportFailed,
	Imported,
	ImportPending,
}

impl FromStr for PackageStatus {
	type Err = ();
	fn from_str(s: &str) -> Result<Self, ()> {
		match s {
			"New" => Ok(Self::New),
			"Export Pending" => Ok(Self::ExportPending),
			"Exported" => Ok(Self::Exported),
			"Export Failed" => Ok(Self::ExportFailed),
			"Import Failed" => Ok(Self::ImportFailed),
			"Imported" => Ok(Self::Imported),
			"Import Pending" => Ok(Self::ImportPending),
			_ => Err(()),
		}
	}
}

impl Api {
	pub fn new(base_url: &str, credentials: Credentials) -> Api {
		let api_url = String::from(base_url) + "/api/p/v1/om";
		let client = Client::builder().cookie_store(true).build().unwrap();
		Api {
			client: client,
			api_url,
			base_url: base_url.to_owned(),
			credentials,
		}
	}

	pub async fn login(&self) -> Result<(), Box<dyn Error>> {
		let body = serde_json::json!({
			"userName": self.credentials.user,
			"password": self.credentials.password,
			"normal": false,
			"api": true,
		});
		let url = self.base_url.clone() + "/p/websignon/signon";
		let response = self.client.post(url).json(&body).send().await?;
		match response.status() {
			StatusCode::OK => {
				println!("🔐 TRIRIGA Authenticated");
				Ok(())
			}
			_ => Err("Unauthenticated")?,
		}
	}

	#[allow(dead_code)]
	fn authenticated_post(&self, url: &str) -> RequestBuilder {
		self.client.post(url)
	}
	fn get(&self, url: &str) -> RequestBuilder {
		self.client.get(url)
	}

	#[allow(dead_code)]
	pub async fn add_all_application_dependant_objects_to_package_by_id(
		&self,
		package_id: u128,
		date_in_millis: &str,
	) -> Result<(), Box<dyn std::error::Error>> {
		let url = self.api_url.clone()
			+ "/addAllApplicationDependentObjectsToPackage"
			+ "/" + &package_id.to_string();
		self.get(&url)
			.query(&[("dateInMillis", date_in_millis)])
			.send()
			.await?;
		Ok(())
	}

	#[allow(dead_code)]
	pub async fn add_all_application_dependant_objects_to_package_by_name(
		&self,
		package_name: &str,
		date_in_millis: &str,
	) -> Result<(), Box<dyn std::error::Error>> {
		let url = self.api_url.clone() + "/addAllApplicationDependentObjectsToPackage";
		self.get(&url)
			.query(&[
				("packageName", package_name),
				("dateInMillis", date_in_millis),
			])
			.send()
			.await?;
		Ok(())
	}
	#[allow(dead_code)]
	pub async fn add_object_to_package_by_name(
		&self,
		package_name: &str,
		object_type: &str,
		object_name: &str,
		module_name: &str,
		bo_name: &str,
	) -> Result<(), Box<dyn Error>> {
		let url = self.api_url.clone() + "/addObjectToPackage";
		// A hack due to yaml deserialization bug. serde_yaml @ 0.8
		let correct_bo_name = if bo_name == "~" { "" } else { bo_name };
		let res = self
			.get(&url)
			.query(&[
				("packageName", package_name),
				("objectType", object_type),
				("objectName", object_name),
				("moduleName", module_name),
				("boName", correct_bo_name),
			])
			.send()
			.await?;
		match res.status() {
			StatusCode::OK => Ok(()),
			_ => panic!(
				"Couldn't add object {} to package {}",
				&object_name, &package_name
			),
		}
	}
	pub async fn add_object_to_package_by_id(
		&self,
		package_id: &str,
		object_type: &str,
		object_name: &str,
		module_name: &str,
		bo_name: &str,
	) -> Result<(), Box<dyn Error>> {
		let url = self.api_url.clone() + "/addObjectToPackage" + "/" + package_id;
		// A hack due to yaml deserialization bug. serde_yaml @ 0.8
		let correct_bo_name = if bo_name == "~" { "" } else { bo_name };
		let res = self
			.get(&url)
			.query(&[
				("objectType", object_type),
				("objectName", object_name),
				("moduleName", module_name),
				("boName", correct_bo_name),
			])
			.send()
			.await?;
		match res.status() {
			StatusCode::OK => Ok(()),
			_ => panic!(
				"Couldn't add object {} to package {}. {:#?}",
				&object_name, &package_id, &res
			),
		}
	}

	pub async fn create_new_package(
		&self,
		name: &str,
		description: &str,
	) -> Result<u128, Box<dyn Error>> {
		let url = self.api_url.clone() + "/createEmptyPackage";
		let res = self
			.get(&url)
			.query(&[("packageName", name), ("packageDescription", description)])
			.send()
			.await?;
		let parsed_res = res.json::<Response>().await?;
		Ok(parsed_res.package_id)
	}

	pub async fn download_package_as_zip(
		&self,
		package_name: &str,
		path: &Path,
	) -> Result<(), Box<dyn Error>> {
		let url = self.api_url.clone() + "/downloadPackageFromUserFiles";
		let res = self
			.get(&url)
			.query(&[("packageName", package_name)])
			.send()
			.await?;
		let content = res.bytes().await?;
		self.save_as_file(path, &content)?;
		Ok(())
	}

	#[allow(dead_code)]
	pub async fn export_package_by_name(
		&self,
		package_name: &str,
	) -> Result<(), Box<dyn Error>> {
		let url = self.api_url.clone() + "/export";
		let res = self
			.get(&url)
			.query(&[("packageName", package_name)])
			.send()
			.await?;
		match res.status() {
			StatusCode::OK => Ok(()),
			e => panic!("Unable to export package. {}", e),
		}
	}
	pub async fn export_package_by_id(&self, package_id: &str) -> Result<(), Box<dyn Error>> {
		let url = self.api_url.clone() + "/export/" + package_id;
		let res = self.get(&url).send().await?;
		match res.status() {
			StatusCode::OK => Ok(()),
			e => panic!("Unable to export package. {}", e),
		}
	}

	pub async fn get_package_status_by_id(
		&self,
		package_id: &str,
	) -> Result<PackageStatus, Box<dyn Error>> {
		let url = self.api_url.clone() + "/packageStatus/" + package_id;
		let res = self.get(&url).send().await?;
		let parsed_res = self.parse_response(res).await?;

		match PackageStatus::from_str(&parsed_res.status) {
			Ok(s) => Ok(s),
			Err(_) => panic!("Could not parse status"),
		}
	}

	pub async fn get_package_status_by_name(
		&self,
		package_name: &str,
	) -> Result<PackageStatus, Box<dyn Error>> {
		let url = self.api_url.clone() + "/packageStatus";
		let res = self
			.get(&url)
			.query(&[("packageName", package_name)])
			.send()
			.await?;
		let parsed_res = self.parse_response(res).await?;
		match PackageStatus::from_str(&parsed_res.status) {
			Ok(s) => Ok(s),
			Err(_) => panic!("Could not parse status"),
		}
	}

	fn save_as_file(&self, path: &Path, content: &[u8]) -> Result<(), Box<dyn Error>> {
		if path.exists() {
			println!("⚠️ [WARN] File {:?} exists. Overwriting...", path)
		} else {
			println!("🆕 Creating file {:?}", path)
		}
		let mut file = File::create(path)?;
		file.write_all(content)?;
		Ok(())
	}

	async fn parse_response(&self, r: reqwest::Response) -> Result<Response, Box<dyn Error>> {
		match r.json::<Response>().await {
			Ok(res) => Ok(res),
			Err(e) => panic!("Failed to parse response from API: {}", e,),
		}
	}
}