module Transproc::HashTransformations

Transformation functions for Hash objects

@example

require 'transproc/hash'

include Transproc::Helper

fn = t(:symbolize_keys) >> t(:nest, :address, [:street, :zipcode])

fn["street" => "Street 1", "zipcode" => "123"]
# => {:address => {:street => "Street 1", :zipcode => "123"}}

@api public

Constants

EMPTY_HASH

Public Class Methods

accept_keys(hash, keys) click to toggle source

Accepts specified keys from a hash

@example

Transproc(:accept_keys, [:name])[name: 'Jane', email: 'jane@doe.org']
# => {:name=>"Jane"}

@param [Hash] hash The input hash @param [Array] keys The keys to be accepted

@return [Hash]

@api public

# File lib/transproc/hash.rb, line 228
def self.accept_keys(hash, keys)
  Hash[hash].slice(*keys)
end
copy_keys(source_hash, mapping) click to toggle source

Copy all keys in a hash using provided mapping hash

@example

Transproc(:copy_keys, user_name: :name)[user_name: 'Jane']
# => {:user_name => "Jane", :name => "Jane"}

@param [Hash] source_hash The input hash @param [Hash] mapping The key-copy mapping

@return [Hash]

@api public

# File lib/transproc/hash.rb, line 189
def self.copy_keys(source_hash, mapping)
  Hash[source_hash].tap do |hash|
    mapping.each do |original_key, new_keys|
      [*new_keys].each do |new_key|
        hash[new_key] = hash[original_key]
      end
    end
  end
end
deep_merge(hash, other) click to toggle source

Merge a hash recursively

@example

input = { 'foo' => 'bar', 'baz' => { 'one' => 1 } }
other = { 'foo' => 'buz', 'baz' => { :one => 'one', :two => 2 } }

t(:deep_merge)[input, other]
# => { 'foo' => "buz", :baz => { :one => 'one', 'one' => 1, :two => 2 } }

@param [Hash] @param [Hash]

@return [Hash]

@api public

# File lib/transproc/hash.rb, line 443
def self.deep_merge(hash, other)
  Hash[hash].merge(other) do |_, original_value, new_value|
    if original_value.respond_to?(:to_hash) &&
       new_value.respond_to?(:to_hash)
      deep_merge(Hash[original_value], Hash[new_value])
    else
      new_value
    end
  end
end
deep_stringify_keys(hash) click to toggle source

Stringify keys in a hash recursively

@example

input = { :foo => "bar", :baz => [{ :one => 1 }] }

t(:deep_stringify_keys)[input]
# => { "foo" => "bar", "baz" => [{ "one" => 1 }] }

@param [Hash]

@return [Hash]

@api public

# File lib/transproc/hash.rb, line 120
def self.deep_stringify_keys(hash)
  hash.each_with_object({}) do |(key, value), output|
    output[key.to_s] =
      case value
      when Hash
        deep_stringify_keys(value)
      when Array
        value.map { |item|
          item.is_a?(Hash) ? deep_stringify_keys(item) : item
        }
      else
        value
      end
  end
end
deep_symbolize_keys(hash) click to toggle source

Symbolize keys in a hash recursively

@example

input = { 'foo' => 'bar', 'baz' => [{ 'one' => 1 }] }

t(:deep_symbolize_keys)[input]
# => { :foo => "bar", :baz => [{ :one => 1 }] }

@param [Hash]

@return [Hash]

@api public

# File lib/transproc/hash.rb, line 76
def self.deep_symbolize_keys(hash)
  hash.each_with_object({}) do |(key, value), output|
    output[key.to_sym] =
      case value
      when Hash
        deep_symbolize_keys(value)
      when Array
        value.map { |item|
          item.is_a?(Hash) ? deep_symbolize_keys(item) : item
        }
      else
        value
      end
  end
end
eval_values(hash, args, filters = []) click to toggle source

Recursively evaluate hash values if they are procs/lambdas

@example

hash = {
  num: -> i { i + 1 },
  str: -> i { "num #{i}" }
}

t(:eval_values, 1)[hash]
# => {:num => 2, :str => "num 1" }

# with filters
t(:eval_values, 1, [:str])[hash]
# => {:num => #{still a proc}, :str => "num 1" }

@param [Hash] @param [Array,Object] args Anything that should be passed to procs @param [Array] filters A list of attribute names that should be evaluated

@api public

# File lib/transproc/hash.rb, line 405
def self.eval_values(hash, args, filters = [])
  hash.each_with_object({}) do |(key, value), output|
    output[key] =
      case value
      when Proc
        if filters.empty? || filters.include?(key)
          value.call(*args)
        else
          value
        end
      when Hash
        eval_values(value, args, filters)
      when Array
        value.map { |item|
          item.is_a?(Hash) ? eval_values(item, args, filters) : item
        }
      else
        value
      end
  end
end
fold(hash, key, tuple_key) click to toggle source

Folds array of tuples to array of values from a specified key

@example

source = {
  name: "Jane",
  tasks: [{ title: "be nice", priority: 1 }, { title: "sleep well" }]
}
Transproc(:fold, :tasks, :title)[source]
# => { name: "Jane", tasks: ["be nice", "sleep well"] }
Transproc(:fold, :tasks, :priority)[source]
# => { name: "Jane", tasks: [1, nil] }

@param [Hash] hash @param [Object] key The key to fold values to @param [Object] tuple_key The key to take folded values from

@return [Hash]

@api public

# File lib/transproc/hash.rb, line 340
def self.fold(hash, key, tuple_key)
  hash.merge(key => ArrayTransformations.extract_key(hash[key], tuple_key))
end
map_keys(source_hash, fn) click to toggle source

Map all keys in a hash with the provided transformation function

@example

Transproc(:map_keys, -> s { s.upcase })['name' => 'Jane']
# => {"NAME" => "Jane"}

@param [Hash]

@return [Hash]

@api public

# File lib/transproc/hash.rb, line 36
def self.map_keys(source_hash, fn)
  Hash[source_hash].transform_keys!(&fn)
end
map_value(hash, key, fn) click to toggle source

Map a key in a hash with the provided transformation function

@example

Transproc(:map_value, 'name', -> s { s.upcase })['name' => 'jane']
# => {"name" => "JANE"}

@param [Hash]

@return [Hash]

@api public

# File lib/transproc/hash.rb, line 248
def self.map_value(hash, key, fn)
  hash.merge(key => fn[hash[key]])
end
map_values(source_hash, fn) click to toggle source

Map all values in a hash using transformation function

@example

Transproc(:map_values, -> v { v.upcase })[:name => 'Jane']
# => {"name" => "JANE"}

@param [Hash]

@return [Hash]

@api public

# File lib/transproc/hash.rb, line 148
def self.map_values(source_hash, fn)
  Hash[source_hash].transform_values!(&fn)
end
nest(hash, root, keys) click to toggle source

Nest values from specified keys under a new key

@example

Transproc(:nest, :address, [:street, :zipcode])[street: 'Street', zipcode: '123']
# => {address: {street: "Street", zipcode: "123"}}

@param [Hash]

@return [Hash]

@api public

# File lib/transproc/hash.rb, line 263
def self.nest(hash, root, keys)
  child = {}

  keys.each do |key|
    child[key] = hash[key] if hash.key?(key)
  end

  output = Hash[hash]

  child.each_key { |key| output.delete(key) }

  old_root = hash[root]

  if old_root.is_a?(Hash)
    output[root] = old_root.merge(child)
  else
    output[root] = child
  end

  output
end
reject_keys(hash, keys) click to toggle source

Rejects specified keys from a hash

@example

Transproc(:reject_keys, [:name])[name: 'Jane', email: 'jane@doe.org']
# => {:email => "jane@doe.org"}

@param [Hash] hash The input hash @param [Array] keys The keys to be rejected

@return [Hash]

@api public

# File lib/transproc/hash.rb, line 211
def self.reject_keys(hash, keys)
  Hash[hash].reject { |k, _| keys.include?(k) }
end
rename_keys(source_hash, mapping) click to toggle source

Rename all keys in a hash using provided mapping hash

@example

