Search code examples
rubyhashactivesupport

How to deep_transform_values with keys in Ruby?


Active Support's deep_transform_values recursively transforms all values of a hash. However, is there a similar method that would allow to access the keys of values while transforming?

I'd like to be able to do the following:

keys_not_to_transform = ['id', 'count']

response = { result: 'ok', errors: [], data: { id: '123', price: '100.0', quotes: ['1.0', '2.0'] }, count: 10 }
response.deep_transform_values! do |key, value|
  # Use value's key to help decide what to do
  return value if keys_not_to_transform.any? key.to_s

  s = value.to_s
  if s.present? && /\A[+-]?\d+(\.\d+)?\z/.match?(s)
    return BigDecimal(s)
  else
    value
  end
end

#Expected result 
# =>{:result=>"ok", :errors=>[], :data=>{:id=>"123", :price=>0.1e3, :quotes=>[0.1e1, 0.2e1]}, :count=>10}

Note that we are not interested in transforming the key itself, just having it on hand while transforming the corresponding values.


Solution

  • You could use Hash#deep_merge! (provided by ActiveSupport) like so:

    keys_not_to_transform = ['id', 'count']
    
    transform_value = lambda do |value|
        s = value.to_s
        if s.present? && /\A[+-]?\d+(\.\d+)?\z/.match?(s)
          BigDecimal(s)
        else
          value
        end
    end
    
    transform = Proc.new do |key,value|
      if keys_not_to_transform.include? key.to_s
        value
      elsif value.is_a?(Array)
        value.map! do |v| 
          v.is_a?(Hash) ? v.deep_merge!(v,&transform) : transform_value.(v)
        end 
      else   
        transform_value.(value)
      end  
    end
    
    response = { result: 'ok', errors: [], data: { id: '123', price: '100.0', quotes: ['1.0', '2.0'], other: [{id: '124', price: '17.0'}] }, count: 10 }
    
    response.deep_merge!(response, &transform)
    

    This outputs:

    #=>{:result=>"ok", :errors=>[], :data=>{:id=>"123", :price=>0.1e3, :quotes=>[0.1e1, 0.2e1], :other=>[{:id=>"124", :price=>0.17e2}]}, :count=>10}