bile 0.1.1

A simple build system, structured around Lua scripts.
//! Structures for collecting build steps from a build.lua file and executing
//! them. This is where most of the magic happens.
//!
//! # Usage in Lua
//!
//! Builders are created in Rust, passed over to Lua to be populated (see
//! [`Builder::add_methods`] for the exact methods exposed to Lua).
//!
//! ## Examples
//!
//! ```lua
//! ---@param builder builder.Builder
//! ---@return builder.Builder
//! function mod.foo(builder)
//!     builder:provide_description("Runs Foo.")
//!
//!     builder:add_sh_step("echo hello from Foo!")
//!
//!     return builder
//! end
//! ```

use std::collections::VecDeque;
use std::error::Error;
use std::fmt::{Display, Error as FmtError};
use std::io::Error as IoError;
use std::process;
use std::sync::Arc;

use clap::Command;
use mlua::{Error as LuaError, FromLua, Function, Lua, Table, UserData, UserDataMethods, Value};

/// A structure for building and collecting build steps.
///
/// See [module level documentation](crate::builder) for more.
#[derive(Debug, Clone)]
pub struct Builder {
    /// Description for the command.
    pub description: Option<String>,
    /// Name of the subcommand.
    pub name: String,
    /// The series of steps to be executed by the [`Builder`].
    steps: VecDeque<Step>,
}

impl Builder {
    /// Creates a [`Command`] from this [`Builder`].
    #[must_use]
    pub fn make_command(&self) -> Command {
        self.description.as_ref().map_or_else(
            || Command::new(&self.name),
            |description| Command::new(&self.name).about(description),
        )
    }

    /// Creates an empty [`Builder`].
    #[must_use]
    pub const fn new() -> Self {
        Self {
            description: None,
            name: String::new(),
            steps: VecDeque::new(),
        }
    }

    /// Creates a new [`Builder`] with the provided name.
    #[must_use]
    pub fn with_name(name: String) -> Self {
        Self {
            name,
            ..Self::new()
        }
    }

    /// Extend this [`Builder`] by applying the Lua [function] `func` to it.
    ///
    /// # Errors
    ///
    /// This function will return an error if the called Lua function doesn't
    /// successfully execute, or returns something that isn't a [`Builder`].
    ///
    /// [function]: `mlua::Function`
    pub fn apply_through_lua(self, func: &Function) -> Result<Self, LuaError> {
        func.call::<Self>(self)
    }

    /// Adds a shell script [step] to this [`Builder`].
    ///
    /// [step]: `Step::Sh`
    pub fn add_sh_step(&mut self, sh_str: String) {
        self.steps.push_back(Step::Sh(sh_str));
    }

    /// Sets the description of this [`Builder`].
    pub fn set_description(&mut self, description: String) {
        self.description = Some(description);
    }

    /// Like [`Self::set_description`], but it can only be called once.
    pub fn provide_description(&mut self, description: String) -> bool {
        if self.description.is_none() {
            self.description = Some(description);
            true
        } else {
            false
        }
    }

    /// Executes every [`Step`] from `steps`. This function is very effectful, be wary.
    ///
    /// # Errors
    ///
    /// See the individual error sections for each [`Step`].
    pub fn execute(self) -> Result<(), IoError> {
        for step in self.steps {
            step.execute()?;
        }
        Ok(())
    }
}

impl Default for Builder {
    fn default() -> Self {
        Self::new()
    }
}

/// Error for when casting to a [`Builder`] from a [`mlua::Value`] fails.
///
/// Contains the value which the cast failed on.
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub struct BuilderFromLuaError(pub &'static str);

impl From<Value> for BuilderFromLuaError {
    fn from(found: Value) -> Self {
        Self(found.type_name())
    }
}

impl Error for BuilderFromLuaError {}
impl Display for BuilderFromLuaError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), FmtError> {
        write!(f, "Expected a builder.Builder found a {}", self.0)
    }
}

impl FromLua for Builder {
    fn from_lua(value: Value, _lua: &Lua) -> Result<Self, LuaError> {
        match value {
            Value::UserData(any_user_data) => any_user_data.take(),
            found => Err(LuaError::ExternalError(Arc::new(BuilderFromLuaError(
                found.type_name(),
            )))),
        }
    }
}

impl UserData for Builder {
    fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
        methods.add_method_mut("add_sh_step", |_lua, builder, shell: String| {
            builder.add_sh_step(shell);
            Ok(())
        });

        methods.add_method_mut(
            "provide_description",
            |_lua, builder, description: String| {
                builder
                    .provide_description(description)
                    .ok_or(LuaError::external(
                        "Called provide_description on a builder.Builder with a description",
                    ))
            },
        );
    }
}

/// A build step, such as running a shell script or moving a file.
#[derive(Debug, Clone)]
pub enum Step {
    /// Exeuction of a shell script.
    ///
    /// # Errors
    ///
    /// This step can Error if spawning the shell script fails, it will not
    /// (yet) error properly if the shell runs and fails.
    Sh(String),
}

impl Step {
    /// Executes this [`Step`].
    ///
    /// # Errors
    ///
    /// Very contextual to the type of step.
    pub fn execute(&self) -> Result<(), IoError> {
        match self {
            Self::Sh(script) => {
                let _exit = process::Command::new("sh")
                    .arg("-c")
                    .arg(script)
                    .spawn()?
                    .wait()?;
                // TODO: Handle the exit code
                Ok(())
            }
        }
    }
}

/// Collects a Rust [`Vec`] of [`Builder`] outputs from a Lua [table] of Builders.
///
/// > Note: The output is sorted by the `Builder`'s `name` fields.
///
/// [table]: `Table`
pub fn collect_builders_from_table(table: &Table) -> Vec<Builder> {
    fn collect_builder((key, func): (String, Function)) -> Option<Builder> {
        func.call::<Builder>(Builder::with_name(key)).ok()
    }

    // ISSUE: Fails silently on a malformed builder
    let mut res = table
        .pairs::<String, Function>()
        .flatten()
        .filter_map(collect_builder)
        .collect::<Vec<Builder>>();

    res.sort_by(|a, b| a.name.cmp(&b.name));

    res
}