Module: Collapsium::Support::Methods

Included in:
EnvironmentOverride, IndifferentAccess, PathedAccess, ViralCapabilities, ViralCapabilities
Defined in:
lib/collapsium/support/methods.rb

Overview

Functionality for extending the behaviour of Hash methods

Constant Summary collapse

WRAPPER_HASH =
"@__collapsium_methods_wrappers".freeze
BUILTINS =

class << self

Methods.builtins.freeze

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.builtinsObject

Return built-in symbols. It’s called to initialize the BUILTINS constant early on. It also caches its result, so should be safe to call later, too.



30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# File 'lib/collapsium/support/methods.rb', line 30

def builtins
  @builtins ||= nil
  if not @builtins.nil?
    return @builtins
  end

  # Object's constants contain all Module and Class definitions to date.
  # We need to get the constant values, though, not just their names.
  # Note: the $VERBOSE mess is to silence deprecation warnings, which
  #       occur on newer Ruby versions.
  verbose = $VERBOSE
  $VERBOSE = nil
  builtins = Object.constants.sort.map do |const_name|
    Object.const_get(const_name)
  end
  $VERBOSE = verbose

  # If JSON was required, there will be some generator methods that
  # override the above generators. We want to filter those out as well.
  # If, however, JSON was not required, we'll get a NameError and won't
  # add anything new.
  # rubocop:disable Lint/HandleExceptions
  begin
    json_builtins = JSON::Ext::Generator::GeneratorMethods.constants.sort
    json_builtins.map! do |const_name|
      JSON::Ext::Generator::GeneratorMethods.const_get(const_name)
    end
    builtins += json_builtins
  rescue NameError
    # We just ignore this; if JSON is automatically required, there will
    # be some mixin Modules here, otherwise we will just process them.
  end
  # rubocop:enable Lint/HandleExceptions

  # Last, we want to filter, so only Class and Module items are kept.
  builtins.select! do |item|
    item.is_a?(Module) or item.is_a?(Class)
  end

  @builtins = builtins
  return @builtins
end

.loop_detected?(the_binding, stack) ⇒ Boolean

Given a call stack and a binding, returns true if there seems to be a loop in the call stack with the binding causing it, false otherwise.

Returns:

  • (Boolean)


263
264
265
266
267
268
269
270
271
272
# File 'lib/collapsium/support/methods.rb', line 263

def loop_detected?(the_binding, stack)
  # Make a temporary stack with the binding pushed
  tmp_stack = stack.dup
  tmp_stack << the_binding
  loops = Methods.repeated(tmp_stack)

  # If we do find a loop with the current binding involved, we'll just
  # call the wrapped method.
  return loops.include?(the_binding)
end

.repeated(array) ⇒ Object

Given an input array, return repeated sequences from the array. It’s used in loop detection.



255
256
257
258
259
# File 'lib/collapsium/support/methods.rb', line 255

def repeated(array)
  counts = Hash.new(0)
  array.each { |val| counts[val] += 1 }
  return counts.reject { |_, count| count == 1 }.keys
end

.wrappers(base, method_name, visited = nil) ⇒ Object

Given any base (value, class, module) and a method name, returns the wrappers defined for the base, in order of definition. If no wrappers are defined, an empty Array is returned.



201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
# File 'lib/collapsium/support/methods.rb', line 201

def wrappers(base, method_name, visited = nil)
  # First, check the instance, then its class for a wrapper. If either of
  # them succeeds, exit with a result.
  [base, base.class].each do |item|
    item_wrappers = item.instance_variable_get(WRAPPER_HASH)
    if not item_wrappers.nil? and item_wrappers.include?(method_name)
      return item_wrappers[method_name]
    end
  end

  # If neither of the above contained a wrapper, look at ancestors
  # recursively.
  ancestors = nil
  begin
    ancestors = base.ancestors
  rescue NoMethodError
    ancestors = base.class.ancestors
  end
  ancestors = ancestors - Object.ancestors - BUILTINS

  # Bail out if there are no ancestors to process.
  if ancestors.empty?
    return []
  end

  # We add the base and its class to the set of visited items. Note
  # that we're doing it late, so we only have to do it when we have
  # ancestors to visit.
  if visited.nil?
    visited = Set.new
  end
  visited.add(base)
  visited.add(base.class)

  ancestors.each do |ancestor|
    # Skip an visited item...
    if visited.include?(ancestor)
      next
    end
    visited.add(ancestor)

    # ... and recurse into unvisited ones
    anc_wrappers = wrappers(ancestor, method_name, visited)
    if not anc_wrappers.empty?
      return anc_wrappers
    end
  end

  # Return an empty list if we couldn't find anything.
  return []
