Module: NRSER::NicerError

Included in:
AbstractMethodError, ArgumentError, ConflictError, TypeError, Types::CheckError, Types::FromStringError
Defined in:
lib/nrser/errors/nicer_error.rb

Overview

A mixin for Exception and utilities to make life better… even when things go wrong.

“Nicer” errors do a few things:

  1. **‘message` is a splat/`Array`**

    Accept an Array ‘message` instead of just a string, dumping non-string values and joining everything together.

    This lets you deal with printing/dumping all in one place instead of ad-hoc’ing ‘#to_s`, `#inspect`, `#pretty_inspect`, etc. all over the place (though you can still dump values yourself of course since string pass right through).

    Write things like:

    MyError.new "The value", value, "sucks, it should be", expected
    

    This should cut down the amount of typing when raising as well, which is always welcome.

    It also allows for a future where we get smarter about dumping things, offer configuration options, switch on environments (slow, rich dev versus fast, concise prod), etc.

  2. **“Extended” Messages**

    The normal message that we talked about in (1) - that we call the *summary message* or super-message (since it gets passed up to the built-in Exception’s ‘#initialize`) - is intended to be:

    1. Very concise

      • A single line well under 80 characters if possible.

      • This just seems like how Ruby exception messages were meant to be, I guess, and in many situations it’s all you would want or need (production, when it just gets rescued anyways, there’s no one there to read it, etc.).

    2. Cheap to render.

      • We may be trying to do lot very quickly on a production system.

    However - especially when developing - it can be really nice to add considerably more detail and feedback to errors.

    To support this important use case as well, ‘NicerError` introduces the idea of an *extended message* that does not need to be rendered and output along with the summary/super-message.

    It’s rendering is done on-demand, so systems that are not configured to use it will pay a minimal cost for it’s existence.

    > See #extended_message.

    The extended message is composed of:

    1. Text details, optionally rendered via Binding#erb when a binding is provided.

    2. A context of name and value pairs to dump.

    Both are provided as optional keyword parameters to #initialize.

Constant Summary collapse

DEFAULT_COLUMN_WIDTH =

Default column width

78

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.column_widthFixnum

TODO:

Implement terminal width detection like Thor?

Column width to format for (just summary/super-message at the moment).

Returns:

  • (Fixnum)

    Positive integer.



104
105
106
# File 'lib/nrser/errors/nicer_error.rb', line 104

def self.column_width
  DEFAULT_COLUMN_WIDTH
end

Instance Method Details

#add_extended_message?Boolean

TODO:

Just returns ‘true` for now… should be configurable in the future.

Should we add the extended message to #to_s output?

Returns:

  • (Boolean)


293
294
295
# File 'lib/nrser/errors/nicer_error.rb', line 293

def add_extended_message?
  true
end

#contextHash<Symbol, *>

Any additional context values to add to extended messages provided to #initialize.

Returns:



200
201
202
# File 'lib/nrser/errors/nicer_error.rb', line 200

def context
  @context
end

#context_sectionString?

Returns:



245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
# File 'lib/nrser/errors/nicer_error.rb', line 245

def context_section
  lazy_var :@context_section do
    if context.empty?
      nil
    else
      "# Context:\n\n" + context.map { |name, value|
        name_str = name.to_s
        value_str = PP.pp \
          value,
          ''.dup,
          (NRSER::NicerError.column_width - name_str.length - 2)
        
        if value_str.lines.count > 1
          "#{ name_str }:\n\n#{ value_str.indent 4 }\n"
        else
          "#{ name_str }: #{ value_str }\n"
        end
      }.join
    end
  end
end

#default_messageString

Main message to use when none provided to #initialize.

Returns:



190
191
192
# File 'lib/nrser/errors/nicer_error.rb', line 190

def default_message
  "(no message)"
end

#detailsObject



205
206
207
# File 'lib/nrser/errors/nicer_error.rb', line 205

def details
  @details
end

#details_sectionString?

Render details (first time only, then cached) and return the string.

Returns:



214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
# File 'lib/nrser/errors/nicer_error.rb', line 214

def details_section
  lazy_var :@details_section do
    # No details if we have nothing to work with
    if details.nil?
      nil
    else
      contents = case details
      when Proc
        details.call
      when String
        details
      else
        details.to_s
      end
      
      if contents.empty?
        nil
      else
        if @binding
          contents = binding.erb contents
        end
        
        "# Details\n\n" + contents
      end
    end
  end
end

#extended_messageString

Return the extended message, rendering if necessary (cached after first call).

Returns:

  • (String)

    Will be empty if there is no extended message.



274
275
276
277
278
279
280
281
282
283
# File 'lib/nrser/errors/nicer_error.rb', line 274

def extended_message
  @extended_message ||= begin
    sections = []
    
    sections << details_section unless details_section.nil?
    sections << context_section unless context_section.nil?
    
    joined = sections.join "\n\n"
  end
end

#format_message(*message) ⇒ String

Format the main message by converting args to strings and joining them.

Parameters:

  • *message (Array)

    Message segments.

Returns:

  • (String)

    Formatted and joined message ready to pass up to the built-in exception’s ‘#initialize`.



181
182
183
# File 'lib/nrser/errors/nicer_error.rb', line 181

def format_message *message
  message.map( &method( :format_message_segment ) ).join( ' ' )
end

#format_message_segment(segment) ⇒ String

Format a segment of the error message.

Strings are simply returned. Other things are inspected (for now).

Parameters:

  • segment (Object)

    The segment.

Returns:

  • (String)

    The formatted string for the segment.



162
163
164
165
166
167
168
169
# File 'lib/nrser/errors/nicer_error.rb', line 162

def format_message_segment segment
  return segment.to_summary if segment.respond_to?( :to_summary )
  
  return segment if String === segment
  
  # TODO  Do better!
  segment.inspect
end

#initialize(*message, binding: nil, details: nil, **context) ⇒ Object

Construct a nicer error.

Parameters:

  • *message (Array)

    Main message segments.

  • binding: (Binding?) (defaults to: nil)

    When provided any details string will be rendered using it’s Binding#erb method.

  • details: (nil | String | Proc<()=>String> | #to_s) (defaults to: nil)

    Additional text details to add to the extended message. When:

    1. ‘nil` - no details will be added.

    2. ‘String` - the value will be used. If `binding:` is provided, it will be rendered against it as ERB.

    3. ‘Proc<()=>String>` - if and when an extended message is needed the proc will be called, and the resulting string will be used as in (2).

    4. ‘#to_s` - catch all; if and when an extended message is needed `#to_s` will be called on the value and the result will be used as in (2).

  • **context (Hash<Symbol, VALUE>)

    Any additional names and values to dump with an extended message.



137
138
139
140
141
142
143
144
145
146
147
148
149
# File 'lib/nrser/errors/nicer_error.rb', line 137

def initialize  *message,
                binding: nil,
                details: nil,
                **context
  @binding = binding
  @context = context
  @details = details
  
  message = default_message if message.empty?
  super_message = format_message *message
  
  super super_message
end

#to_s(extended: nil) ⇒ String

Note:

This is a bit weird, having to do with what I can tell about the built-in errors and how they handle their message - they have no instance variables, and seem to rely on ‘#to_s` to get the message out of C-land, however that works.

Exception#message just forwards here, so I overrode that with #message to just get the summary/super-message from this method.

Get the message or the extended message.

Parameters:

  • extended: (Boolean?) (defaults to: nil)

    Flag to explicitly control summary/super or extended message:

    1. ‘nil` - call #add_extended_message? to decide (default).

    2. ‘false` - return just the summary/super-message.

    3. ‘true` - always add the *extended message* (unless it’s empty).

Returns:



318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
# File 'lib/nrser/errors/nicer_error.rb', line 318

def to_s extended: nil
  # The way to get the superclass' message
  message = super()
  
  # If `extended` is explicitly `false` then just return that
  return message if extended == false
  
  # Otherwise, see if the extended message was explicitly requested,
  # of if we're configured to provide it as well.
  # 
  # Either way, don't add it it's empty.
  # 
  if  (extended || add_extended_message?) &&
      !extended_message.empty?
    message + "\n\n" + extended_message
  else
    message
  end
end