Skip to content

Commit

Permalink
Post: Destructure the world in Ruby
Browse files Browse the repository at this point in the history
  • Loading branch information
joshmfrankel committed May 29, 2024
1 parent 09d22e4 commit 55e4a80
Showing 1 changed file with 134 additions and 0 deletions.
134 changes: 134 additions & 0 deletions _posts/2024/2024-05-29-destructuring-the-world-in-ruby.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
---
layout: post
title: Destructuring the World (and any Object) in Ruby
categories:
- articles
tags:
- ruby
---

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.
<!--excerpt-->

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

``` ruby
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.

``` ruby
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.

``` ruby
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.

``` ruby
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.

``` ruby
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.

``` ruby
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:

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

That's it! Go destructure the world!

0 comments on commit 55e4a80

Please sign in to comment.