Created
March 26, 2014 16:09
-
-
Save peter/9786913 to your computer and use it in GitHub Desktop.
Using a recursive struct in Ruby to honor the Uniform Access Principle when accessing data from hashes/structs/objects
This file contains hidden or 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
# Simple wrapper to allow hashes to be accessed via dot notation recursively. | |
# Recurses over hashes and arrays. Works with string keys | |
# and symbol keys - other types of keys are not supported and | |
# all keys must be of the same type. Write access is only supported via | |
# []= Hash syntax. Supports accessing hash values with square bracket Hash syntax ([...]) | |
# and access is indifferent to if the key is given as a string or a symbol. | |
# Supports JSON generation. | |
# | |
# Dependencies: Ruby. | |
# | |
# Usage examples: | |
# | |
# struct = RecursiveStruct.new({foo: {bla: [{id: 1}]}}) | |
# struct.foo.bla[0] | |
# => <RecursiveStruct:0x007fa52df5a288 @hash={:id=>1}> | |
# struct.foo.bla[0].id | |
# => 1 | |
# struct[:foo][:bla][0][:id] | |
# => 1 | |
# struct['foo']['bla'][0]['id'] | |
# => 1 | |
# struct.asdfasdfahfasdf | |
# => nil | |
# RecursiveStruct.new(JSON.parse('{"foo": 1}')).foo | |
# => 1 | |
# JSON.generate(RecursiveStruct.new(foo: 1)) | |
# => "{\"foo\":1}" | |
# | |
class RecursiveStruct | |
def initialize(hash) | |
@hash = hash | |
@key_class = hash.empty? ? Symbol : hash.keys[0].class | |
self.class.assert_valid_key_class(@key_class) | |
end | |
def to_h(*args) | |
@hash | |
end | |
alias_method :to_hash, :to_h | |
alias_method :as_json, :to_h | |
def to_json(*args) | |
@hash.to_json | |
end | |
def [](name) | |
data = @hash[typed_key(name)] | |
if data.is_a?(Hash) | |
self.class.new(data) | |
elsif data.is_a?(Array) | |
data.map do |item| | |
item.is_a?(Hash) ? self.class.new(item) : item | |
end | |
else | |
data | |
end | |
end | |
def []=(name, value) | |
@hash[typed_key(name)] = value | |
end | |
def method_missing(name, *args) | |
self[name] | |
end | |
def typed_key(key) | |
@key_class == String ? key.to_s : key.to_sym | |
end | |
def self.assert_valid_key_class(key_class) | |
raise "Invalid key_class #{key_class} must be one of #{valid_key_classes.join(' ')}" unless valid_key_classes.include?(key_class) | |
end | |
def self.valid_key_classes | |
[String, Symbol] | |
end | |
end | |
################################################# | |
# | |
# Test case | |
# | |
################################################# | |
require 'test_helper' | |
require 'json' | |
require File.expand_path('../../../app/lib/recursive_struct', __FILE__) | |
class RecursiveStructTest < MiniTest::Unit::TestCase | |
def test_dot_and_hash_and_indifferent_access | |
hash = {foo: {bla: [{id: 1}]}, bar: '2'} | |
struct = RecursiveStruct.new(hash) | |
assert_equal nil, struct.ajsdkfjalsdghasdf | |
assert_equal hash, struct.to_h | |
assert_equal '2', struct.bar | |
assert_equal '2', struct[:bar] | |
assert_equal '2', struct['bar'] | |
assert_equal RecursiveStruct, struct.foo.class | |
assert_equal Array, struct.foo.bla.class | |
assert_equal 1, struct.foo.bla[0].id | |
assert_equal 1, struct[:foo][:bla][0][:id] | |
end | |
def test_string_keys | |
hash = {'foo' => {'bla' => [{'id' => 1}]}} | |
struct = RecursiveStruct.new(hash) | |
assert_equal 1, struct.foo.bla[0].id | |
assert_equal 1, struct[:foo][:bla][0][:id] | |
assert_equal 1, struct['foo']['bla'][0]['id'] | |
end | |
def test_invalid_keys | |
assert_raises(RuntimeError) do | |
struct = RecursiveStruct.new({4 => 1}) | |
end | |
end | |
def test_nested_arrays_with_primitives | |
hash = {foo: [:a, :b, :c]} | |
struct = RecursiveStruct.new(hash) | |
assert_equal Array, struct.foo.class | |
assert_equal :b, struct.foo[1] | |
end | |
def test_json_generation | |
assert_equal '{"bla":{"foo":[{"id":2}]}}', JSON.generate({bla: RecursiveStruct.new({foo: [{id: 2}]})}) | |
end | |
def test_write_access | |
struct = RecursiveStruct.new(foo: 1) | |
assert_equal 1, struct.foo | |
struct[:foo] = 2 | |
assert_equal 2, struct.foo | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment