Steve Alex
Posted on September 24, 2022
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?
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
November 29, 2024