DEV Community

Steve Alex
Steve Alex

Posted on

1

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?

Qodo Takeover

Introducing Qodo Gen 1.0: Transform Your Workflow with Agentic AI

While many AI coding tools operate as simple command-response systems, Qodo Gen 1.0 represents the next generation: autonomous, multi-step problem-solving agents that work alongside you.

Read full post

Top comments (0)

AWS Security LIVE!

Join us for AWS Security LIVE!

Discover the future of cloud security. Tune in live for trends, tips, and solutions from AWS and AWS Partners.

Learn More

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay