Class: Philiprehberger::DotAccess::Wrapper

Inherits:
Object
  • Object
show all
Includes:
Enumerable
Defined in:
lib/philiprehberger/dot_access.rb

Overview

Dot-notation wrapper for a hash

Constant Summary collapse

INDEX_SEGMENT =

Regex matching strings that should be treated as array indices (signed or unsigned integers).

/\A-?\d+\z/

Instance Method Summary collapse

Constructor Details

#initialize(hash) ⇒ Wrapper

Wrap the given Hash and freeze the resulting instance.

Parameters:

  • hash (Hash)

    the hash to wrap



126
127
128
129
130
131
# File 'lib/philiprehberger/dot_access.rb', line 126

def initialize(hash)
  @data = hash.each_with_object({}) do |(key, value), memo|
    memo[key.is_a?(String) ? key.to_sym : key] = value
  end
  freeze
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(name, *args) ⇒ Object (private)



388
389
390
391
392
393
394
395
396
# File 'lib/philiprehberger/dot_access.rb', line 388

def method_missing(name, *args)
  if @data.key?(name)
    wrap_value(@data[name])
  elsif name.to_s.end_with?('=') || !args.empty?
    super
  else
    NullAccess.new
  end
end

Instance Method Details

#==(other) ⇒ Boolean

Returns:

  • (Boolean)


380
381
382
383
384
# File 'lib/philiprehberger/dot_access.rb', line 380

def ==(other)
  return to_h == other.to_h if other.is_a?(Wrapper)

  false
end

#compactWrapper

Return a new Wrapper with all “nil“ values removed at every depth.

Returns:

  • (Wrapper)

    a new wrapper with nils removed from hashes and arrays



270
271
272
# File 'lib/philiprehberger/dot_access.rb', line 270

def compact
  Philiprehberger::DotAccess.wrap(deep_compact(to_h))
end

#delete(path) ⇒ Wrapper

Remove a key at a dot-path, returning a new Wrapper.

Integer-looking segments delete the matching array element when the current node is an Array.

Parameters:

  • path (String, Symbol)

    dot-separated key path

Returns:

  • (Wrapper)

    a new wrapper without the specified path



254
255
256
257
258
# File 'lib/philiprehberger/dot_access.rb', line 254

def delete(path)
  keys = path.to_s.split('.')
  new_data = deep_delete(to_h, keys)
  Wrapper.new(new_data)
end

#dig(key) ⇒ Object

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

Dig into nested keys.

Parameters:

  • key (Symbol)

    the key to look up

Returns:

  • (Object)

    the (possibly wrapped) value



369
370
371
372
# File 'lib/philiprehberger/dot_access.rb', line 369

def dig(key)
  value = @data[key]
  wrap_value(value)
end

#each {|Symbol, Object| ... } ⇒ Enumerator Also known as: each_pair

Iterate over top-level key-value pairs.

Nested Hash values are yielded as wrapped Philiprehberger::DotAccess::Wrapper instances so block callers can chain dot-notation.

Yields:

  • (Symbol, Object)

    each key and its (possibly wrapped) value

Returns:

  • (Enumerator)

    if no block is given



308
309
310
311
312
313
314
# File 'lib/philiprehberger/dot_access.rb', line 308

def each(&)
  return enum_for(:each) unless block_given?

  @data.each do |key, value|
    yield key, wrap_value(value)
  end
end

#empty?Boolean

Returns “true“ if the wrapped hash has no keys.

Returns:

  • (Boolean)

    “true“ if the wrapped hash has no keys



319
320
321
# File 'lib/philiprehberger/dot_access.rb', line 319

def empty?
  @data.empty?
end

#exists?(path) ⇒ Boolean

Check whether a dot-separated path exists in the wrapped structure.

Parameters:

  • path (String, Symbol)

    dot-separated key path

Returns:

  • (Boolean)

    “true“ if the path exists (even when the value at the path is “nil“)



182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
# File 'lib/philiprehberger/dot_access.rb', line 182

def exists?(path)
  keys = path.to_s.split('.')
  current = @data
  keys.each do |key|
    case current
    when Hash
      return false unless current.key?(key.to_sym)

      current = current[key.to_sym]
    when Array
      index = array_index(key, current)
      return false if index.nil?

      current = current[index]
    when Wrapper
      return false unless current.key?(key.to_sym)

      current = current[key.to_sym]
    else
      return false
    end
  end
  true
end

#fetch!(path) ⇒ Object

Fetch a value at a dot-path, raising if missing.

Parameters:

  • path (String, Symbol)

    dot-separated key path

Returns:

  • (Object)

    the value at the path

Raises:

  • (KeyError)

    if the path does not exist



220
221
222
223
224
# File 'lib/philiprehberger/dot_access.rb', line 220

def fetch!(path)
  raise KeyError, "path not found: #{path.inspect}" unless exists?(path)

  get(path)
end

#flattenHash

Flatten the nested structure into a hash whose keys are dot-paths.

Returns:

  • (Hash)

    flat hash where keys are dot-path strings



263
264
265
# File 'lib/philiprehberger/dot_access.rb', line 263

def flatten
  flatten_hash(@data, '')
end

#get(path, default: nil) ⇒ Object

Access a value by dot-path string.

Path segments that look like integers (e.g. “0“, “-1“) are interpreted as array indices when the value currently being traversed is an Array. Otherwise they are treated as Hash keys.

Parameters:

  • path (String, Symbol)

    dot-separated key path

  • default (Object) (defaults to: nil)

    value returned if the path is not found

Returns:

  • (Object)

    the value at the path, or “default“



142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
# File 'lib/philiprehberger/dot_access.rb', line 142

