Overwriting case equality method
The big part of my work in gabi is to process the data in form of the Ruby Hash. If some keys are present and have a particular value I have to apply some logic. The trouble here is that the Hash can hold multiple nested Hashes.
I’ve used to apply if
statements with Hash#has_key?
method which as you might guess resulted in a verbose code. Hard to have a good sleep if you know that somewhere in your project there’s a hairy code like that. So I’ve spent some time trying to rectify this situation.
Looking with envy at the Elixir’s pattern matching I wanted to have something similar. A way of specifying how a part of a Hash looks like, without going into details what the values are.
The idea that I came up with is to use the recently covered on my blog the case equality method.
After all the purpose of it is to overwrite it in your own class in order “to provide meaningful semantics in case statements”.
When a value of a Hash is indiffirent I can “match” it against Object
since everything in Ruby is an Object
. Or use the ActiveSupport’s Object.present?
method.
The goal was to make this:
if input["a"] &&
input.fetch("b", {})["c"].present?
#do something
elsif input.fetch("d", {})["e"]&.downcase == "no" ||
input.fetch("d", {})["f"]&.downcase == "yes"
#do something else
elsif input.fetch("g", 0) > 0
#do something completely else
end
Look like this:
case input
when Includes["a" => Object, "b" => {"c" => :present?.to_proc}]
#do something
when Includes["d" => {"e" => /^no$/i}],
Includes["d" => {"f" => /^yes$/i}]
#do something else
when Includes["g" => -> v { v > 0 }]
#do something completely else
end
The implementation that achieves it:
module Includes
def self.[](*args)
arg, *rest = args
if rest.empty?
case arg
when ::Hash
Includes::Hash.new(arg)
when ::Array
Includes::Array.new(arg)
else
Includes::Array.new([arg])
end
else
Includes::Array.new(args)
end
end
class Abstract
def initialize(obj)
@obj = obj
end
def wrap(arg)
case arg
when ::Array
Includes::Array.new(arg)
when ::Hash
Includes::Hash.new(arg)
else
arg
end
end
end
class Hash < Abstract
def ===(other)
case other
when ::Hash, Hash
if @obj.empty?
@obj === other
else
@obj.all? do |(key, value)|
other.has_key?(key) && wrap(value) === other[key]
end
end
else
@obj === other
end
end
end
class Array < Abstract
def ===(other)
case other
when ::Array, Array
if @obj.empty?
@obj === other
else
@obj.all? do |value|
other.any? do |o|
wrap(value) === o
end
end
end
else
@obj === other
end
end
end
end
For sure it’s not a full blown pattern matching library. But it’s small, simple and makes the job done.