Skip to content

Instantly share code, notes, and snippets.

@prschmid
Created January 26, 2017 14:54
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save prschmid/07a83baa42a37ed18e06519618621511 to your computer and use it in GitHub Desktop.
Save prschmid/07a83baa42a37ed18e06519618621511 to your computer and use it in GitHub Desktop.
Ruby method to enable "sending" nested calls to an object
class Object
# Retrieve the value of a deeply nested attribute
#
# Example usage
#
# attribute = "data.foo['bar'].id"
# value = obj.send_nested(attribute)
#
# Under the hood this will do something akin to
# obj.send(data).send(foo)['bar'].send(id)
#
# This also works with symbols in the attribute string
#
# attribute = "data.foo[:bar].id"
# value = obj.send_nested(attribute)
#
# Under the hood this will do something akin to
# obj.send(data).send(foo)['bar'].send(id)
#
#
# Parameters
# attribute: The (nested) attribute to query for
# with_indifferent_access: If this is true, then this will convert any
# hashes to HashWithIndifferentAccess. As such,
# anything that was a symbol before will become a
# string.
# Returns whatever the attribute evaluates to with the major caveat is that
# if `with_indifferent_access` is used, any resulting hashes will have string
# keys even if they were symbols to begin with as this is how the ruby method
# hash.with_indifferent_access works
def send_nested(attribute, with_indifferent_access: false)
# Split in to the attribute and the deeper nested things
# E.g. this will split "data.foo['bar'].id" in to
# data
# .foo['bar'].id
matches = /([^\.\[\]]+)(.*)/.match(attribute)
attr = matches.captures[0]
obj = send(attr)
until matches.captures[1].empty?
next_part = matches.captures[1]
kind = next_part.slice!(0)
# This means to use a nested send
if kind == '.'
matches = /([^\.\[\]]+)(.*)/.match(next_part)
attr = matches.captures[0]
obj = obj.send(attr)
# This means we want to access something by a key or index
elsif kind == '['
matches = /(['|"|:]*[^\]]+['|"]*)\](.*)/.match(next_part)
attr = matches.captures[0]
# Figure out if the attribute is a string, symbol, or an integer index
# and do the right thing to convert it to the attribute we care about
if attr.starts_with?(':')
attr.slice!(0)
attr = attr.to_sym
elsif attr.starts_with?("'")
unless attr.ends_with?("'")
raise ArgumentError, "Hash key starts with a ' but does not end with one: #{attr}"
end
attr.slice!(0)
attr.chop!
elsif attr.starts_with?('"')
unless attr.ends_with?('"')
raise ArgumentError, "Hash key starts with a \" but does not end with one: #{attr}"
end
attr.slice!(0)
attr.chop!
elsif attr.to_i.to_s == attr
attr = attr.to_i
end
if with_indifferent_access && obj.is_a?(Hash)
obj = obj.with_indifferent_access
end
obj = obj[attr]
else
raise ArgumentError, "Could not parse attribute #{attribute}"
end
end
obj
end
end
require 'test_helper'
require 'send_nested'
class DummyObject
def simple_attr
'foo'
end
def attr_with_hash
{ foo: {
bar: 'baz'
} }
end
def attr_with_array
[%w(a b), %w(c d)]
end
def attr_with_hash_and_array
{
foo: [
{ bar: 'baz' },
{ bam: 'splat' }
]
}
end
def attr_with_nested_object
NestedDummyObject.new
end
end
class NestedDummyObject
def nested_attr_with_hash
{ foo: {
bar: 'baz'
} }
end
end
class NestedSendTest < ActiveSupport::TestCase
setup do
@obj = DummyObject.new
end
test 'can get simple attr' do
assert_equal 'foo', @obj.send_nested('simple_attr')
end
test 'without indifferent access is the default' do
h = { bar: 'baz' }
assert_equal h, @obj.send_nested('attr_with_hash[:foo]')
assert_nil @obj.send_nested('attr_with_hash["foo"]')
end
test 'can access hash with nested keys with indifferent access' do
h = { 'bar' => 'baz' }
assert_equal h, @obj.send_nested('attr_with_hash["foo"]', with_indifferent_access: true)
assert_equal h, @obj.send_nested("attr_with_hash['foo']", with_indifferent_access: true)
assert_equal h, @obj.send_nested('attr_with_hash[:foo]', with_indifferent_access: true)
assert_equal 'baz', @obj.send_nested('attr_with_hash["foo"]["bar"]', with_indifferent_access: true)
assert_equal 'baz', @obj.send_nested("attr_with_hash['foo']['bar']", with_indifferent_access: true)
assert_equal 'baz', @obj.send_nested('attr_with_hash[:foo]["bar"]', with_indifferent_access: true)
assert_equal 'baz', @obj.send_nested('attr_with_hash[:foo][:bar]', with_indifferent_access: true)
end
test 'can access hash with nested keys without indifferent access' do
h = { bar: 'baz' }
assert_equal h, @obj.send_nested('attr_with_hash[:foo]', with_indifferent_access: false)
assert_equal 'baz', @obj.send_nested('attr_with_hash[:foo][:bar]', with_indifferent_access: false)
assert_nil @obj.send_nested('attr_with_hash["foo"]', with_indifferent_access: false)
assert_nil @obj.send_nested("attr_with_hash['foo']", with_indifferent_access: false)
end
test 'can access array with nested indices' do
a = %w(a b)
assert_equal a, @obj.send_nested('attr_with_array[0]')
assert_equal 'b', @obj.send_nested('attr_with_array[0][1]')
# Should not affect an array
assert_equal a, @obj.send_nested('attr_with_array[0]', with_indifferent_access: true)
assert_equal 'b', @obj.send_nested('attr_with_array[0][1]', with_indifferent_access: true)
end
test 'can access hash and array with nested keys and indices' do
h = [{ bar: 'baz' }, { bam: 'splat' }]
assert_equal h, @obj.send_nested('attr_with_hash_and_array[:foo]')
h = [{ 'bar' => 'baz' }, { 'bam' => 'splat' }]
assert_equal h, @obj.send_nested('attr_with_hash_and_array["foo"]', with_indifferent_access: true)
h = { bar: 'baz' }
assert_equal h, @obj.send_nested('attr_with_hash_and_array[:foo][0]')
h = { 'bar' => 'baz' }
assert_equal h, @obj.send_nested('attr_with_hash_and_array["foo"][0]', with_indifferent_access: true)
assert_equal 'baz', @obj.send_nested('attr_with_hash_and_array[:foo][0][:bar]')
assert_equal 'baz', @obj.send_nested('attr_with_hash_and_array[:foo][0][:bar]', with_indifferent_access: true)
assert_equal 'baz', @obj.send_nested('attr_with_hash_and_array["foo"][0]["bar"]', with_indifferent_access: true)
assert_equal 'baz', @obj.send_nested('attr_with_hash_and_array[:foo][0]["bar"]', with_indifferent_access: true)
assert_equal 'baz', @obj.send_nested('attr_with_hash_and_array["foo"][0][:bar]', with_indifferent_access: true)
end
test 'can get nested object' do
h = { foo: { bar: 'baz' } }
assert_equal h, @obj.send_nested('attr_with_nested_object.nested_attr_with_hash')
h = { bar: 'baz' }
assert_equal h, @obj.send_nested('attr_with_nested_object.nested_attr_with_hash[:foo]')
h = { 'bar' => 'baz' }
assert_equal h, @obj.send_nested('attr_with_nested_object.nested_attr_with_hash[:foo]', with_indifferent_access: true)
assert_equal h, @obj.send_nested('attr_with_nested_object.nested_attr_with_hash["foo"]', with_indifferent_access: true)
assert_equal 'baz', @obj.send_nested('attr_with_nested_object.nested_attr_with_hash[:foo][:bar]')
assert_equal 'baz', @obj.send_nested('attr_with_nested_object.nested_attr_with_hash[:foo][:bar]', with_indifferent_access: true)
assert_equal 'baz', @obj.send_nested('attr_with_nested_object.nested_attr_with_hash["foo"]["bar"]', with_indifferent_access: true)
assert_equal 'baz', @obj.send_nested('attr_with_nested_object.nested_attr_with_hash[:foo]["bar"]', with_indifferent_access: true)
assert_equal 'baz', @obj.send_nested('attr_with_nested_object.nested_attr_with_hash["foo"][:bar]', with_indifferent_access: true)
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment