module Fortitude module MethodOverriding class << self # This is Fortitude’s way of maintaining compatibility both with Ruby < 2.0 (no support for Module#prepend) # and Ruby 2.0 and later (alias_method_chain is deprecated). Here’s how it works: # # * For a method 'foo' that you want to override using a 'feature name' of bar, you define a method called # 'foo_uniwith_bar' -- but not in the target module or class (containing the method to be overridden); rather, # it must be in a separate Module, and, importantly, not one that you have already Module#include'd into the # target module or class. This method has the same signature as the original, except that it also takes, as a first # parameter, a #call'able object (typically a Proc or lambda) that represents the original, un-overridden # method. You use this, instead of calling 'foo_without_bar' or 'super', in order to invoke the original # method. # * You then call Fortitude::MethodOverriding.override_methods. +target_module+ is the module containing the # method you want to override, +override_methods_module+ is the module containing your overriding method # ('foo_uniwith_bar'), +feature_name+ is the name of the feature you're using ('bar'), and +method_names+ # is an Array of Symbols, each of which is the name of a method you want to override (_e.g._, 'foo'). # # This class then performs the appropriate logic to use 'alias_method_chain' or Module#prepend appropriately. # # One exception: using Module#prepend seems to cause Fortitude all kinds of problems with JRuby 9.1.5.0 (as of # this writing, the latest version of JRuby). In particular, you get things like a java.lang.BootstrapMethodError # at "require at org/jruby/RubyKernel.java:956", and various java.lang.StackOverflowErrors that seem to make no # sense at all -- and if you use alias_method_chain instead, everything seems to work perfectly. As a result, # we currently fall back to using alias_method_chain on JRuby. (You only get deprecation warnings with this # when running with Rails 5, which is not yet supported by JRuby anyway, at least as of this writing.) def override_methods(target_module, override_methods_module, feature_name, method_names) if RUBY_VERSION =~ /^2\./ && (! ((RUBY_ENGINE || '').to_s.downcase.strip == 'jruby')) override_methods_using_prepend(target_module, override_methods_module, feature_name, method_names) else override_methods_using_alias_method_chain(target_module, override_methods_module, feature_name, method_names) end end private def override_methods_using_prepend(target_module, override_methods_module, feature_name, method_names) method_names.each do |method_name| universal_name = universal_method_name(method_name, feature_name) override_methods_module.class_eval <<-EOS def #{method_name}(*args, &block) original_method = Proc.new { |*args, &block| super(*args, &block) } #{universal_name}(original_method, *args, &block) end EOS end target_module.send(:prepend, override_methods_module) end def override_methods_using_alias_method_chain(target_module, override_methods_module, feature_name, method_names = nil) method_names.each do |method_name| universal_name = universal_method_name(method_name, feature_name) with_name = with_feature_name(method_name, feature_name) without_name = without_feature_name(method_name, feature_name) override_methods_module.class_eval <<-EOS def #{with_name}(*args, &block) original_method = Proc.new { |*args, &block| #{without_name}(*args, &block) } #{universal_name}(original_method, *args, &block) end EOS target_module.send(:include, override_methods_module) target_module.send(:alias_method_chain, method_name, feature_name) end end def suffix_method_name(method_name, suffix) if method_name.to_s =~ /^(.*?)([\?\_\!]+)$/i "#{$1}#{suffix}#{$2}" else "#{method_name}#{suffix}" end end def with_feature_name(method_name, feature_name) suffix_method_name(method_name, "_with_#{feature_name}") end def without_feature_name(method_name, feature_name) suffix_method_name(method_name, "_without_#{feature_name}") end def universal_method_name(method_name, feature_name) suffix_method_name(method_name, "_uniwith_#{feature_name}") end end end end