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
-
.builtins ⇒ Object
Return built-in symbols.
-
.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.
-
.repeated(array) ⇒ Object
Given an input array, return repeated sequences from the array.
-
.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.
Instance Method Summary collapse
- #resolve_helpers(base, method_name, raise_on_missing) ⇒ Object
-
#wrap_method(base, method_name, options = {}, &wrapper_block) ⇒ Object
Given the base module, wraps the given method name in the given block.
Class Method Details
.builtins ⇒ Object
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.
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, = {}, &wrapper_block) # Option defaults (need to check for nil if we default to true) if [:raise_on_missing].nil? [:raise_on_missing] = true end # Grab helper methods base_method, def_method = resolve_helpers(base, method_name, [: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 |