subcomponent 0.1.0

A components orchestrator
/*
 * Copyright (c) 2016-2017 Jean Guyomarc'h
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 */

extern crate getopts;
extern crate std;
extern crate term;

macro_rules! foreach_cmd {
    ($mac:ident) => {
        $mac!(template);
        $mac!(fetch);
        $mac!(status);
    }
}

macro_rules! declare_mod {
    ($name:ident) => { pub mod $name; }
}

foreach_cmd!(declare_mod);


use std::error::Error as FmtError;
use self::Error::*;
use subprocess;
use compiler;
use fetcher;


pub trait Errno {
    fn errno(&self) -> Option<i32>;
}

#[derive(Debug)]
pub enum Error {
    CmdGetoptsError(getopts::Fail),
    CmdInvalid,
    CmdIOError(std::io::Error),
    CompileError(compiler::Error),
    FetchError(fetcher::Error),
    NoSubcomponentFile,
    TermError(term::Error),
    ProcessError(subprocess::Error),
    InvalidComponentName,
    InvalidHookName,
}

pub fn get_commands_list() -> Vec<&'static str> {
   let mut list = Vec::new();

   macro_rules! register_cmd {
      ($name:ident) => {
         list.push(stringify!($name))
      }
   }
   foreach_cmd!(register_cmd);

   list
}

pub trait Cmd {
    fn help(&self, opts: &getopts::Options);
    fn getopts_set(&self, opts: &mut getopts::Options);
    fn getopts_matches_use(&mut self, matches: &getopts::Matches);
    fn run(&self, parser_arg: &Option<compiler::parser::Parser>) -> Result<(), Error>;
}

impl Errno for Error {
    fn errno(&self) -> Option<i32> {
       /* clippy says (rightfully) that arms yielding the same value should be
        * combined toghether. I'm not happy with that, and for this particular
        * case, I prefer to keep them separated.
        */
       #[cfg_attr(feature = "cargo-clippy", allow(match_same_arms))]
        match *self {
            CmdInvalid => Some(127),
            CmdIOError(ref err) => err.raw_os_error(),
            CmdGetoptsError(_) => Some(1),
            CompileError(_) => Some(1),
            FetchError(_) => Some(1),
            NoSubcomponentFile => Some(2),
            TermError(_) => Some(10),
            ProcessError(_) => Some(1),
            InvalidComponentName => Some(1),
            InvalidHookName => Some(1),
        }
    }
}

impl From<std::io::Error> for Error {
    fn from(error: std::io::Error) -> Self {
        CmdIOError(error)
    }
}

impl From<subprocess::Error> for Error {
   fn from(error: subprocess::Error) -> Self {
      ProcessError(error)
   }
}

impl From<term::Error> for Error {
    fn from(error: term::Error) -> Self {
        TermError(error)
    }
}

impl From<getopts::Fail> for Error {
    fn from(error: getopts::Fail) -> Self {
        CmdGetoptsError(error)
    }
}

impl From<compiler::Error> for Error {
    fn from(error: compiler::Error) -> Self {
        CompileError(error)
    }
}

impl From<fetcher::Error> for Error {
    fn from(error: fetcher::Error) -> Self {
        FetchError(error)
    }
}

impl std::fmt::Display for Error {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(f, "{}", self.description())
    }
}

impl std::error::Error for Error {
    fn description(&self) -> &str {
        match *self {
            CmdGetoptsError(ref err) => err.description(),
            CmdIOError(ref err) => err.description(),
            CmdInvalid => "Invalid command",
            CompileError(ref err) => err.description(),
            FetchError(ref err) => err.description(),
            NoSubcomponentFile => "No subcomponent file was found",
            TermError(ref err) => err.description(),
            ProcessError(ref err) => err.description(),
            InvalidComponentName => "Invalid component name",
            InvalidHookName => "Invalid hook name",
        }
    }
}

fn command_get(name: &str) -> Option<Box<Cmd>>
{
    macro_rules! match_cmd {
        ($name:ident) => {
            if name == stringify!($name) {
                return Some(Box::new($name::new()));
            }
        }
    }
    foreach_cmd!(match_cmd);
    None
}

fn hook_for_all_components_get(hook_name: &str, parser: &compiler::parser::Parser) -> Option<Vec<Box<Cmd>>> {
   let mut hooks: Vec<Box<Cmd>> = Vec::new();

   for component in parser.components_get().iter() {
      for hook in component.borrow().hooks_get().iter() {
         if hook_name == hook.name_get() {
            hooks.push(Box::new((*hook).clone()));
            break;
         }
      }
   }

   if hooks.is_empty() {
      None
   } else {
      Some(hooks)
   }
}

fn hook_for_named_components_get(hook_name: &str,
                                 components: &[String],
                                 parser: &compiler::parser::Parser)
   -> Result<Vec<Box<Cmd>>, Error>
{
   let mut hooks: Vec<Box<Cmd>> = Vec::new();

   /* Go through the components that we seek */
   for component_name in components.iter() {
      let mut is_found = false;

      /* And now in the components available */
      for component in parser.components_get().iter() {
         let comp = component.borrow();
         /* Yep, found a component */
         if component_name == comp.id_get() {
            is_found = true;

            /* Select the hook with the right name */
            let mut hook_found = false;
            for hook in comp.hooks_get().iter() {
               if hook_name == hook.name_get() {
                  hook_found = true;
                  hooks.push(Box::new((*hook).clone()));
                  break;
               }
            }
            if ! hook_found {
               error!("Failed to find hook '{}' for component '{}'",
                      hook_name, component_name);
               return Err(Error::InvalidHookName);
            }
            break;
         }
      }

      /* Component was not found, that's an error! */
      if ! is_found {
         error!("Failed to find component '{}'", component_name);
         return Err(Error::InvalidComponentName);
      }
   }

   Ok(hooks)
}

fn hook_commands_get(name: &str, components: &[String], parser_opt: &Option<compiler::parser::Parser>)
   -> Result< Option<Vec<Box<Cmd>>>, Error >
{
    /*
     * If we have a valid parser provided, then we can search for hooks defined
     * by components. If a component has defined the hook corresponding to the
     * command we are searching for, return the hook as the command.
     */
    if let Some(ref parser) = *parser_opt {
       if components.is_empty() {
          return Ok(hook_for_all_components_get(name, parser));
       } else {
          /* Convert Result<data> into Result<Option<data>> */
          let ret = hook_for_named_components_get(name, components, parser);
          return ret.map(Some);
       }
    }
    Ok(None)
}

#[cfg_attr(feature = "cargo-clippy", allow(borrowed_box))]
fn handle_command(args: &[String], cmd: &mut Box<Cmd>) -> Result<bool, Error> {
   let mut opts = getopts::Options::new();

   /* Common help option for all commands */
   opts.optflag("h", "help", "Display this message");

   /* Allow commands to provide their own options before parsing */
   cmd.getopts_set(&mut opts);
   let matches = opts.parse(&args[1..])?;

    /* If we end up with the --help option, print the help, and we are done */
    if matches.opt_present("help") {
        cmd.help(&opts);
        return Ok(false);
    }

    cmd.getopts_matches_use(&matches);
    Ok(true)
}

pub fn getopts(args: &[String], parser_opt: &Option<compiler::parser::Parser>) -> Result<Option<Vec<Box<Cmd>>>, Error> {
   let cmd_name = args[0].as_str();
   let components = &args[1..];
   if let Some(mut cmd) = command_get(cmd_name) {
      if handle_command(args, &mut cmd)? {
         Ok(Some(vec![cmd]))
      } else  {
         Ok(None)
      }
   } else if let Some(mut hooks) = hook_commands_get(cmd_name, components, parser_opt)? {
      if handle_command(args, &mut hooks[0])? {
         Ok(Some(hooks))
      } else {
         Ok(None)
      }
   } else {
      error!("Command '{}' is not defined.", cmd_name);
      Err(CmdInvalid)
   }
}