Created
January 26, 2017 14:54
-
-
Save prschmid/07a83baa42a37ed18e06519618621511 to your computer and use it in GitHub Desktop.
Ruby method to enable "sending" nested calls to an object
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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