Module: Collapsium::Support::Methods

Included in:
EnvironmentOverride, 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

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.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)


200
201
202
203
204
205
206
207
208
209
# File 'lib/collapsium/support/methods.rb', line 200

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.



192
193
194
195
196
# File 'lib/collapsium/support/methods.rb', line 192

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 = Set.new) ⇒ 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.



148
149
150
151
152
153
154
155
156
157
158
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
# File 'lib/collapsium/support/methods.rb', line 148

def wrappers(base, method_name, visited = Set.new)
  # 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

  # We add the base and its class to the set of visited items.
  visited.add(base)
  visited.add(base.class)

  # 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 - [Object, Class, Module]

  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



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
# File 'lib/collapsium/support/methods.rb', line 106

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

“‘



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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
# File 'lib/collapsium/support/methods.rb', line 45

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).
    require 'zlib'
    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