Create recursive OpenStruct from a Ruby Hash
2013-04-24
When I develop some ruby app, mostly rails ones, I need to manage several configuration variables (eg: API keys) and I like to store them in a unique place. So I usually end up with something like this:
# config/initializers/_load_config.rb
path = File.read("#{Rails.root}/config/config.yml")
APP_CONF = ActiveSupport::HashWithIndifferentAccess.new(
YAML.load(ERB.new(path).result)[Rails.env]
)
It does a good job:
- it first compiles eventual ERB tags (so you can store values in
ENV
) - then it loads YAML
- finally initializes a HashWithIndifferentAccess, so I can lookup keys with either a String or a Symbol
The last point is a plus, kindly offered by Rails' ActiveSupport library, I prefer Symbol over String for Hash keys.
The problem
However, I knew that there's a smarter way to achieve same goal, even with some more adavantages. I started from OpenStruct:
- it solves the problem of Hash key lookup by providing them as methods, for example:
require 'ostruct'
hash = {a: 1, b: 2}
mystruct = OpenStruct.new hash
mystruct.a # => 1
mystruct.b # => 2
- it's part of the Ruby's standard library (along with YAML and ERB as well)
- it's very simple to hack ;-)
Looks excellent, right? Well, it's not perfect, because it doesn't handle nested hashes, in other words, here's what happen when you pass an Hash that has other hashes as values:
require 'ostruct'
hash = {a: {b: 1}}
mystruct = OpenStruct.new hash
mystruct.a # => {b: 1}
mystruct.a.b # => NoMethodError: undefined method 'b' for {:b=>1}:Hash
Moreover, there's no way to access the original hash (well, there's one but that's another point), it might be useful when you need to pass an entire Hash.
A solution
OpenStruct
already does a great part fo the dirty job, it only needs some change to get what we need. I called it, DeepStruct
:
# deep_struct.rb
require 'ostruct'
class DeepStruct < OpenStruct
def initialize(hash=nil)
@table = {}
@hash_table = {}
if hash
hash.each do |k,v|
@table[k.to_sym] = (v.is_a?(Hash) ? self.class.new(v) : v)
@hash_table[k.to_sym] = v
new_ostruct_member(k)
end
end
end
def to_h
@hash_table
end
end
There's not too much to explain here, I've overridden OpenStruct#new method to make two basic things:
- Iterate the key/values of the passed Hash, then:
- initialize a new
DeepStruct
if a given value is an Hash - store original key/values in @hash_table, so I can retrieve it with
#to_h
method
- initialize a new
That's all we need, here's an example:
require 'deep_struct'
hash = {a: {b: 1}}
mystruct = DeepStruct.new hash
mystruct.a # => #<DeepStruct b=1>
mystruct.a.b # => 1
mystruct.a.to_h # => {b: 1}
Conclusion
I've said I'd mainly use this to store configs for my apps, so go back to the initial example:
# config/initializers/_load_config.rb
path = File.read("#{Rails.root}/config/config.yml")
APP_CONF = DeepStruct.new(YAML.load(ERB.new(path).result)[Rails.env])
Enjoy and/or leave a comment :-)