Class: Hash

Inherits:
Object
  • Object
show all
Defined in:
lib/everythingrb/core/hash.rb

Overview

Extensions to Ruby’s core Hash class

These additions make working with hashes more convenient by adding conversion methods to different data structures and string formatting helpers.

Examples:

Converting to different structures

user = { name: "Alice", roles: ["admin"] }
user.to_struct    # => #<struct name="Alice", roles=["admin"]>
user.to_ostruct   # => #<OpenStruct name="Alice", roles=["admin"]>

# Filtering and joining hash entries
{ a: 1, b: nil, c: 3 }.join_map(", ") { |k, v| "#{k}:#{v}" if v }
# => "a:1, c:3"

Constant Summary collapse

EMPTY_STRUCT =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

A minimal empty struct for Ruby 3.2+ compatibility

Ruby 3.2 enforces stricter argument handling for Struct. This means trying to create a Struct from an empty Hash will result in an ArgumentError being raised. This is trying to keep a consistent experience with that version and newer versions.

Returns:

  • (Struct)

    A struct with a single nil-valued field

Struct.new(:_).new(nil)

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.new_nested_hashHash

Note:

While extremely convenient, be cautious with very deep structures as this creates new hashes on demand for any key access

Creates a new Hash that automatically initializes missing keys with nested hashes

This method creates a hash where any missing key access will automatically create another nested hash with the same behavior, allowing for unlimited nesting depth without explicit initialization.

Examples:

Basic usage with two levels

users = Hash.new_nested_hash
users[:john][:role] = "admin"  # No need to initialize users[:john] first
users # => {john: {role: "admin"}}

Deep nesting without initialization

stats = Hash.new_nested_hash
(stats[:server][:region][:us_east][:errors] = []) << "Some Error"
stats # => {server: {region: {us_east: {errors: ["Some Error"]}}}}

Returns:

  • (Hash)

    A hash that recursively creates nested hashes for missing keys



54
55
56
# File 'lib/everythingrb/core/hash.rb', line 54

def self.new_nested_hash
  new { |hash, key| hash[key] = new_nested_hash }
end

Instance Method Details

#deep_freezeself

Note:

CAUTION: Be careful when freezing collections that contain class objects or singleton instances - this will freeze those classes/objects globally! Only use deep_freeze on pure data structures you want to make immutable.

Recursively freezes self and all of its values

Examples:

{ user: { name: "Alice", roles: ["admin"] } }.deep_freeze
# => Hash and all nested structures are now frozen

Returns:

  • (self)

    Returns the frozen hash



223
224
225
226
# File 'lib/everythingrb/core/hash.rb', line 223

def deep_freeze
  each_value { |v| v.respond_to?(:deep_freeze) ? v.deep_freeze : v.freeze }
  freeze
end

#join_map(join_with = "") {|key, value| ... } ⇒ String

Combines filter_map and join operations

Examples:

{ a: 1, b: nil, c: 2, d: nil, e: 3 }.join_map(", ") { |k, v| "#{k}-#{v}" if v }
# => "a-1, c-2, e-3"

Without a block

{ a: 1, b: nil, c: 2 }.join_map(" ")
# => "a 1 b c 2"

Parameters:

  • join_with (String) (defaults to: "")

    The delimiter to join elements with (defaults to empty string)

Yields:

  • (key, value)

    Block that filters and transforms hash entries

Yield Parameters:

  • key (Object)

    The current key

  • value (Object)

    The current value

Returns:

  • (String)

    Joined string of filtered and transformed entries



77
78
79
80
81
# File 'lib/everythingrb/core/hash.rb', line 77

def join_map(join_with = "", &block)
  block = ->(kv_pair) { kv_pair.compact } if block.nil?

  filter_map(&block).join(join_with)
end

#og_transform_valuesObject

Allows calling original method. See below



229
# File 'lib/everythingrb/core/hash.rb', line 229

