I’ve really been enjoying Ruby’s destructuring syntax for Hashes. So much so that I dug in a little further and figured out how to destructure pretty much everything in Ruby.

First off, if you aren’t familiar with Ruby destructure syntax here’s a quick refresher:

my_hash = { first: "the first", second: "not first"}
my_hash => { first: }

# First is now a variable
first #=> "the first"

Now let’s say you have an Object that you only want to pull certain attributes out of. Additionally, you want to set these to temporary variables. Below I’ve outlined how this would typically be accomplished.

class MyClass
  attr_reader :name

  def initialize
    @name = "deconstruct"
  end
end

new_object = MyClass.new
name = new_object.name #=> "deconstruct"

If we try to use the destructuring syntax on the object, the error message gives us a clue as to how to make our object destructurable.

new_object = MyClass.new
new_object => { name: }
#=> `#<MyClass:0x00007f0e8823ae68 @name="hi"> does not respond to #deconstruct_keys (NoMatchingPatternError)

Our class does not respond to #deconstruct_keys, so let’s implement it.

  def deconstruct_keys(keys)
    keys.each_with_object({}) do |key, hash|
      hash[key] = self.public_send(key)
    end
  end

What the above is doing, is building up a useful Hash for the object to respond with. This relies on the object having a least a public method for the incoming key (e.g. attr_reader :name). This fulfills the contract that destructuring expects when you use the => syntax.

Now the above is just the basic implementation. If you need more specialization you can add conditional logic for example to combine a value that contains an Array with #join. Additionally, you can allowlist only certain keys for destructuring.

ALLOWED_KEYS = [:name, :email]

def deconstruct_keys(keys)
  keys.each_with_object({}) do |key, hash|
    next unless ALLOWED_KEYS.include?(key) # Allowlist for keys

    value = self.public_send(key)
    value = value.join if value.respond_to?(:join) # For Array values
    hash[key] = value
  end
end

The final result could look like the following. Even better as a module mixin.

class MyClass
  attr_reader :name

  def initialize
    @name = "deconstruct"
  end

  def deconstruct_keys(keys)
    keys.each_with_object({}) do |key, hash|
      hash[key] = self.public_send(key)
    end
  end
end

########################
# Or as a module mixin #
########################
module Deconstructable
  extend ActiveSupport::Concern

  included do
    def deconstruct_keys(keys)
      keys.each_with_object({}) do |key, hash|
        hash[key] = self.public_send(key)
      end
    end
  end
end

class MyClass
  include Deconstructable

  attr_reader :name

  def initialize
    @name = "deconstruct"
  end
end

And here’s the usage:

new_object = MyClass.new
new_object => { name: }
name #=> "deconstruct"

That’s it! Go destructure the world!

Previous

Assignment destructuring; and my other favorite Ruby 3 features

Next

Well-factored Data Migrations

Join the conversation

comments powered by Disqus