tarantool-runner 0.1.0

CLI tool to execute tarantool Rust applications and build other tarantool-oriented utilities.
Documentation
#![doc = include_str!("../README.md")]
use std::{
    fs::{create_dir, File},
    io::{Read, Write},
    path::{Path, PathBuf},
    process::Command as SysCommand,
};

use anyhow::{bail, Context};
use clap::{Args, Parser, Subcommand};
use tempfile::tempdir;

pub const DEFAULT_LUA_INIT: &str = include_str!("default_init.lua");

/// An application that `tarantool-runner` runs(if used as a binary - see `README`) and exports for reuse.
#[derive(Parser)]
#[command(author, version, about, long_about = None)]
#[command(propagate_version = true)]
pub struct Cli {
    #[command(subcommand)]
    command: Command,
}

impl Cli {
    /// Processes parsed command.
    ///
    /// Could fail only in case the underlying command fails.
    pub fn process_command(self) -> anyhow::Result<()> {
        match self.command {
            Command::Run(args) => process_run_command(args),
        }
    }
}

#[derive(Subcommand, Clone)]
enum Command {
    Run(RunArgs),
}

#[derive(Args, Clone)]
struct RunArgs {
    #[arg(
        short,
        long,
        help = r#"A path to the package you want to run. Package must export specified entrypoint. Example: "/path/to/package.so""#,
        value_name = "FILE"
    )]
    path: PathBuf,

    #[arg(
        short,
        long,
        help = r#"An entrypoint you want to execute. Package must export function with the same name you specify here. Example: "some_entrypoint""#
    )]
    entrypoint: String,

    #[arg(
        short,
        long,
        help = r#"An initializing script that tarantool-runner would use instead of built-in script. Use it whenever you need to override specific options, like use vinyl instead of memtx."#
    )]
    init_script: Option<PathBuf>,

    #[arg(
        last = true,
        help = r#"An OPTIONAL input which is propagated to the executed entrypoint. It is given to your entrypoint in string format."#
    )]
    input: Option<String>,
}

impl RunArgs {
    /// Returns content of the init script file, supplied by user.
    fn init_script_content(&self) -> anyhow::Result<Option<String>> {
        self.init_script
            .as_ref()
            .map(|file| {
                let mut file = File::open(file)?;
                let mut result = String::new();
                file.read_to_string(&mut result)?;
                Ok(result)
            })
            .transpose()
    }
}

fn process_run_command(args: RunArgs) -> anyhow::Result<()> {
    let base_dir =
        tempdir().with_context(|| "failed to create base dir for storing runtime data")?;

    let current_dir = Path::new(".");
    let package_location = args.path.parent().unwrap_or(current_dir);

    let package_name = args
        .path
        .file_stem()
        .with_context(|| "path is malformed - it doesn't points to a file")?;

    let tnt_init_file = base_dir.path().join("init.lua");
    let tnt_tmpdir = base_dir.path().join("tmp");

    let maybe_init_script = args.init_script_content()?;
    let init_content = maybe_init_script.as_deref().unwrap_or(DEFAULT_LUA_INIT);

    let mut file = File::create(&tnt_init_file)?;
    file.write_all(init_content.as_bytes())?;

    create_dir(tnt_tmpdir.as_path())
        .with_context(|| "failed to create tmpdir for tarantool runtime")?;

    let status = SysCommand::new("tarantool")
        .arg(tnt_init_file)
        // Fullpath points literally to the given package.
        .env("TARANTOOL_RUNNER_PACKAGE_FULLPATH", &args.path)
        // Package name without extension - supplied so it can be used with require.
        .env("TARANTOOL_RUNNER_PACKAGE_NAME", package_name)
        // Package location is the dir where package could be found.
        .env("TARANTOOL_RUNNER_PACKAGE_LOCATION", package_location)
        // Package entrypoint is the function user would like to execute.
        .env("TARANTOOL_RUNNER_PACKAGE_ENTRYPOINT", &args.entrypoint)
        // Base dir is the runtime directory of `tarantool-runner`. There goes initialization scripts, package symlink, etc.
        .env("TARANTOOL_RUNNER_BASEDIR", base_dir.path())
        // Tmpdir is the directory tarantool instance must use to store its temporary files.
        .env("TARANTOOL_RUNNER_TMPDIR", tnt_tmpdir)
        // An input to the entrypoint.
        .env(
            "TARANTOOL_RUNNER_INPUT",
            args.input.as_deref().unwrap_or(""),
        )
        .status()
        .with_context(|| "failed to get status code from tarantool process")?;

    if !status.success() {
        bail!("exit code of tarantool process is not success: {status:?}")
    }

    Ok(())
}