lib/tiny_hooks.rb in tiny_hooks-1.0.0 vs lib/tiny_hooks.rb in tiny_hooks-2.0.0

- old
+ new

@@ -19,10 +19,11 @@ # @api private def self.included(base) base.class_eval do @_originals = {} + @_class_originals = {} @_targets = UNDEFINED_TARGETS @_public_only = false end base.extend ClassMethods end @@ -44,50 +45,84 @@ module ClassMethods # Define hook with kind and target method # # @param [Symbol, String] kind the kind of the hook, possible values are: :before, :after and :around # @param [Symbol, String] target the name of the targeted method + # @param hook_method_name [Symbol, String] the name of a method which should be called as a hook # @param [Symbol] terminator choice for terminating execution, default is throwing abort symbol # @param [Symbol] if condition to determine if it should define callback. Block is evaluated in context of self - def define_hook(kind, target, terminator: :abort, if: nil, &block) # rubocop:disable Naming/MethodParameterName - raise ArgumentError, 'You must provide a block' unless block + # @param class_method [Boolean] treat target as class method + def define_hook(kind, target, hook_method_name = nil, terminator: :abort, if: nil, class_method: false, &block) # rubocop:disable Naming/MethodParameterName + raise ArgumentError, 'You must provide a block or hook_method_name' unless block || hook_method_name raise ArgumentError, 'terminator must be one of the following: :abort or :return_false' unless %i[abort return_false].include? terminator.to_sym raise TinyHooks::TargetError, "Hook for #{target} is not allowed" if @_targets != UNDEFINED_TARGETS && !@_targets.include?(target) - is_private = private_instance_methods.include?(target.to_sym) + if class_method + is_private = private_methods.include?(target.to_sym) - begin - original_method = @_public_only ? public_instance_method(target) : instance_method(target) - rescue NameError => e - raise unless e.message.include?('private') + begin + original_method = @_public_only ? public_method(target) : method(target) + rescue NameError => e + raise unless e.message.include?('private') - raise TinyHooks::PrivateError, "Public only mode is on and hooks for private methods (#{target} for this time) are not available." - end - @_originals[target.to_sym] = original_method unless @_originals[target.to_sym] + raise TinyHooks::PrivateError, "Public only mode is on and hooks for private methods (#{target} for this time) are not available." + end + @_class_originals[target.to_sym] = original_method unless @_class_originals[target.to_sym] - undef_method(target) - define_method(target, &method_body(kind, original_method, terminator, binding.local_variable_get(:if), &block)) - private target if is_private + block ||= -> { __send__(hook_method_name) } + body = method_body(kind, original_method, terminator, binding.local_variable_get(:if), &block) + singleton_class.class_eval do + undef_method(target) + define_method(target, &body) + private target if is_private + end + else # instance method + is_private = private_instance_methods.include?(target.to_sym) + + begin + original_method = @_public_only ? public_instance_method(target) : instance_method(target) + rescue NameError => e + raise unless e.message.include?('private') + + raise TinyHooks::PrivateError, "Public only mode is on and hooks for private methods (#{target} for this time) are not available." + end + @_originals[target.to_sym] = original_method unless @_originals[target.to_sym] + + block ||= -> { __send__(hook_method_name) } + + undef_method(target) + define_method(target, &method_body(kind, original_method, terminator, binding.local_variable_get(:if), &block)) + private target if is_private + end end # Restore original method # # @param [Symbol, String] target - def restore_original(target) - original_method = @_originals[target.to_sym] || instance_method(target) - - undef_method(target) - define_method(target, original_method) + # @param class_method [Boolean] treat target as class method + def restore_original(target, class_method: false) + if class_method + original_method = @_class_originals[target.to_sym] || method(target) + singleton_class.class_eval do + undef_method(target) + define_method(target, original_method) + end + else + original_method = @_originals[target.to_sym] || instance_method(target) + undef_method(target) + define_method(target, original_method) + end end # Defines target for hooks # @param include_pattern [Regexp] # @param exclude_pattern [Regexp] def target!(include_pattern: nil, exclude_pattern: nil) raise ArgumentError if include_pattern.nil? && exclude_pattern.nil? candidates = @_public_only ? instance_methods : instance_methods + private_instance_methods + candidates += @public_only ? methods : methods + private_methods @_targets = if include_pattern && exclude_pattern targets = candidates.grep(include_pattern) targets.grep_v(exclude_pattern) elsif include_pattern candidates.grep(include_pattern) @@ -129,11 +164,11 @@ end return if abort_result.nil? && terminator == :abort return if hook_result == false && terminator == :return_false end - original_method.bind_call(self, *args, **kwargs, &blk) + original_method.is_a?(UnboundMethod) ? original_method.bind_call(self, *args, **kwargs, &blk) : original_method.call(*args, **kwargs, &blk) end else proc do |*args, &blk| if if_proc.nil? || instance_exec(&if_proc) != false hook_result = nil @@ -143,45 +178,53 @@ end return if abort_result.nil? && terminator == :abort return if hook_result == false && terminator == :return_false end - original_method.bind(self).call(*args, &blk) + original_method = original_method.bind(self) if original_method.is_a?(UnboundMethod) + original_method.call(*args, &blk) end end end def _after(original_method, if_proc:, &block) if RUBY_VERSION >= '2.7' proc do |*args, **kwargs, &blk| - original_method.bind_call(self, *args, **kwargs, &blk) + original_method.is_a?(UnboundMethod) ? original_method.bind_call(self, *args, **kwargs, &blk) : original_method.call(*args, **kwargs, &blk) instance_exec(*args, **kwargs, &block) if if_proc.nil? || instance_exec(&if_proc) != false end else proc do |*args, &blk| - original_method.bind(self).call(*args, &blk) + original_method = original_method.bind(self) if original_method.is_a?(UnboundMethod) + original_method.call(*args, &blk) instance_exec(*args, &block) if if_proc.nil? || instance_exec(&if_proc) != false end end end def _around(original_method, if_proc:, &block) if RUBY_VERSION >= '2.7' proc do |*args, **kwargs, &blk| - wrapper = -> { original_method.bind_call(self, *args, **kwargs, &blk) } + wrapper = lambda do + original_method.is_a?(UnboundMethod) ? original_method.bind_call(self, *args, **kwargs, &blk) : original_method.call(*args, **kwargs, &blk) + end instance_exec(wrapper, *args, **kwargs, &block) if if_proc.nil? || instance_exec(&if_proc) != false end else proc do |*args, &blk| - wrapper = -> { original_method.bind(self).call(*args, &blk) } + wrapper = lambda do + original_method = original_method.bind(self) if original_method.is_a?(UnboundMethod) + original_method.call(*args, &blk) + end instance_exec(wrapper, *args, &block) if if_proc.nil? || instance_exec(&if_proc) != false end end end def inherited(subclass) super subclass.instance_variable_set(:@_originals, instance_variable_get(:@_originals).clone) + subclass.instance_variable_set(:@_class_originals, instance_variable_get(:@_class_originals).clone) subclass.instance_variable_set(:@_targets, instance_variable_get(:@_targets).clone) subclass.instance_variable_set(:@_public_only, instance_variable_get(:@_public_only).clone) end end end