Ruby's exception to message mapper

| · @kyrylosilin · bluesky:@kyrylo.org

Exception to message mapper is a Ruby library bundled with the standard Ruby library distribution since Ruby 1.1 (released in the 20th century). It was written by Keiju ISHITSUKA and apparently meant to be used internally, to simplify Ruby’s stdlib source code. But since it’s available in the stdlib, it’s also available for us. You may be wondering what this library is about. Well, guess what?.. It maps messages to exceptions!

Why would you use it

Probably you won’t use it anyway (and I understand why), but let’s imagine a somewhat far-fetched example. When you define a custom exception it usually doesn’t have any context. For example, ArgumentError doesn’t tell a lot on its own. If you want to use the exception (that is, you raise it), you usually write an accompanying message that explains why this exception is being raised. By doing this you connect the explanation to the exception, which forms 1 logical unit. If your class often raises the same (or almost the same exception), if the exception messages are long, they may clutter the code, because you constantly “connect” explanations. Ever had such problems? It makes sense to refactor the messy code. An exception to message mapper comes to the rescue.

Say you have Validator, the ultimate validator class of the new generation that validates everyone and everything. It’s simple, but not simple-minded. The validator dislikes and rejects integer 0, string "password" and sneaky SQL queries like "SELECT * FROM users;". If it sees something like that, it whines and raises an exception. Otherwise it returns true.

class Validator
  class ValidatorError < ArgumentError; end

  def self.validate(datum)
     case datum
     when 0
        raise ValidatorError, "meh, no-one uses zeros in 2014"
     when "password"
       if Time.now.year == 2015
         raise ValidatorError, "meh, no-one uses passwords in 2015"
       else
         true
       end
     when /SELECT \* FROM .+;/i
       raise ValidatorError, "cheating is not allowed"
     else
       true
     end
  end
end

As you can see there’s a problem with this class: it raises the same exception with almost the same exception message. Let’s solve the problem by mapping a predefined message to Validator::ValidatorError to remove duplication. No need to reinvent the wheel, we have a handy tool in our tool belt.

Example usage

The e2mmap library implements an exception to message mapper. Behind the scenes the implementation uses really old school Ruby with for loops and whistles but that’s none of our concern. Firstly, require e2mmap, which defines the Exception2MessageMapper class.

require 'e2mmap' # From stdlib.

Next, let’s define a separate module that maps messages to exceptions. It’s worth mentioning that you can bake this straight into the Validator’s body.

class Validator
  module ExceptionsForValidator
    extend Exception2MessageMapper
    def_exception :ValidatorError, "meh, no-one uses %s in %s", ArgumentError
  end
end

Here we extended ExceptionsForValidator with e2mmap and defined a new exception class (ValidatorError) with a message attached to it. The message is "meh, no-one uses %s in %s". The ArgumentError argument is the superclass of ValidatorError.

Now we can use the module in the Validator class. We also need to change the implementation of the .validate method slightly. In places where we want to reuse our exception message raise becomes Raise. Raise is just a method defined on self. It accepts the next arguments: an error class to be raised, the arguments to substitute for %s’s in the message.

class Validator
  include ValidatorErrors

  def self.validate(datum)
     case datum
     when 0 then Raise ValidatorError, "zeros", 2014
     when "password"
       if Time.now.year == 2015
         Raise ValidatorError, "passwords", 2015
       else
         true
       end
     when /SELECT \* FROM .+;/i
       raise ValidatorError, "cheating is not allowed"
     else
       true
     end
  end
end

e2mmap does not restrict you to one message. The SQL query example shows that you still can use plain old raise with the exception defined via e2mmap and pass an arbitrary message.

More features

If you prefer to use fail instead of raise, e2mmap includes an alias for Raise, which is called Fail. You can also use fail (e2mmap overrides the default fail, so I do not recommend to use it in order to avoid confusion).

If you want to use a predefined message for an existing exception, you can use the def_e2message method (instead of def_exception).

require 'e2mmap'

class Validator
  extend Exception2MessageMapper
  def_e2message ArgumentError, "sorry, wrong argument"

  def self.testRaise
    Raise ArgumentError
    # or Fail ArgumentError
    # or fail ArgumentError (not recommended)
  end
end

Validator.testRaise #=> ArgumentError: "sorry, wrong argument"

Where does Ruby use e2mmap internally? Well, for example, IRB heavily uses it. Other noticeable parts of Ruby’s stdlib that use e2mmap are Matrix and Shell.

Conclusion

I’ve never used this library myself (and never seen it being used in open source projects), and I didn’t find any mentions of this library on the internet. Hence, someone had to write about it. For additional reference you can read the source code of the library. Also, this is probably the last Ruby article of 2014.

Happy New Year!

← 🇺🇸 Internship in San Francisco · Home · A bit of bacon, lettuce and... →