Class: MotherBrain::ErrorHandler

Inherits:
Object
  • Object
show all
Defined in:
lib/mb/error_handler.rb

Overview

An object to help with the display of errors in a more user-friendly format. An ErrorHandler is created with an error and a set of options to control the display of the error. Some options can be inferred from the error itself. A typical use case would be to wrap an error generated deep in a call stack, and then add data to the error as it bubbles up.

Examples:

Wrapping and raising an error with more data


ErrorHandler.wrap StandardError.new,
  file_path: "/a/b/c.rb",
  method_name: :wat,
  plugin_name: "hi",
  plugin_version: "1.2.3",
  text: "Invalid thing"

# Would raise an error with a message of:

hi (1.2.3)
/a/b/c.rb, on line 1, in 'wat'
Invalid thing

Wrapping an error at multiple points in the call chain


def load_file(path)
  load File.read(path)
rescue => error
  ErrorHandler.wrap error, file_path: path
end

def load(code)
  eval code
rescue => error
  ErrorHandler.wrap error, plugin_name: code.lines.to_a.first
end

def method_missing(method_name, *args, &block)
  ErrorHandler.wrap CodeError.new, method_name: method_name
end

Constant Summary collapse

NEWLINE =
"\n"
SOURCE_RANGE =
5
OPTIONS =
%w[
  backtrace
  file_path
  method_name
  plugin_name
  plugin_version
  text
].map(&:to_sym)

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(error, options = {}) ⇒ ErrorHandler

Returns a new instance of ErrorHandler.

Parameters:

  • error (StandardError)
  • options (Hash) (defaults to: {})

    a customizable set of options

Options Hash (options):

  • backtrace (Array)

    An array of strings containing filenames, line numbers, and method names. Typically comes from ‘caller`.

  • file_path (String)

    The location of a file on disk to display to the user.

  • method_name (Symbol)

    The name of the method or keyword which generated the error.

  • plugin_name (String)

    The name of the plugin the error relates to.

  • plugin_version (String)

    The version of the plugin the error relates to.

  • text (String)

    A custom error message to display to the user.



94
95
96
97
98
99
100
101
102
103
# File 'lib/mb/error_handler.rb', line 94

def initialize(error, options = {})
  error = error.new if error.is_a? Class

  @error = error

  extract_data_from_options options
  extract_data_from_error

  embed_data_in_error
end

Instance Attribute Details

#errorObject (readonly)

Returns the value of attribute error.



58
59
60
# File 'lib/mb/error_handler.rb', line 58

def error
  @error
end

Class Method Details

.wrap(error, options = {}) ⇒ Object

Wraps an error with additional data and raises it.

Raises:

  • (StandardError)

See Also:



51
52
53
54
55
# File 'lib/mb/error_handler.rb', line 51

def wrap(error, options = {})
  error_handler = new(error, options)

  raise error_handler.error, error_handler.message
end

Instance Method Details

#embed_data_in_errorObject

Stores the data in the error and defines getters.



133
134
135
136
137
138
139
140
141
# File 'lib/mb/error_handler.rb', line 133

def embed_data_in_error
  OPTIONS.each do |option|
    data = instance_variable_get "@#{option}"

    if data
      error.instance_variable_set "@_error_handler_#{option}", data
    end
  end
end

#extract_data_from_errorObject

Extracts data from the error and stores it in instance variables. Does not overwrite existing instance variables.



118
119
120
121
122
123
124
125
126
127
128
129
130
# File 'lib/mb/error_handler.rb', line 118

def extract_data_from_error
  OPTIONS.each do |option|
    data = error.instance_variable_get "@_error_handler_#{option}"

    unless instance_variable_get "@#{option}"
      instance_variable_set "@#{option}", data
    end
  end

  @backtrace ||= error.backtrace
  @method_name ||= error.name if error.respond_to? :name
  @text ||= error.message
end

#extract_data_from_options(options) ⇒ Object

Extracts data from an options hash and stores it in instance variables.

Parameters:

  • options (Hash)

See Also:



110
111
112
113
114
# File 'lib/mb/error_handler.rb', line 110

def extract_data_from_options(options)
  OPTIONS.each do |option|
    instance_variable_set "@#{option}", options[option]
  end
end

#file_contentsString

Returns:

  • (String)


158
159
160
161
162
# File 'lib/mb/error_handler.rb', line 158

def file_contents
  return unless file_path and File.exist? file_path

  File.read file_path
end

#file_path_and_line_number_and_method_nameString

Returns:

  • (String)


190
191
192
193
194
195
196
# File 'lib/mb/error_handler.rb', line 190

def file_path_and_line_number_and_method_name
  buffer = []
  buffer << file_path if file_path
  buffer << "on line #{line_number}" if line_number
  buffer << "in '#{method_name}'" if method_name
  buffer.join ", "
end

#line_numberFixnum

Extracts the first line number from the backtrace.

Returns:

  • (Fixnum)


206
207
208
209
210
# File 'lib/mb/error_handler.rb', line 206

def line_number
  return unless backtrace and backtrace[0]

  backtrace[0].split(":")[1].to_i
end

#messageString

Returns:

  • (String)


144
145
146
147
148
149
150
151
152
153
154
155
# File 'lib/mb/error_handler.rb', line 144

def message
  result = [
    plugin_name_and_plugin_version,
    file_path_and_line_number_and_method_name,
    text,
    relevant_source_lines
  ].compact.join NEWLINE

  result << NEWLINE unless result.end_with? NEWLINE

  result
end

#numbered_source_linesArray

Returns:

  • (Array)


174
175
176
177
178
179
180
181
182
183
184
185
186
187
# File 'lib/mb/error_handler.rb', line 174

def numbered_source_lines
  lines = file_contents.lines.to_a.map(&:rstrip)
  rjust_size = lines.count.to_s.length

  result = []

  lines.each_with_index do |line, index|
    current_line_number = index + 1

    result << "#{current_line_number.to_s.rjust rjust_size}#{line_number == current_line_number ? '>>' : ': '}#{line}"
  end

  result
end

#plugin_name_and_plugin_versionString

Returns:

  • (String)


199
200
201
# File 'lib/mb/error_handler.rb', line 199

def plugin_name_and_plugin_version
  "#{plugin_name} (#{plugin_version})" if plugin_name and plugin_version
end

#relevant_source_linesString

Returns:

  • (String)


165
166
167
168
169
170
171
# File 'lib/mb/error_handler.rb', line 165

def relevant_source_lines
  return unless file_contents and line_number

  beginning = line_number - (SOURCE_RANGE / 2) - 1
  beginning = [beginning, 0].max
  numbered_source_lines[beginning, SOURCE_RANGE].join NEWLINE
end