alias_method :og_transform_values, :transform_values

#og_transform_values!Object

Allows calling original method. See below



232
# File 'lib/everythingrb/core/hash.rb', line 232

alias_method :og_transform_values!, :transform_values!

#to_deep_hHash

Recursively converts all values that respond to #to_h

Similar to #to_h but recursively traverses the Hash structure and calls #to_h on any object that responds to it. Useful for normalizing nested data structures and parsing nested JSON.

Examples:

Converting nested Data objects

user = { name: "Alice", metadata: Data.define(:source).new(source: "API") }
user.to_deep_h  # => {name: "Alice", metadata: {source: "API"}}

Parsing nested JSON strings

nested = { profile: '{"role":"admin"}' }
nested.to_deep_h  # => {profile: {role: "admin"}}

Mixed nested structures

data = {
  config: OpenStruct.new(api_key: "secret"),
  users: [
    Data.define(:name).new(name: "Bob"),
    {role: "admin"}
  ]
}
data.to_deep_h
# => {
#      config: {api_key: "secret"},
#      users: [{name: "Bob"}, {role: "admin"}]
#    }

Returns:

  • (Hash)

    A deeply converted hash with all nested objects converted



114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
# File 'lib/everythingrb/core/hash.rb', line 114

def to_deep_h
  transform_values do |value|
    case value
    when Hash
      value.to_deep_h
    when Array
      value.to_deep_h
    when String
      # If the string is not valid JSON, #to_deep_h will return `nil`
      value.to_deep_h || value
    else
      value.respond_to?(:to_h) ? value.to_h : value
    end
  end
end

#to_istructData

Converts hash to an immutable Data structure

Examples:

hash = { person: { name: "Bob", age: 30 } }
data = hash.to_istruct
data.person.name # => "Bob"
data.class # => Data

Returns:

  • (Data)

    An immutable Data object with the same structure



141
142
143
144
145
146
147
148
149
150
151
152
153
154
# File 'lib/everythingrb/core/hash.rb', line 141

def to_istruct
  recurse = lambda do |input|
    case input
    when Hash
      input.to_istruct
    when Array
      input.map(&recurse)
    else
      input
    end
  end

  Data.define(*keys.map(&:to_sym)).new(*values.map { |value| recurse.call(value) })
end

#to_ostructOpenStruct

Converts hash to an OpenStruct recursively

Examples:

hash = { config: { api_key: "secret" } }
config = hash.to_ostruct
config.config.api_key # => "secret"

Returns:

  • (OpenStruct)

    An OpenStruct with methods matching hash keys



195
196
197
198
199
200
201
202
203
204
205
206
207
208
# File 'lib/everythingrb/core/hash.rb', line 195

def to_ostruct
  recurse = lambda do |value|
    case value
    when Hash
      value.to_ostruct
    when Array
      value.map(&recurse)
    else
      value
    end
  end

  OpenStruct.new(**transform_values { |value| recurse.call(value) })
end

#to_structStruct

Converts hash to a Struct recursively

Examples:

hash = { user: { name: "Alice", roles: ["admin"] } }
struct = hash.to_struct
struct.user.name # => "Alice"
struct.class # => Struct

Returns:

  • (Struct)

    A struct with methods matching hash keys



167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
# File 'lib/everythingrb/core/hash.rb', line 167

def to_struct
  # For Ruby 3.2, it raises if you attempt to create a Struct with no keys
  return EMPTY_STRUCT if RUBY_VERSION.start_with?("3.2") && empty?

  recurse = lambda do |value|
    case value
    when Hash
      value.to_struct
    when Array
      value.map(&recurse)
    else
      value
    end
  end

  Struct.new(*keys.map(&:to_sym)).new(*values.map { |value| recurse.call(value) })
end

#transform_values {|value| ... } ⇒ Hash, Enumerator

Transforms hash values while allowing access to keys via the chainable with_key method

