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

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.



136
137
138
139
140
141
142
143
144
145
# File 'lib/collapsium/support/methods.rb', line 136

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.



128
129
130
131
132
# File 'lib/collapsium/support/methods.rb', line 128

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

Instance Method Details

#resolve_helpers(base, method_name, raise_on_missing) ⇒ Object



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

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 base_method, def_method
    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.
    base_method = base.method(method_name.to_s).unbind
    # 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

“‘



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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
# File 'lib/collapsium/support/methods.rb', line 43

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

  # Hack for calling the private method "define_method"
  def_method.call(method_name) 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(JSON::dump(args))
    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
end