Skip to content

Instantly share code, notes, and snippets.

@peter
Created March 26, 2014 16:09
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save peter/9786913 to your computer and use it in GitHub Desktop.
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
# 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