lib/toys/errors.rb



# frozen_string_literal: true

module Toys
  ##
  # An exception indicating an error in a tool definition.
  #
  class ToolDefinitionError < ::StandardError
  end

  ##
  # An exception indicating that a tool has no run method.
  #
  class NotRunnableError < ::StandardError
  end

  ##
  # An exception indicating problems parsing arguments.
  #
  class ArgParsingError < ::StandardError
    ##
    # Create an ArgParsingError given a set of error messages
    # @param errors [Array<Toys::ArgParser::UsageError>]
    #
    def initialize(errors)
      @usage_errors = errors
      super(errors.join("\n"))
    end

    ##
    # The individual usage error messages.
    # @return [Array<Toys::ArgParser::UsageError>]
    #
    attr_reader :usage_errors
  end

  ##
  # An exception indicating a problem during tool lookup
  #
  class LoaderError < ::StandardError
  end

  ##
  # A wrapper exception used to provide user-oriented context for an exception
  #
  class ContextualError < ::StandardError
    ## @private
    def initialize(cause, banner,
                   config_path: nil, config_line: nil,
                   tool_name: nil, tool_args: nil)
      super("#{banner} : #{cause.message} (#{cause.class})")
      @cause = cause
      @banner = banner
      @config_path = config_path
      @config_line = config_line
      @tool_name = tool_name
      @tool_args = tool_args
    end

    attr_reader :cause
    attr_reader :banner

    attr_accessor :config_path
    attr_accessor :config_line
    attr_accessor :tool_name
    attr_accessor :tool_args

    class << self
      ## @private
      def capture_path(banner, path, **opts)
        yield
      rescue ContextualError => e
        add_fields_if_missing(e, opts)
        add_config_path_if_missing(e, path)
        raise e
      rescue ::SyntaxError => e
        if (match = /#{::Regexp.escape(path)}:(\d+)/.match(e.message))
          opts = opts.merge(config_path: path, config_line: match[1].to_i)
          e = ContextualError.new(e, banner, **opts)
        end
        raise e
      rescue ::ScriptError, ::StandardError => e
        e = ContextualError.new(e, banner)
        add_fields_if_missing(e, opts)
        add_config_path_if_missing(e, path)
        raise e
      end

      ## @private
      def capture(banner, **opts)
        yield
      rescue ContextualError => e
        add_fields_if_missing(e, opts)
        raise e
      rescue ::ScriptError, ::StandardError => e
        raise ContextualError.new(e, banner, **opts)
      end

      private

      def add_fields_if_missing(error, opts)
        opts.each do |k, v|
          error.send(:"#{k}=", v) if error.send(k).nil?
        end
      end

      def add_config_path_if_missing(error, path)
        if error.config_path.nil? && error.config_line.nil?
          l = (error.cause.backtrace_locations || []).find { |b| b.absolute_path == path }
          if l
            error.config_path = path
            error.config_line = l.lineno
          end
        end
      end
    end
  end
end