Class: ElasticGraph::Support::HashUtil

Inherits:
Object
  • Object
show all
Defined in:
lib/elastic_graph/support/hash_util.rb

Class Method Summary collapse

Class Method Details

.deep_merge(hash1, hash2) ⇒ Object

Recursively merges the values from ‘hash2` into `hash1`, without mutating either `hash1` or `hash2`. When a key is in both `hash2` and `hash1`, takes the value from `hash2` just like `Hash#merge` does.



104
105
106
107
108
109
110
111
112
113
# File 'lib/elastic_graph/support/hash_util.rb', line 104

def self.deep_merge(hash1, hash2)
  # `_ =` needed to satisfy steep--the types here are quite complicated.
  _ = hash1.merge(hash2) do |key, hash1_value, hash2_value|
    if ::Hash === hash1_value && ::Hash === hash2_value
      deep_merge(hash1_value, hash2_value)
    else
      hash2_value
    end
  end
end

.disjoint_merge(hash1, hash2) ⇒ Object

Like ‘Hash#merge`, but verifies that the hashes were strictly disjoint (e.g. had no keys in common). An error is raised if they do have any keys in common.



35
36
37
38
39
40
41
42
43
44
45
46
47
# File 'lib/elastic_graph/support/hash_util.rb', line 35

def self.disjoint_merge(hash1, hash2)
  conflicting_keys = [] # : ::Array[untyped]
  merged = hash1.merge(hash2) do |key, v1, _v2|
    conflicting_keys << key
    v1
  end

  unless conflicting_keys.empty?
    raise ::KeyError, "Hashes were not disjoint. Conflicting keys: #{conflicting_keys.inspect}."
  end

  merged
end

.fetch_leaf_values_at_path(hash, key_path, &default) ⇒ Object

Fetches a list of (potentially) nested value from a hash. The ‘key_path` is expected to be a string with dots between the nesting levels (e.g. `foo.bar`). Returns `[]` if the value at any parent key is `nil`. Returns a flat array of values if the structure at any level is an array.

Raises an error if the key is not found unless a default block is provided. Raises an error if any parent value is not a hash as expected. Raises an error if the provided path is not a full path to a leaf in the nested structure.



123
124
125
# File 'lib/elastic_graph/support/hash_util.rb', line 123

def self.fetch_leaf_values_at_path(hash, key_path, &default)
  do_fetch_leaf_values_at_path(hash, key_path.split("."), 0, &default)
end

.fetch_value_at_path(hash, key_path) ⇒ Object

Fetches a single value from the hash at the given path. The ‘key_path` is expected to be a string with dots between the nesting levels (e.g. `foo.bar`).

If any parent value is not a hash as expected, raises an error. If the key at any level is not found, yields to the provided block (which can provide a default value) or raises an error if no block is provided.



133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
# File 'lib/elastic_graph/support/hash_util.rb', line 133

def self.fetch_value_at_path(hash, key_path)
  path_parts = key_path.split(".")

  path_parts.each.with_index(1).reduce(hash) do |inner_hash, (key, num_parts)|
    if inner_hash.is_a?(::Hash)
      inner_hash.fetch(key) do
        missing_path = path_parts.first(num_parts).join(".")
        return yield missing_path if block_given?
        raise KeyError, "Key not found: #{missing_path.inspect}"
      end
    else
      raise KeyError, "Value at key #{path_parts.first(num_parts - 1).join(".").inspect} is not a `Hash` as expected; " \
        "instead, was a `#{(_ = inner_hash).class}`"
    end
  end
end

.flatten_and_stringify_keys(source_hash, prefix: nil) ⇒ Object

Recursively flattens the provided source hash, converting keys to strings along the way with dots used to separate nested parts. For example:

flatten_and_stringify_keys({ a: { b: 3 }, c: 5 }, prefix: “foo”) returns: { “foo.a.b” => 3, “foo.c” => 5 }



93
94
95
96
97
98
99
100
# File 'lib/elastic_graph/support/hash_util.rb', line 93

def self.flatten_and_stringify_keys(source_hash, prefix: nil)
  # @type var flat_hash: ::Hash[::String, untyped]
  flat_hash = {}
  prefix = prefix ? "#{prefix}." : ""
  # `_ =` is needed by steep because it thinks `prefix` could be `nil` in spite of the above line.
  populate_flat_hash(source_hash, _ = prefix, flat_hash)
  flat_hash
end

.recursively_prune_nils_and_empties_from(object, &block) ⇒ Object

Recursively prunes nil values or empty hash/array values from the hash, at any level of its structure, without mutating the provided argument. Key paths that are pruned are yielded to the caller to allow the caller to have awareness of what was pruned.



78
79
80
81
82
83
84
85
86
# File 'lib/elastic_graph/support/hash_util.rb', line 78

def self.recursively_prune_nils_and_empties_from(object, &block)
  recursively_prune_if(object, block) do |value|
    if value.is_a?(::Hash) || value.is_a?(::Array)
      value.empty?
    else
      value.nil?
    end
  end
end

.recursively_prune_nils_from(object, &block) ⇒ Object

Recursively prunes nil values from the hash, at any level of its structure, without mutating the provided argument. Key paths that are pruned are yielded to the caller to allow the caller to have awareness of what was pruned.



71
72
73
# File 'lib/elastic_graph/support/hash_util.rb', line 71

def self.recursively_prune_nils_from(object, &block)
  recursively_prune_if(object, block, &:nil?)
end

.strict_to_h(pairs) ⇒ Object

Like ‘Hash#to_h`, but strict. When the given input has conflicting keys, `Hash#to_h` will happily let the last pair when. This method instead raises an exception.



22
23
24
25
26
27
28
29
30
31
# File 'lib/elastic_graph/support/hash_util.rb', line 22

def self.strict_to_h(pairs)
  hash = pairs.to_h

  if hash.size < pairs.size
    conflicting_keys = pairs.map(&:first).tally.filter_map { |key, count| key if count > 1 }
    raise ::KeyError, "Cannot build a strict hash, since input has conflicting keys: #{conflicting_keys.inspect}."
  end

  hash
end

.stringify_keys(object) ⇒ Object

Recursively transforms any hash keys in the given object to string keys, without mutating the provided argument.



51
52
53
54
55
# File 'lib/elastic_graph/support/hash_util.rb', line 51

def self.stringify_keys(object)
  recursively_transform(object) do |key, value, hash|
    hash[key.to_s] = value
  end
end

.symbolize_keys(object) ⇒ Object

Recursively transforms any hash keys in the given object to symbol keys, without mutating the provided argument.

Important note: this should never be used on untrusted input. Symbols are not GCd in Ruby in the same way as strings.



62
63
64
65
66
# File 'lib/elastic_graph/support/hash_util.rb', line 62

def self.symbolize_keys(object)
  recursively_transform(object) do |key, value, hash|
    hash[key.to_sym] = value
  end
end

.verbose_fetch(hash, key) ⇒ Object

Fetches a key from a hash (just like ‘Hash#fetch`) but with a more verbose error message when the key is not found. The error message indicates the available keys unlike `Hash#fetch`.



14
15
16
17
18
# File 'lib/elastic_graph/support/hash_util.rb', line 14

def self.verbose_fetch(hash, key)
  hash.fetch(key) do
    raise ::KeyError, "key not found: #{key.inspect}. Available keys: #{hash.keys.inspect}."
  end
end