DEV Community

Steve Alex
Steve Alex

Posted on

Rails Hash to Struct - A monkey patch

The Hobby Developer(me!) has been busy refactoring a few my sites. I've mentioned in a posts about using serialized fields in a model Rails - Using the Attributes API to manage serialized preferences. Most were just adding a serialize attribute in a model serialize :settings, ActiveSupport::HashWithIndifferentAccess. Rails 7.0.4 kind of mucked that up. I had been using HashWithIndifferentAccess because I like using symbols in a hash versus 'strings'. Rails serializes hashes using YAML. YAML had a security bug and Rails fixed it by requiring you to explicitly define what will be serialized. That took a little while to get right, but in the talk about the bug, they basically said: 'why not just use JSON'.

That's a little of what I've been refactoring. I'm still trying to figure out change a few attributes from HashWithIndifferentAccess to JSON. I'm afraid its going to be something like:

  • Take the server down
  • Remove serialize :settings, ActiveSupport::HashWithIndifferentAccess
  • Deploy conversion version parse the YMAL and save as JSON
  • Add serialize :settings, JSON and redeploy.

It will Probably take 15 minutes, but I want to think about it a little more.

What I've been doing recently is converting some of these setting/preference Hashes to Struct (originally OpenStruct - but abandoned that). Again, it's just a personal preference, I prefer settings.acct_placeholders than settings['acct_placeholders'].I originally did this using a Monkey Patch I stuck in config/initializers.

Hash.class_eval do def to_struct Struct.new(*keys.map(&:to_sym)).new(*values) end end 
Enter fullscreen mode Exit fullscreen mode

Probably not a good idea, but it worked for a simple hash, but not a nested hash. In reading a little more about Monkey Patching in Monkey patching in Rails and 3 Ways to Monkey-Patch Without Making a Mess, I decided to do it the Rails way using modules.

I added a folder to /lib core_extensions and two sub-folder hash and array. In the subfolders and added my monkey patches.

  • core_extensions
    • hash
    • as_struct.rb
    • to_struct.rb
    • array
    • test_array.rb

I just added the array as a proof of concept.

# just a proof of concept module CoreExtensions module Array def test_array puts "test_array" self end end end 
Enter fullscreen mode Exit fullscreen mode

To get these patches to work, you have to load the patches, so in config/initializers I added monkey_patches.rb

# config/initializers/money_patches.rb # Require all Ruby files in the core_extensions directory by class Dir[Rails.root.join('lib', 'core_extensions/*', '*.rb')].each { |f| require f } # Apply the monkey patches Array.include CoreExtensions::Array Hash.include CoreExtensions::Hash 
Enter fullscreen mode Exit fullscreen mode

For the hash.to_struct patch I ended up with two patches: .to_struct and .as_struct. This is a spinoff and Rails .to_json and .as_json. One (.as_json) sanitizes a hash and the other does the conversion.

# /lib/core_extensins/as_struct.rb # convert Hash to Struct on a single level module CoreExtensions module Hash def as_struct Struct.new(*keys.map(&:to_sym)).new(*values) end end end 
Enter fullscreen mode Exit fullscreen mode
# /lib/core_extensins/to_struct.rb # convert Hash to a nested Struct  module CoreExtensions module Hash def to_struct hash_to_struct(self) end private def hash_to_struct(ahash) struct = ahash.as_struct # convert to struct struct.members.each do |m| if struct[m].is_a? Hash struct[m] = hash_to_struct(struct[m]) # nested hash, recursive call elsif struct[m].is_a? Array # look for hashes in an array and convert to struct struct[m].each_index do |i| # normal use, an array of hashes struct[m][i] = hash_to_struct(struct[m][i]) if struct[m][i].is_a? Hash # convoluded use, an array that may contain hash(es) struct[m][i] = hash_in_array(struct[m][i]) if struct[m][i].is_a? Array end end end struct end def hash_in_array(arr) arr.each_index do |ii| arr[ii] = hash_to_struct(arr[ii]) if arr[ii].is_a? Hash end arr end end end 
Enter fullscreen mode Exit fullscreen mode

So if I define a convoluted nested Hash (I wouldn't do this... but again proof of concept)

h = { game:{id:1,date:'2022-09-11',player:6}, players:[{name:'Joe',quota:21},{name:'Harry',quota:26},{name:'Pete',quota:14}, {name:'don',quota:21},{name:'sally',quota:26},{name:'red',quota:14}], teams:[['joe','don',team:{a:1,b:2,c:3}],['harry','sally',lost:{skins:2,par3:9}],['pete','red']]} 
Enter fullscreen mode Exit fullscreen mode

and call s = h.to_struct, I get a convoluted Struct:

<struct game=<struct id=1, date="2022-09-11", player=6>, players= [<struct name="Joe", quota=21>, <struct name="Harry", quota=26>, <struct name="Pete", quota=14>, <struct name="don", quota=21>, <struct name="sally", quota=26>, <struct name="red", quota=14>], teams= [["joe", "don", <struct team=<struct a=1, b=2, c=3>>], ["harry", "sally", <struct lost=<struct skins=2, par3=9>>], ["pete", "red"]]> 
Enter fullscreen mode Exit fullscreen mode

So

 # s.game returns  <struct id=1, date="2022-09-11", player=6> # s.game.date return "2022-09-11" 
Enter fullscreen mode Exit fullscreen mode

That it!

Again, I'm just a hobbyist and my Ruby skill are not deep, but a lot better that what I knew 12 years ago.

Any comments?

Top comments (0)