use colored::Colorize;
use serde::{Deserialize, Serialize};
use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
use crate::actions::test::TestAction;
use crate::bmap;
use crate::entities::custom_command::CustomCommand;
use crate::entities::environment::RunEnvironment;
use crate::entities::info::ShortName;
#[derive(Deserialize, Serialize, PartialEq, Eq, Hash, Clone, PartialOrd, Ord)]
#[serde(rename_all = "snake_case", tag = "type")]
#[allow(missing_docs)]
pub enum Requirement {
Exists {
path: PathBuf,
#[serde(default, skip_serializing_if = "String::is_empty")]
desc: String,
},
ExistsAny {
paths: BTreeSet<PathBuf>,
#[serde(default, skip_serializing_if = "String::is_empty")]
desc: String,
},
InPath {
executable: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
desc: String,
},
CheckSuccess {
#[serde(flatten)]
action: TestAction,
#[serde(default, skip_serializing_if = "String::is_empty")]
desc: String,
},
RemoteAccessibleAndReady { remote_host_name: ShortName },
}
impl Requirement {
pub fn in_path(executable: impl ToString) -> Self {
Self::InPath {
executable: executable.to_string(),
desc: String::new(),
}
}
pub async fn satisfy<'a>(&'a self, env: &RunEnvironment<'_>) -> Result<(), SatisfyErr<'a>> {
match self {
Self::Exists { path, desc } => {
if path.resolve_exists(env.run_dir) {
Ok(())
} else {
if !desc.is_empty() {
println!("Can't satisfy requirement. {}", desc.blue().italic());
}
Err(SatisfyErr::Exists(path))
}
}
Self::ExistsAny { paths, desc } => {
if paths.iter().any(|p| p.resolve_exists(env.run_dir)) {
Ok(())
} else {
if !desc.is_empty() {
println!("Can't satisfy requirement. {}", desc.blue().italic());
}
Err(SatisfyErr::ExistsAny(paths))
}
}
Self::InPath { executable, desc } => match CustomCommand::run_simple(env, format!("which {executable}")).await {
Ok(_) => Ok(()),
Err(_) => {
if !desc.is_empty() {
println!("Can't satisfy requirement. {}", desc.blue().italic());
}
Err(SatisfyErr::NoBinary(executable.to_owned()))
}
},
Self::CheckSuccess { action, desc } => {
let (status, out) = action
.execute(env, &bmap!())
.await
.map_err(|e| SatisfyErr::Check(vec![e.to_string()]))?;
if status {
Ok(())
} else {
if !desc.is_empty() {
println!("Can't satisfy requirement. {}", desc.blue().italic());
}
Err(SatisfyErr::Check(out))
}
}
Self::RemoteAccessibleAndReady { remote_host_name } => {
let globals = crate::rw::read::<crate::globals::DeployerGlobalConfig>(&env.config_dir, crate::GLOBAL_CONF);
if let Some(remote) = globals.remote_hosts.get(remote_host_name) {
remote.check().await.map_err(|e| SatisfyErr::Remote(e.to_string()))
} else {
Err(SatisfyErr::Remote(
"There is no such remote host in Deployer's Registry!".to_string(),
))
}
}
}
}
}
trait ResolveExists {
fn resolve_exists(&self, run_dir: &Path) -> bool;
}
impl ResolveExists for PathBuf {
fn resolve_exists(&self, run_dir: &Path) -> bool {
use dirs::home_dir;
let lossy = self.to_string_lossy();
if lossy.starts_with("~/") {
PathBuf::from(lossy.replace("~", &home_dir().unwrap().to_string_lossy())).exists()
} else if self.is_absolute() {
self.exists()
} else {
run_dir.join(self).exists()
}
}
}
#[allow(missing_docs)]
pub enum SatisfyErr<'a> {
Exists(&'a PathBuf),
ExistsAny(&'a BTreeSet<PathBuf>),
NoBinary(String),
Check(Vec<String>),
Remote(String),
}
impl std::fmt::Display for Requirement {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Exists { path, .. } => write!(f, "{path:?}"),
Self::ExistsAny { paths, .. } => {
if !paths.is_empty() {
write!(f, "{paths:?}")
} else {
write!(f, "[]")
}
}
Self::InPath { executable, .. } => write!(f, "$ which {executable}"),
Self::CheckSuccess { action, .. } => write!(f, "`{}`", action.command.cmd),
Self::RemoteAccessibleAndReady { remote_host_name } => {
write!(f, "`{}`", remote_host_name.as_str())
}
}
}
}