Rails Hash to Struct - A monkey patch

salex

Steve Alex

Posted on September 24, 2022

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?

💖 💪 🙅 🚩
salex
Steve Alex

Posted on September 24, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related