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.



101
102
103
104
105
106
107
108
109
110
# File 'lib/elastic_graph/support/hash_util.rb', line 101

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.



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

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 an array of path parts. 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.



119
120
121
# File 'lib/elastic_graph/support/hash_util.rb', line 119

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

.fetch_value_at_path(hash, path_parts) ⇒ Object

Fetches a single value from the hash at the given path. The ‘path_parts` is expected to be an array.

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.

Note: this is a somewhat lengthy implementation, but it was chosen based on benchmarking. This method needs to be fast because it gets used repeatedly when resolving GraphQL queries at all levels of the response structure.



132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
# File 'lib/elastic_graph/support/hash_util.rb', line 132

def self.fetch_value_at_path(hash, path_parts)
  # We expect the most common case to be a single path part. Benchmarks have shown that special casing it
  # is quite worthwhile, as it is much faster than the general purpose implementation further below.
  if path_parts.size == 1
    return hash.fetch(path_parts.first) do
      return yield path_parts if block_given?
      raise KeyError, "Key not found: #{path_parts.inspect}"
    end
  end

  current = hash
  i = 0

  while i < path_parts.length
    key = path_parts[i]

    unless current.is_a?(Hash)
      raise KeyError, "Value at key #{path_parts.first(i).inspect} is not a `Hash` as expected; " \
        "instead, was a `#{current.class}`"
    end

    current = current.fetch(key) do
      missing_path = path_parts.first(i + 1)
      return yield missing_path if block_given?
      raise KeyError, "Key not found: #{missing_path.inspect}"
    end

    i += 1
  end

  current
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 }



90
91
92
93
94
95
96
97
# File 'lib/elastic_graph/support/hash_util.rb', line 90

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.



75
76
77
78
79
80
81
82
83
# File 'lib/elastic_graph/support/hash_util.rb', line 75

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.



68
69
70
# File 'lib/elastic_graph/support/hash_util.rb', line 68

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.



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

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.



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

def self.stringify_keys(object)
  transform_keys(object, :to_s)
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.



61
62
63
# File 'lib/elastic_graph/support/hash_util.rb', line 61

def self.symbolize_keys(object)
  transform_keys(object, :to_sym)
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`.



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

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