def get(path, default: nil)
  keys = path.to_s.split('.')
  result = keys.reduce(@data) do |current, key|
    case current
    when Hash then current[key.to_sym]
    when Array
      index = array_index(key, current)
      return default if index.nil?

      current[index]
    when Wrapper then current[key.to_sym]
    else return default
    end
  end

  result.nil? ? default : result
end

#inspectString

Returns:

  • (String)


375
376
377
# File 'lib/philiprehberger/dot_access.rb', line 375

def inspect
  "#<DotAccess::Wrapper #{to_h.inspect}>"
end

#key?(key) ⇒ Boolean

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

Check if a key exists in the underlying data.

Parameters:

  • key (Symbol)

    the key to check

Returns:

  • (Boolean)


351
352
353
# File 'lib/philiprehberger/dot_access.rb', line 351

def key?(key)
  @data.key?(key)
end

#keys(depth: nil) ⇒ Array<String>

List every dot-path in the wrapped structure.

Parameters:

  • depth (Integer, nil) (defaults to: nil)

    maximum traversal depth (“nil“ for unlimited)

Returns:

  • (Array<String>)

    dot-path strings



211
212
213
# File 'lib/philiprehberger/dot_access.rb', line 211

def keys(depth: nil)
  collect_keys(@data, '', depth, 1)
end

#merge(other) ⇒ Wrapper

Deep merge with another Wrapper or Hash.

Parameters:

  • other (Wrapper, Hash)

    the other structure to merge

Returns:

  • (Wrapper)

    a new wrapper with merged values



278
279
280
281
282
# File 'lib/philiprehberger/dot_access.rb', line 278

def merge(other)
  other_hash = other.is_a?(Wrapper) ? other.to_h : symbolize_keys(other)
  merged = deep_merge(to_h, other_hash)
  Wrapper.new(merged)
end

#set(path, value) ⇒ Wrapper

Set a value at a dot-path, returning a new Wrapper.

Integer-looking path segments are applied as array indices when the current node is an Array. Out-of-bounds indices raise ArgumentError; negative indices follow Ruby conventions.

Parameters:

  • path (String, Symbol)

    dot-separated key path

  • value (Object)

    the value to set

Returns:

  • (Wrapper)

    a new wrapper with the updated value

Raises:

  • (ArgumentError)

    when an integer segment is out of bounds for the current Array node



171
172
173
174
175
# File 'lib/philiprehberger/dot_access.rb', line 171

def set(path, value)
  keys = path.to_s.split('.')
  new_data = deep_set(to_h, keys, value)
  Wrapper.new(new_data)
end

#sizeInteger Also known as: count

Returns the number of top-level keys.

Returns:

  • (Integer)

    the number of top-level keys



324
325
326
# File 'lib/philiprehberger/dot_access.rb', line 324

def size
  @data.size
end

#slice(*paths) ⇒ Wrapper

Return a new Wrapper containing only the specified dot-paths.

Parameters:

  • paths (Array<String, Symbol>)

    dot-separated key paths to retain

Returns:

  • (Wrapper)

    a new wrapper with only the given paths



230
231
232
233
234
235
236
237
# File 'lib/philiprehberger/dot_access.rb', line 230

def slice(*paths)
  new_data = paths.reduce({}) do |acc, path|
    next acc unless exists?(path)

    deep_set(acc, path.to_s.split('.'), get(path))
  end
  Wrapper.new(new_data)
end

#to_hHash

Return the underlying hash with symbol keys.

Returns:

  • (Hash)

    the original hash structure



358
359
360
361
362
# File 'lib/philiprehberger/dot_access.rb', line 358

def to_h
  @data.each_with_object({}) do |(key, value), memo|
    memo[key] = value.is_a?(Wrapper) ? value.to_h : value
  end
end

#to_json(*args) ⇒ String

Serialize the wrapped hash to a JSON string.

Parameters:

  • args (Array<Object>)

    passed through to “Hash#to_json“

Returns:

  • (String)

    JSON representation



334
335
336
# File 'lib/philiprehberger/dot_access.rb', line 334

def to_json(*args)
  to_h.to_json(*args)
end

#to_yaml(*args) ⇒ String

Serialize the wrapped hash to a YAML string.

Parameters:

  • args (Array<Object>)

    passed through to “Hash#to_yaml“

Returns:

  • (String)

    YAML representation



342
343
344
# File 'lib/philiprehberger/dot_access.rb', line 342

def to_yaml(*args)
  to_h.to_yaml(*args)
end

#update(paths_hash) ⇒ Wrapper

Batch-set multiple dot-paths, returning a new Wrapper.

Applies every entry in “paths_hash“ in iteration order, following the same semantics as #set. The receiver is not mutated.

Parameters:

  • paths_hash (Hash{String, Symbol => Object})

    map of dot-paths to values

Returns:

  • (Wrapper)

    a new frozen wrapper with every path applied

Raises:

  • (ArgumentError)

    when an integer segment is out of bounds for an Array node (propagated from #set)



294
295
296
297
298
299
# File 'lib/philiprehberger/dot_access.rb', line 294

def update(paths_hash)
  new_data = paths_hash.reduce(to_h) do |acc, (path, value)|
    deep_set(acc, path.to_s.split('.'), value)
  end
  Wrapper.new(new_data)
end

#values_at(*paths) ⇒ Array<Object>

Return values at the given dot-paths as an array.

Parameters:

  • paths (Array<String, Symbol>)

    dot-separated key paths

Returns:

  • (Array<Object>)

    values in the order of the given paths



243
244
245
# File 'lib/philiprehberger/dot_access.rb', line 243

def values_at(*paths)
  paths.map { |path| get(path) }
end