end

Instance Method Details

#resolve_helpers(base, method_name, raise_on_missing) ⇒ Object



159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
# File 'lib/collapsium/support/methods.rb', line 159

def resolve_helpers(base, method_name, raise_on_missing)
  # The base class must define an instance method of method_name, otherwise
  # this will NameError. That's also a good check that sensible things are
  # being done.
  base_method = nil
  def_method = nil
  if base.is_a? Module
    # Modules *may* not be fully defined when this is called, so in some
    # cases it's best to ignore NameErrors.
    begin
      base_method = base.instance_method(method_name.to_sym)
    rescue NameError
      if raise_on_missing
        raise
      end
      return nil, nil, nil
    end
    def_method = base.method(:define_method)
  else
    # For Objects and Classes, the unbound method will later be bound to
    # the object or class to define the method on.
    begin
      base_method = base.method(method_name.to_s).unbind
    rescue NameError
      if raise_on_missing
        raise
      end
      return nil, nil, nil
    end
    # With regards to method defintion, we only want to define methods
    # for the specific instance (i.e. use :define_singleton_method).
    def_method = base.method(:define_singleton_method)
  end

  return base_method, def_method
end

#wrap_method(base, method_name, options = {}, &wrapper_block) ⇒ Object

Given the base module, wraps the given method name in the given block. The block must accept the wrapped_method as the first parameter, followed by any arguments and blocks the super method might accept.

The canonical usage example is of a module that when prepended wraps some methods with extra functionality:

“‘ruby

module MyModule
  class << self
    include ::Collapsium::Support::Methods

    def prepended(base)
      wrap_method(base, :method_name) do |wrapped_method, *args, &block|
        # modify args, if desired
        result = wrapped_method.call(*args, &block)
        # do something with the result, if desired
        next result
      end
    end
  end
end

“‘



99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
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
# File 'lib/collapsium/support/methods.rb', line 99

def wrap_method(base, method_name, options = {}, &wrapper_block)
  # Option defaults (need to check for nil if we default to true)
  if options[:raise_on_missing].nil?
    options[:raise_on_missing] = true
  end

  # Grab helper methods
  base_method, def_method = resolve_helpers(base, method_name,
                                            options[:raise_on_missing])
  if base_method.nil?
    # Indicates that we're not done building a Module yet
    return
  end

  wrap_method_block = proc do |*args, &method_block|
    # We're trying to prevent loops by maintaining a stack of wrapped
    # method invocations.
    @__collapsium_methods_callstack ||= []

    # Our current binding is based on the wrapper block and our own class,
    # as well as the arguments (CRC32).
    signature = Zlib.crc32(args.to_s)
    the_binding = [wrapper_block.object_id, self.class.object_id, signature]

    # We'll either pass the wrapped method to the wrapper block, or invoke
    # it ourselves.
    wrapped_method = base_method.bind(self)

    # If we do find a loop with the current binding involved, we'll just
    # call the wrapped method.
    if Methods.loop_detected?(the_binding, @__collapsium_methods_callstack)
      next wrapped_method.call(*args, &method_block)
    end

    # If there is no loop, call the wrapper block and pass along the
    # wrapped method as the first argument.
    args.unshift(wrapped_method)

    # Then yield to the given wrapper block. The wrapper should decide
    # whether to call the old method or not. But by modifying our stack
    # before/after the invocation, we allow the loop detection above to
    # work.
    @__collapsium_methods_callstack << the_binding
    result = wrapper_block.call(*args, &method_block)
    @__collapsium_methods_callstack.pop

    next result
  end

  # Hack for calling the private method "define_method"
  def_method.call(method_name, &wrap_method_block)

  # Register this wrapper with the base
  base_wrappers = base.instance_variable_get(WRAPPER_HASH)
  base_wrappers ||= {}
  base_wrappers[method_name] ||= []
  base_wrappers[method_name] << wrapper_block
  base.instance_variable_set(WRAPPER_HASH, base_wrappers)
end