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
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
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
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
# /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
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']]}
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"]]>
So
# s.game returns
<struct id=1, date="2022-09-11", player=6>
# s.game.date return
"2022-09-11"
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)