tlopo-ruby/tlopo-cli

View on GitHub
lib/tlopo/cli.rb

Summary

Maintainability
A
25 mins
Test Coverage
require 'tlopo/cli/version'
require 'yaml'
require 'optparse'

module Tlopo
  # A library to speedup CLI apps development
  class Cli
    def initialize(opts = {})
      msg = 'Either config or config_file must be specified'
      raise msg unless opts[:config_file] || opts[:config]
      raise msg if opts[:config_file] && opts[:config]
      @cfg = YAML.safe_load(File.read(opts[:config_file])) if opts[:config_file]
      @cfg = opts[:config] if opts[:config]
      @globals = @cfg[:globals]
      @usage = @cfg[:usage]
      @cfg.delete(:globals)
      @cfg.delete(:usage)
    end

    def run
      validate_recursive
      parse_recursive(@cfg, {})
    end

    private

    def validate_recursive(obj = @cfg)
      validate_command(obj)
      obj['switches'].each { |sw| validate_switch(sw) } if obj['switches']
      obj['subcommands'].each { |sub| validate_recursive(sub) } if obj['subcommands']
    end

    def parse_options(obj, result, key)
      return nil if obj.nil?
      result[key] = {} unless result[key]
      result[key]['class'] = obj['class']
      op = OptionParser.new do |opts|
        opts.banner = obj['banner']
        if obj['switches']
          obj['switches'].each do |sw|
            short = sw['short']
            long = sw['long']
            desc = sw['desc']
            opts.on(short, long, desc) { |v| result[key][sw['name']] = v }
          end
        end
      end
      result[key]['_usage'] = op.to_s if @usage
      op
    end

    def parse_recursive(obj, result, stack = [])
      stack << obj['name']
      key = stack.join('::')
      _p = parse_options(obj, result, key)
      _p.order! unless _p.nil?
      subcommand = ARGV.shift
      return invoke_class_run(result, key) if subcommand.nil?
      command = obj['subcommands'].find { |e| e['name'] == subcommand } if obj['subcommands']
      raise "Unknown subcommand '#{subcommand}' for [ #{stack.join(' -> ')} ]" unless command
      parse_recursive(command, result, stack)
    end

    def invoke_class_run(result, key)
      _class = result[key]['class']
      opts = result[key]
      opts.delete('class')
      result.delete(key)
      opts['_globals'] = result if @globals
      Object.const_get(_class).run(opts)
    end

    def validate_command(command)
      required = %w[name banner class]
      optional = %w[switches subcommands]
      required.each do |e|
        raise "Field '#{e}' is required. received: [#{command.inspect}]" unless command[e]
      end
      known = required + optional
      command.each_key do |k|
        raise "Unknown field '#{k}', known values #{known.inspect}" unless known.include?(k)
      end
      begin
        Object.const_get(command['class'])
      rescue NameError
        raise "Can't find class #{command['class']}, make sure it's loaded."
      end
    end

    def validate_switch(switch)
      required = %w[name long desc]
      optional = ['short']
      required.each do |e|
        raise "Field '#{e}' is required. received: [#{switch.inspect}]" unless switch[e]
      end
      known = required + optional
      switch.each_key do |k|
        raise "Unknown field '#{k}', known values #{known.inspect}" unless known.include?(k)
      end
    end
  end
end