Transproc(:rename_keys, user_name: :name)[user_name: 'Jane']
# => {:name => "Jane"}

@param [Hash] source_hash The input hash @param [Hash] mapping The key-rename mapping

@return [Hash]

@api public

# File lib/transproc/hash.rb, line 171
def self.rename_keys(source_hash, mapping)
  Hash[source_hash].tap do |hash|
    mapping.each { |k, v| hash[v] = hash.delete(k) if hash.key?(k) }
  end
end
split(hash, key, keys) click to toggle source

Splits hash to array by all values from a specified key

The operation adds missing keys extracted from the array to regularize the output.

@example

input = {
  name: 'Joe',
  tasks: [
    { title: 'sleep well', priority: 1 },
    { title: 'be nice',    priority: 2 },
    {                      priority: 2 },
    { title: 'be cool'                 }
  ]
}
Transproc(:split, :tasks, [:priority])[input]
=> [
    { name: 'Joe', priority: 1,   tasks: [{ title: 'sleep well' }]              },
    { name: 'Joe', priority: 2,   tasks: [{ title: 'be nice' }, { title: nil }] },
    { name: 'Joe', priority: nil, tasks: [{ title: 'be cool' }]                 }
  ]

@param [Hash] hash @param [Object] key The key to split a hash by @param [Array] subkeys The list of subkeys to be extracted from key

@return [Array<Hash>]

@api public

# File lib/transproc/hash.rb, line 372
def self.split(hash, key, keys)
  list = Array(hash[key])
  return [hash.reject { |k, _| k == key }] if list.empty?

  existing  = list.flat_map(&:keys).uniq
  grouped   = existing - keys
  ungrouped = existing & keys

  list = ArrayTransformations.group(list, key, grouped) if grouped.any?
  list = list.map { |item| item.merge(reject_keys(hash, [key])) }
  ArrayTransformations.add_keys(list, ungrouped)
end
stringify_keys(hash) click to toggle source

Stringify all keys in a hash

@example

Transproc(:stringify_keys)[:name => 'Jane']
# => {"name" => "Jane"}

@param [Hash]

@return [Hash]

@api public

# File lib/transproc/hash.rb, line 103
def self.stringify_keys(hash)
  map_keys(hash, Coercions[:to_string].fn)
end
symbolize_keys(hash) click to toggle source

Symbolize all keys in a hash

@example

Transproc(:symbolize_keys)['name' => 'Jane']
# => {:name => "Jane"}

@param [Hash]

@return [Hash]

@api public

# File lib/transproc/hash.rb, line 58
def self.symbolize_keys(hash)
  map_keys(hash, Coercions[:to_symbol].fn)
end
unwrap(source_hash, root, selected = nil, options = EMPTY_HASH) click to toggle source

Collapse a nested hash from a specified key

@example

Transproc(:unwrap, :address, [:street, :zipcode])[address: { street: 'Street', zipcode: '123' }]
# => {street: "Street", zipcode: "123"}

@param [Hash] source_hash @param [Mixed] root The root key to unwrap values from @param [Array] selected The keys that should be unwrapped (optional) @param [Hash] options hash of options (optional) @option options [Boolean] :prefix if true, unwrapped keys will be prefixed

with the root key followed by an underscore (_)

@return [Hash]

@api public

# File lib/transproc/hash.rb, line 301
def self.unwrap(source_hash, root, selected = nil, options = EMPTY_HASH)
  return source_hash unless source_hash[root]
  options, selected = selected, nil if options.empty? && selected.is_a?(::Hash)

  add_prefix = ->(key) do
    combined = [root, key].join('_')
    root.is_a?(::Symbol) ? combined.to_sym : combined
  end

  Hash[source_hash].merge(root => Hash[source_hash[root]]).tap do |hash|
    nested_hash = hash[root]
    keys = nested_hash.keys
    keys &= selected if selected
    new_keys = options[:prefix] ? keys.map(&add_prefix) : keys

    hash.update(Hash[new_keys.zip(keys.map { |key| nested_hash.delete(key) })])
    hash.delete(root) if nested_hash.empty?
  end
end