kdbanman kdbanman - 2 months ago 7
Ruby Question

Is there a lightweight way to lock down a set of keys on a Hash?

To be clear, I'm perfectly happy implementing this functionality as a custom class myself, but I want to make sure I'm not overlooking some bit of ruby or rails magic. I have googled every meaningful permutation of the keywords "ruby rails hash keys values immutable lock freeze". But no luck so far!




Problem: I need to give a
Hash
a set of keys, possibly at run time, and then lock the set of keys without locking their values. Something like the following:

to_lock = {}
to_lock[:name] = "Bill"
to_lock[:age] = 42

to_lock.freeze_keys # <-- this is what I'm after, so that:

to_lock[:name] = "Bob" # <-- this works fine,
to_lock[:height] # <-- this returns nil, and
to_lock[:height] = 175 # <-- this throws some RuntimeError


Question: Is there a bit of ruby or rails tooling to allow this?




I know of
Object#freeze
and of
Immutable::Hash
, but both lock keys and values.

Sticking with out-of-the-box ruby, the use case could be mostly met by manipulating the methods or accessors of classes at runtime, as in this or this, then overriding
#method_missing
. But that feels quite a bit clunkier. Those techniques also don't really "lock" the set of methods or accessors, it's just awkward to add more. At that point it'd be better to simply write a class that exactly implements the snippet above and maintain it as needed.

Answer

You can achieve this by defining a custom []= for your "to-lock" instance of a hash, after you've added the allowed keys:

x = { name: nil, age: nil }

def x.[]=(key, value)
  # blow up unless the key already exists in the hash
  raise 'no' unless keys.include?(key)
  super
end

x[:name] # nil
x[:name] = "Bob" # "Bob"

x[:size] # nil
x[:size] = "large" # raise no

Note that this won't prevent you from inadvertently adding keys using something like merge!.

Comments