How can I convert a hash into a struct in ruby?
Given this:
h = { :a => 1, :b => 2 } I want a struct such that:
s.a == 1 s.b == 2 If you already have a struct defined, and you want to instantiate an instance with a hash:
Person = Struct.new(:first_name, :last_name, :age) person_hash = { first_name: "Foo", last_name: "Bar", age: 29 } person = Person.new(*person_hash.values_at(*Person.members)) => #<struct Person first_name="Foo", last_name="Bar", age=29> Person.new(**person_hash)If it doesn't specifically have to be a Struct and instead can be an OpenStruct:
pry(main)> require 'ostruct' pry(main)> s = OpenStruct.new(h) => #<OpenStruct a=1, b=2> pry(main)> puts s.a, s.b 1 2 pry(main)> puts s.a, s.b or line 2 should read pry(main)> o = OpenStruct.new(h)Since Hash key order is guaranteed in Ruby 1.9+:
Struct.new(*h.keys).new(*h.values) Struct.new(*h.keys) raises: NameError: identifier my_key needs to be constantStruct.new(*h.keys.map(&:to_sym)).new(*h.values)This is based on @elado's answer above, but using the keyword_init value (Struct Documentation)
You could simply do this:
Person = Struct.new(:first_name, :last_name, :age, keyword_init: true) person_hash = { first_name: "Foo", last_name: "Bar", age: 29 } person = Person.new(person_hash) => #<struct Person first_name="Foo", last_name="Bar", age=29> keyword_init: true was only introduced in Ruby 2.5, whereas 2.4 is still within support.Having Hash#to_struct is quite practical:
class Hash def to_struct Struct.new(*keys).new(*values) end end And some examples:
>> { a: 1, b: 2 }.to_struct => #<struct a=1, b=2> >> { a: 1, b: 2 }.to_struct.a => 1 >> { a: 1, b: 2 }.to_struct.b => 2 >> { a: 1, b: 2 }.to_struct.c NoMethodError: undefined method `c` for #<struct a=1, b=2> Deep to_struct that works with arrays:
class Array def to_struct map { |value| value.respond_to?(:to_struct) ? value.to_struct : value } end end class Hash def to_struct Struct.new(*keys).new(*values.to_struct) end end ['name'], needs to symbolize the keys.You can convert from Hash to Struct using the following code:
Struct.new(*my_hash.keys.map(&:to_sym)).new(*my_hash.values) Ensure you convert all keys to symbols, as it will error on String keys, NameError: identifier my_key needs to be constant
I personally recommend adding a monkey patch to the Hash class because this is such a powerful action
# config/initializers/core_extensions.rb Hash.class_eval do def to_struct Struct.new(*keys.map(&:to_sym)).new(*values) end end Here's an example to map the values to the proper order of the Struct:
require 'securerandom' Message = Struct.new(:to, :from, :message, :invitee) message_params = {from: "[email protected]", to: "[email protected]", invitee: SecureRandom.uuid, message: "hello"} if Message.members.sort == message_params.keys.sort # Do something with the return struct object here Message.new *Message.members.map {|k| message_params[k] } else raise "Invalid keys for Message" end This gives a clean plain read-only object, similar to a ruby Struct but with deep conversion and extra to_h method to get struct at any point as Hash.
Example
foo = {a:{b:{c:123}}}.to_struct foo.a.b.c # 123 foo.a.to_h # {b:{c:123}} Ruby code
class Hash def to_struct Class.new.tap do |c| c.define_singleton_method(:to_h) do m_list = methods(false) - [:to_h] m_list.inject({}) do |h, m| h[m] = send(m) h[m] = h[m].to_h if h[m].class == Class h end end each do |k, v| v = v.to_struct if v.class == Hash c.define_singleton_method(k) { v } end end end end Not exactly the answer to a question (not a ruby Struct object), but I needed just this while looking for an answer, so I will just post the answer here.
TL;DR;
use OpenStruct.new(hash)
New Better way
hash = {"Name" => "Taimoor", "id" => "222", "SomeKey" => "Some value", "attributes" => {"type" => 'User', 'role' => 'manager'}} OpenStruct.new(hash) This will only convert 1st level of hash to struct. To convert nested attributes hash to struct I did this
hash.attributes = OpenStruct.new(hash.attributes) OLD Way
I had a hash with string keys
{"Name" => "Taimoor", "id" => "222", "SomeKey" => "Some value"} So I need to first convert keys to symbols hash.keys.map(&:to_sym) and to access those keys in the original hash, I used the hash.with_indifferent_access method on the hash.
def hash_to_struct(hash) Struct.new(*(k = hash.keys.map(&:to_sym))) .new(*hash.with_indifferent_access.values_at(*k)) end Now it will work for both symbol and string type keys of the hash.
Note: This will convert the hash to struct at one level only. For nested hash, you need to call this method on each level of nesting.
OpenStruct incurs a performance penalty compared to Struct. It is now recommended to use Struct unless you really need the keys/attributes to be dynamic. See rubydoc.info/gems/rubocop/0.61.1/RuboCop/Cop/Performance/….