lib/tiny_hooks.rb in tiny_hooks-0.3.0 vs lib/tiny_hooks.rb in tiny_hooks-1.0.0
- old
+ new
@@ -6,94 +6,183 @@
# `extend` this module and now you can define hooks with `define_hook` method.
# See the test file for more detailed usage.
module TinyHooks
class Error < StandardError; end
+ class PrivateError < Error; end
+
+ class TargetError < Error; end
+
+ HALTING = Object.new.freeze
+ private_constant :HALTING
+ UNDEFINED_TARGETS = [].freeze
+ private_constant :UNDEFINED_TARGETS
+
# @api private
- def self.extended(mod)
- mod.class_eval { @@_originals ||= {} }
- # mod.instance_variable_set(:@_originals, {}) unless mod.instance_variable_defined?(:@_originals)
- # mod.define_singleton_method(:_originals) do
- # mod.instance_variable_get(:@_originals)
- # end
+ def self.included(base)
+ base.class_eval do
+ @_originals = {}
+ @_targets = UNDEFINED_TARGETS
+ @_public_only = false
+ end
+ base.extend ClassMethods
end
- # 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
- def define_hook(kind, target, &block)
- raise ArgumentError, 'You must provide a block' unless block
+ # @api private
+ def self.with_halting(terminator, *args, **kwargs, &block)
+ hook_result = nil
+ abort_result = catch :abort do
+ hook_result = instance_exec(*args, **kwargs, &block)
+ true
+ end
+ return HALTING if abort_result.nil? && terminator == :abort
+ return HALTING if hook_result == false && terminator == :return_false
- original_method = instance_method(target)
- @@_originals[target.to_sym] = original_method unless @@_originals[target.to_sym]
-
- body = case kind.to_sym
- when :before
- _before(original_method, &block)
- when :after
- _after(original_method, &block)
- when :around
- _around(original_method, &block)
- else
- raise Error, "#{kind} is not supported."
- end
- undef_method(target)
- define_method(target, &body)
+ hook_result
end
- module_function :define_hook
+ # Class methods
+ 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 [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
+ 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)
- # Restore original method
- #
- # @param [Symbol, String] target
- def restore_original(target)
- original_method = @@_originals[target.to_sym] || instance_method(target)
+ is_private = private_instance_methods.include?(target.to_sym)
- undef_method(target)
- define_method(target, original_method)
- end
+ begin
+ original_method = @_public_only ? public_instance_method(target) : instance_method(target)
+ rescue NameError => e
+ raise unless e.message.include?('private')
- 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]
- def _before(original_method, &block)
- if RUBY_VERSION >= '2.7'
- proc do |*args, **kwargs, &blk|
- instance_exec(*args, **kwargs, &block)
- original_method.bind_call(self, *args, **kwargs, &blk)
+ undef_method(target)
+ define_method(target, &method_body(kind, original_method, terminator, binding.local_variable_get(:if), &block))
+ private target if is_private
+ 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)
+ 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
+ @_targets = if include_pattern && exclude_pattern
+ targets = candidates.grep(include_pattern)
+ targets.grep_v(exclude_pattern)
+ elsif include_pattern
+ candidates.grep(include_pattern)
+ else
+ candidates.grep_v(exclude_pattern)
+ end
+ end
+
+ # Enable public only mode
+ def public_only!
+ @_public_only = true
+ end
+
+ # Disable public only mode
+ def include_private!
+ @_public_only = false
+ end
+
+ private
+
+ def method_body(kind, original_method, terminator, if_proc, &block)
+ case kind.to_sym
+ when :before then _before(original_method, terminator: terminator, if_proc: if_proc, &block)
+ when :after then _after(original_method, if_proc: if_proc, &block)
+ when :around then _around(original_method, if_proc: if_proc, &block)
+ else
+ raise Error, "#{kind} is not supported."
end
- else
- proc do |*args, &blk|
- instance_exec(*args, &block)
- original_method.bind(self).call(*args, &blk)
+ end
+
+ def _before(original_method, terminator:, if_proc:, &block)
+ if RUBY_VERSION >= '2.7'
+ proc do |*args, **kwargs, &blk|
+ if if_proc.nil? || instance_exec(&if_proc) != false
+ hook_result = nil
+ abort_result = catch :abort do
+ hook_result = instance_exec(*args, **kwargs, &block)
+ true
+ 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)
+ end
+ else
+ proc do |*args, &blk|
+ if if_proc.nil? || instance_exec(&if_proc) != false
+ hook_result = nil
+ abort_result = catch :abort do
+ hook_result = instance_exec(*args, &block)
+ true
+ end
+ return if abort_result.nil? && terminator == :abort
+ return if hook_result == false && terminator == :return_false
+ end
+
+ original_method.bind(self).call(*args, &blk)
+ end
end
end
- end
- def _after(original_method, &block)
- if RUBY_VERSION >= '2.7'
- proc do |*args, **kwargs, &blk|
- original_method.bind_call(self, *args, **kwargs, &blk)
- instance_exec(*args, **kwargs, &block)
+ 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)
+ 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)
+ instance_exec(*args, &block) if if_proc.nil? || instance_exec(&if_proc) != false
+ end
end
- else
- proc do |*args, &blk|
- original_method.bind(self).call(*args, &blk)
- instance_exec(*args, &block)
- end
end
- end
- def _around(original_method, &block)
- if RUBY_VERSION >= '2.7'
- proc do |*args, **kwargs, &blk|
- wrapper = -> { original_method.bind_call(self, *args, **kwargs, &blk) }
- instance_exec(wrapper, *args, **kwargs, &block)
+ 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) }
+ 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) }
+ instance_exec(wrapper, *args, &block) if if_proc.nil? || instance_exec(&if_proc) != false
+ end
end
- else
- proc do |*args, &blk|
- wrapper = -> { original_method.bind(self).call(*args, &blk) }
- instance_exec(wrapper, *args, &block)
- end
+ end
+
+ def inherited(subclass)
+ super
+ subclass.instance_variable_set(:@_originals, instance_variable_get(:@_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