depl 2.4.3

Toolkit for a bunch of local and remote CI/CD actions
Documentation
//! Requirements module.

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;

/// Requirement.
///
/// Deployer tries to satisfy Pipeline's Actions requirements before every Pipeline execution.
/// If a single requirement fails to satisfy, Deployer exits.
#[derive(Deserialize, Serialize, PartialEq, Eq, Hash, Clone, PartialOrd, Ord)]
#[serde(rename_all = "snake_case", tag = "type")]
#[allow(missing_docs)]
pub enum Requirement {
  /// Requirement of path existence.
  Exists {
    path: PathBuf,
    #[serde(default, skip_serializing_if = "String::is_empty")]
    desc: String,
  },
  /// Requirement of at least single path of a given list existence.
  ExistsAny {
    paths: BTreeSet<PathBuf>,
    #[serde(default, skip_serializing_if = "String::is_empty")]
    desc: String,
  },
  /// Requirement that checks existence of a command in PATH variable paths. Under the hood, it just checks it by `$ which <executable>`.
  InPath {
    executable: String,
    #[serde(default, skip_serializing_if = "String::is_empty")]
    desc: String,
  },
  /// Requirement that executes some Check Action to be satisfied.
  CheckSuccess {
    #[serde(flatten)]
    action: TestAction,
    #[serde(default, skip_serializing_if = "String::is_empty")]
    desc: String,
  },
  /// Requirement that checks the given remote host. See `RemoteHost::check`.
  RemoteAccessibleAndReady { remote_host_name: ShortName },
}

impl Requirement {
  /// Creates a simple `InPath` requirement.
  pub fn in_path(executable: impl ToString) -> Self {
    Self::InPath {
      executable: executable.to_string(),
      desc: String::new(),
    }
  }

  /// Tries to satisfy the given requirement.
  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()
    }
  }
}

/// Satisfy errors type.
#[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())
      }
    }
  }
}