workflow 0.3.0

Execute complex workflows using simple definition files.
Documentation
use std::error::Error;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::{env, process};

use clap::{App, AppSettings, Arg, SubCommand};
use log::LevelFilter;
use simple_logger::SimpleLogger;

use workflow::{Step, Workflow, WorkflowExecutor};

fn main() {
    let matches = App::new("Workflow")
        .version(clap::crate_version!())
        .author(clap::crate_authors!())
        .about(clap::crate_description!())
        .setting(AppSettings::ArgRequiredElseHelp)
        .arg(
            Arg::with_name("log-level")
                .long("log-level")
                .help("Sets the logging level")
                .default_value("info"),
        )
        .subcommand(
            SubCommand::with_name("execute")
                .alias("exec")
                .alias("e")
                .about("Execute given workflow")
                .arg(
                    Arg::with_name("FILE")
                        .help("Sets the workflow file to use")
                        .required(true),
                ),
        )
        .subcommand(
            SubCommand::with_name("validate")
                .alias("v")
                .about("Validate given workflow")
                .arg(
                    Arg::with_name("FILE")
                        .help("Sets the workflow file to use")
                        .required(true),
                ),
        )
        .subcommand(
            SubCommand::with_name("generate")
                .alias("g")
                .about("Generate workflow file"),
        )
        .get_matches();

    configure_logging(matches.value_of("log-level").unwrap()).expect("unable to configure logging");

    if let Some(args) = matches.subcommand_matches("execute") {
        let file = args.value_of("FILE").unwrap();
        match execute_workflow(file) {
            Ok(_) => {}
            Err(e) => {
                log::error!("{}", e);
                process::exit(1);
            }
        }
    } else if let Some(args) = matches.subcommand_matches("validate") {
        let file = args.value_of("FILE").unwrap();
        match validate_workflow(file) {
            Ok(w) => log::info!("Workflow `{}` is valid", w.id),
            Err(e) => {
                log::error!("{}", e);
                process::exit(1);
            }
        }
    } else if matches.subcommand_matches("generate").is_some() {
        match generate_workflow() {
            Ok(w) => {
                let content = serde_yaml::to_string(&w).unwrap();
                println!("{}", content);
            }
            Err(e) => {
                log::error!("{}", e);
                process::exit(1);
            }
        }
    }
}

/// Execute given workflow
fn execute_workflow<P: AsRef<Path>>(path: P) -> Result<(), Box<dyn Error>> {
    let cwd = env::current_dir()?;
    let path = get_workflow_path(path)?;

    let workflow = Workflow::from_file(path)?;
    let executor = WorkflowExecutor::with_cache(cwd.join("workflows"))?;

    executor.execute(&workflow)
}

/// Validate given workflow
fn validate_workflow<P: AsRef<Path>>(path: P) -> Result<Workflow, Box<dyn Error>> {
    let path = get_workflow_path(path)?;

    Workflow::from_file(path)
}

/// Generate sample workflow file
fn generate_workflow() -> Result<Workflow, Box<dyn Error>> {
    let step = Step {
        name: None,
        uses: None,
        exec: Some("echo Hello, world.".to_string()),
        with: None,
    };

    Ok(Workflow {
        id: "hello-world".to_string(),
        name: Some("Hello world".to_string()),
        author: None,
        description: Some("Simple workflow that print `Hello, world.`".to_string()),
        steps: vec![step],
    })
}

fn get_workflow_path<P: AsRef<Path>>(path: P) -> Result<PathBuf, Box<dyn Error>> {
    let cwd = env::current_dir()?;

    if path.as_ref().starts_with(".") {
        // relative path
        Ok(cwd.join(path))
    } else {
        Ok(path.as_ref().to_path_buf())
    }
}

/// Configure the application logging level
/// this will try to resolve given level into a
/// `LevelFilter` type, falling back to `LevelFilter::Debug`
/// if something goes wrong.
///
/// ```rust
/// configure_logging("info").expect("unable to configure logging")
/// ```
fn configure_logging(level: &str) -> Result<(), Box<dyn Error>> {
    SimpleLogger::new()
        .with_level(LevelFilter::from_str(level).unwrap_or(LevelFilter::Debug))
        .init()
        .map_err(|v| v.into())
}