This method either performs a standard transform_values operation if a block is given, or returns an enumerator with a with_key method that passes both the key and value to the block.

Examples:

Standard transform_values

{a: 1, b: 2}.transform_values { |v| v * 2 }
# => {a: 2, b: 4}

Using with_key to access keys during transformation

{a: 1, b: 2}.transform_values.with_key { |k, v| "#{k}_#{v}" }
# => {a: "a_1", b: "b_2"}

Yields:

  • (value)

    Block to transform each value (standard behavior)

Yield Parameters:

  • value (Object)

    The value to transform

Yield Returns:

  • (Object)

    The transformed value

Returns:

  • (Hash, Enumerator)

    Result hash or Enumerator with with_key method



255
256
257
258
259
# File 'lib/everythingrb/core/hash.rb', line 255

def transform_values(&block)
  return transform_values_enumerator if block.nil?

  og_transform_values(&block)
end

#transform_values! {|value| ... } ⇒ self, Enumerator

Transforms hash values in place while allowing access to keys via the chainable with_key method

This method either performs a standard transform_values! operation if a block is given, or returns an enumerator with a with_key method that passes both the key and value to the block, updating the hash in place.

Examples:

Standard transform_values!

hash = {a: 1, b: 2}
hash.transform_values! { |v| v * 2 }
# => {a: 2, b: 4}

Using with_key to access keys during in-place transformation

hash = {a: 1, b: 2}
hash.transform_values!.with_key { |k, v| "#{k}_#{v}" }
# => {a: "a_1", b: "b_2"}

Yields:

  • (value)

    Block to transform each value (standard behavior)

Yield Parameters:

  • value (Object)

    The value to transform

Yield Returns:

  • (Object)

    The transformed value

Returns:

  • (self, Enumerator)

    Original hash with transformed values or Enumerator with with_key method



285
286
287
288
289
# File 'lib/everythingrb/core/hash.rb', line 285

def transform_values!(&block)
  return transform_values_bang_enumerator if block.nil?

  og_transform_values!(&block)
end

#value_where {|key, value| ... } ⇒ Object, ...

Returns the first value where the key-value pair satisfies the given condition

Examples:

Find first admin user by role

users = {
  alice: {name: "Alice", role: "admin"},
  bob: {name: "Bob", role: "user"},
  charlie: {name: "Charlie", role: "admin"}
}
users.value_where { |k, v| v[:role] == "admin" } # => {name: "Alice", role: "admin"}

Yields:

  • (key, value)

    Block that determines whether to include the value

Yield Parameters:

  • key (Object)

    The current key

  • value (Object)

    The current value

Yield Returns:

  • (Boolean)

    Whether to include this value

Returns:

  • (Object, nil)

    The first matching value or nil if none found

  • (Enumerator)

    If no block is given



310
311
312
313
314
# File 'lib/everythingrb/core/hash.rb', line 310

def value_where(&block)
  return to_enum(:value_where) if block.nil?

  find(&block)&.last
end

#values_where {|key, value| ... } ⇒ Array, Enumerator

Returns all values where the key-value pairs satisfy the given condition

Examples:

Find all admin users by role

users = {
  alice: {name: "Alice", role: "admin"},
  bob: {name: "Bob", role: "user"},
  charlie: {name: "Charlie", role: "admin"}
}
users.values_where { |k, v| v[:role] == "admin" }
# => [{name: "Alice", role: "admin"}, {name: "Charlie", role: "admin"}]

Yields:

  • (key, value)

    Block that determines whether to include the value

Yield Parameters:

  • key (Object)

    The current key

  • value (Object)

    The current value

Yield Returns:

  • (Boolean)

    Whether to include this value

Returns:

  • (Array)

    All matching values

  • (Enumerator)

    If no block is given



336
337
338
339
340
# File 'lib/everythingrb/core/hash.rb', line 336

def values_where(&block)
  return to_enum(:values_where) if block.nil?

  select(&block).values
end