barley-std 0.4.0

The Barley standard library
Documentation
use barley_runtime::prelude::*;
use tokio::process::Command as TokioCommand;

pub struct Command {
    command: Vec<ActionInput<String>>,
    check: Option<Vec<ActionInput<String>>>,
    undo: Option<Vec<ActionInput<String>>>
}

impl Command {
    pub fn new(command: Vec<ActionInput<String>>) -> Self {
        Self {
            command,
            check: None,
            undo: None
        }
    }

    pub fn check(&mut self, check: Vec<ActionInput<String>>) -> &mut Self {
        self.check = Some(check);
        self
    }

    pub fn undo(&mut self, undo: Vec<ActionInput<String>>) -> &mut Self {
        self.undo = Some(undo);
        self
    }
}

async fn resolve_argv(argv: &Vec<ActionInput<String>>, ctx: Runtime) -> Result<Vec<String>, ActionError> {
    let mut resolved = Vec::new();

    for arg in argv {
        resolved.push(match arg {
            ActionInput::Static(value) => value.clone(),
            ActionInput::Dynamic(output) => ctx.get_output(output.clone()).await
                .ok_or(ActionError::NoActionReturn)?
                .try_into()?
        });
    }

    Ok(resolved)
}

#[async_trait]
impl Action for Command {
    async fn probe(&self, runtime: Runtime) -> Result<Probe, ActionError> {
        let needs_run = match &self.check {
            Some(check) => {
                let argv = resolve_argv(check, runtime).await?;
                let name = argv.first().unwrap().clone();

                let status = TokioCommand::new(argv.first().unwrap())
                    .args(&argv.into_iter().skip(1).collect::<Vec<String>>())
                    .stdout(std::process::Stdio::null())
                    .stderr(std::process::Stdio::null())
                    .status()
                    .await
                    .map_err(|e| ActionError::ActionFailed(
                        format!("Internal spawn error: {}", e),
                        format!("Failed to spawn command: {}. This is a bug in the Barley engine.", name))
                    )?;
                
                !status.success()
            },
            None => true
        };

        Ok(Probe {
            needs_run,
            can_rollback: self.undo.is_some()
        })
    }

    async fn run(&self, runtime: Runtime, op: Operation) -> Result<Option<ActionOutput>, ActionError> {
        if matches!(op, Operation::Rollback) && self.undo.is_none() {
            return Err(ActionError::OperationNotSupported)
        }

        let argv = resolve_argv(match op {
            Operation::Perform => &self.command,
            Operation::Rollback => self.undo.as_ref().unwrap()
        }, runtime).await?;

        let name = argv.first().unwrap().clone();

        let status = TokioCommand::new(argv.first().unwrap())
            .args(&argv.into_iter().skip(1).collect::<Vec<String>>())
            .stdout(std::process::Stdio::null())
            .stderr(std::process::Stdio::null())
            .status()
            .await
            .map_err(|e| ActionError::ActionFailed(
                format!("Internal spawn error: {}", e),
                format!("Failed to spawn command: {}. This is a bug in the Barley engine.", name)
            ))?;
        
        if !status.success() {
            return Err(ActionError::ActionFailed(
                format!("Command exited with non-zero status code: {}", status.code().unwrap_or(1)),
                format!("Failed to run command: {}", name)
            ))
        } else {
            Ok(None)
        }
    }

    fn display_name(&self) -> String {
        format!("Command: {}", match self.command.first() {
            Some(ActionInput::Static(value)) => value,
            Some(ActionInput::Dynamic(_)) => "<dynamic>",
            None => "<empty>"
        })
    }
}