Class: Aws::Templates::Utils::Options

Inherits:
Object
  • Object
show all
Includes:
Exception, Memoized
Defined in:
lib/aws/templates/utils/options.rb

Overview

Options hash-like class

Implements the core mechanism of hash lookup, merge, and transformation.

It supports nested hash lookup with index function and wildcard hash definitions on any level of nested hierarchy so you can define fallback values right in the input hash. The algorithm will try to select the best combination if it doesn’t see the exact match during lookup.

Instance Method Summary collapse

Methods included from Memoized

#dirty!, #memoize, #memoized

Constructor Details

#initialize(*structures) ⇒ Options

Initialize Options with list of recursive structures (See Options#recursive?)



225
226
227
# File 'lib/aws/templates/utils/options.rb', line 225

def initialize(*structures)
  structures.reject(&:nil?).each { |container| merge!(container) }
end

Instance Method Details

#[](*path) ⇒ Object

Get a parameter from resulting hash or any nested part of it

The method can access resulting hash as a tree performing traverse as needed. Also, it handles nil-pointer situations correctly so you will get no exception but just ‘nil’ even when the whole branch you’re trying to access don’t exist or contains non-hash value somewhere in the middle. Also, the method recognizes asterisk (*) hash records which is an analog of match-all or default values for some sub-branch.

  • path - an array representing path of the value in the nested

    hash
    

Example

opts = Options.new(
  'a' => {
    'b' => 'c',
    '*' => { '*' => 2 }
  },
  'd' => 1
)
opts.to_hash # => { 'a' => { 'b' => 'c', '*' => { '*' => 2 } }, 'd' => 1 }
opts['a'] # => Options.new('b' => 'c', '*' => { '*' => 2 })
opts['a', 'b'] # => 'c'
opts['d', 'e'] # => nil
# multi-level wildcard match
opts['a', 'z', 'r'] # => 2


58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
# File 'lib/aws/templates/utils/options.rb', line 58

def [](*path)
  structures.reverse_each.inject(nil) do |memo, container|
    ret = begin
      Utils.lookup(container, path.dup)
    rescue Exception::OptionValueDeleted, Exception::OptionScalarOnTheWay
      # we discovered that this layer either have value deleted or parent was overriden
      # by a scalar. Either way we just return what we have in the memo
      break memo
    end

    # if current container doesn't have this value - let's go to the next iteration
    next memo if ret.nil?

    # if found value is a scalar then either we return it as is or return memo
    # if memo is not nil it means that we've found hierarchical objects before
    break(memo.nil? ? ret : memo) unless Utils.recursive?(ret)

    # value is not a scalar. it means we need to keep merging them
    memo.nil? ? Options.new(ret) : memo.unshift_layer!(ret)
  end
end

#[]=(*path_and_value) ⇒ Object

Set the parameter with the path to the value

The method can access resulting hash as a tree performing traverse as needed. When stubled uponAlso, it handles non-existent keys correctly creating sub-branches as necessary. If a non-hash and non-nil value discovered in the middle of the path, an exception will be thrown. The method doesn’t give any special meaning to wildcards keys so you can set wildcard parameters also.

  • path - an array representing path of the value in the nested

    hash
    
  • value - value to set the parameter to

Example

opts = Options.new({})
opts.to_hash # => {}
opts['a', 'b'] = 'c'
opts['a', '*', '*'] = 2
opts['d'] = 1
opts.to_hash # => { 'a' => { 'b' => 'c', '*' => { '*' => 2 } }, 'd' => 1 }


120
121
122
123
124
125
# File 'lib/aws/templates/utils/options.rb', line 120

def []=(*path_and_value)
  value = path_and_value.pop
  path = path_and_value
  dirty!.cow! # mooo
  Utils.set_recursively(structures.last, value, path)
end

#compact!Object

Squash all layers into one

Options is designed with very specific goal to be memory-friendly and re-use merged objects as immutable layers. However, after some particular threshold of layer’s stack size, performance of index operations can suffer significantly. To mitigate this user can use the method to squash all layers into one aggregated hash.

The method performs in-place stack modification



247
248
249
250
# File 'lib/aws/templates/utils/options.rb', line 247

def compact!
  @structures = [to_hash]
  self
end

#cow!Object



270
271
272
273
274
275
276
277
# File 'lib/aws/templates/utils/options.rb', line 270

def cow!
  unless @is_cowed
    structures << {}
    @is_cowed = true
  end

  self
end

#delete(*path) ⇒ Object

Delete a branch

Delete a branch in the options. Rather than deleting it from hash, the path is assigned with special marker that it was deleted. It helps avoid hash recalculation leading to memory thrashing simultaneously maintaining semantics close to Hash#delete



133
134
135
# File 'lib/aws/templates/utils/options.rb', line 133

def delete(*path)
  self[*path] = Utils::DELETED_MARKER
end

#dependenciesObject



84
85
86
87
88
89
90
91
92
# File 'lib/aws/templates/utils/options.rb', line 84

def dependencies
  # rubocop:disable Style/SymbolProc
  # Refinements don't support dynamic dispatch yet. So, symbolic methods don't work
  memoize(:dependencies) do
    select_recursively { |obj| obj.dependency? }
      .inject(::Set.new) { |acc, elem| acc.merge(elem.dependencies) }
  end
  # rubocop:enable Style/SymbolProc
end

#dependency?Boolean

Returns:

  • (Boolean)


80
81
82
# File 'lib/aws/templates/utils/options.rb', line 80

def dependency?
  !dependencies.empty?
end

#dupObject

Duplicate the options

Duplicates the object itself and puts another layer of hash map. All original hash maps are not touched if the duplicate is modified.



234
235
236
# File 'lib/aws/templates/utils/options.rb', line 234

def dup
  Options.new(*structures)
end

#filterObject

Filter options

Filter options with provided Proc. The proc should accept one parameter satisfying “recursive” contract. See Utils.recursive



219
220
221
# File 'lib/aws/templates/utils/options.rb', line 219

def filter
  Options.new(yield self)
end

#include?(k) ⇒ Boolean

If top-level key exists

Checks if top-level key exists. Deleted branches are excluded.

Returns:

  • (Boolean)


181
182
183
184
# File 'lib/aws/templates/utils/options.rb', line 181

def include?(k)
  found = structures.reverse_each.find { |container| container.include?(k) }
  !found.nil? && (found[k] != Utils::DELETED_MARKER)
end

#keysObject

Top-level keys

Produces a list of top-level keys from all layers. Deleted branches are not included.



165
166
167
168
169
170
171
172
173
174
175
# File 'lib/aws/templates/utils/options.rb', line 165

def keys
  memoize(:keys) do
    structures
      .each_with_object(::Set.new) do |container, keyset|
        container.keys.each do |k|
          container[k] == Utils::DELETED_MARKER ? keyset.delete(k) : keyset.add(k)
        end
      end
      .to_a
  end
end

#merge(other) ⇒ Object

Merge Options with object

Create new Options object which is a merge of the target Options instance with an object. The object must be “recusrsive” meaning it should satisfy minimum contract for “recursive”. See Utils::recursive? for details



192
193
194
# File 'lib/aws/templates/utils/options.rb', line 192

def merge(other)
  self.class.new(*structures, other)
end

#merge!(other) ⇒ Object

Merge Options with object in-place

Put the passed object as the top layer of the current instance. The object must be “recursive” meaning it should satisfy minimum contract for “recursive”. See Utils::recursive? for details



202
203
204
205
206
207
208
209
210
211
212
# File 'lib/aws/templates/utils/options.rb', line 202

def merge!(other)
  raise Exception::OptionShouldBeRecursive.new(other) unless Utils.recursive?(other)

  if other.is_a?(self.class)
    structures.concat(other.structures)
  else
    structures << other
  end

  dirty!
end

#select_recursively(&blk) ⇒ Object



94
95
96
# File 'lib/aws/templates/utils/options.rb', line 94

def select_recursively(&blk)
  Utils.select_recursively(self, &blk)
end

#structuresObject



25
26
27
# File 'lib/aws/templates/utils/options.rb', line 25

def structures
  @structures ||= []
end

#to_filterObject

Create filter

Gets hash representstion of the Options instance and transforms it to filter



157
158
159
# File 'lib/aws/templates/utils/options.rb', line 157

def to_filter
  to_hash.to_filter
end

#to_hashObject

Transforms to hash object

Produces a hash out of Options object merging COW layers iteratively and calculating them recursively.



142
143
144
145
146
# File 'lib/aws/templates/utils/options.rb', line 142

def to_hash
  memoize(:to_hash) do
    _process_hashed(structures.inject({}) { |acc, elem| Utils.merge(acc, elem) })
  end
end

#to_recursiveObject

The class already supports recursive concept so return self



149
150
151
# File 'lib/aws/templates/utils/options.rb', line 149

def to_recursive
  self
end

#unshift_layer!(layer) ⇒ Object

Put the layer to the bottom of the stack

However it doesn’t resemble exact semantics, the method is similar to reverse_merge! from ActiveSupport. It puts the “recursive” object passed to the bottom of the layer stack of the Options instance effectively making it the least prioritized layer.



258
259
260
261
262
263
264
265
266
267
268
# File 'lib/aws/templates/utils/options.rb', line 258

def unshift_layer!(layer)
  raise Exception::OptionShouldBeRecursive.new(layer) unless Utils.recursive?(layer)

  if layer.is_a?(self.class)
    layer.structures.dup.concat(structures)
  else
    structures.unshift(layer)
  end

  dirty!
end