require "active_support/concern" require "active_support/rescuable" require "active_support/core_ext/class/attribute" require "execute_with_rescue/errors" module ExecuteWithRescue module Mixins module Core extend ActiveSupport::Concern included do include ActiveSupport::Rescuable # Use active support or inheritance will be broken class_attribute( :_execute_with_rescue_before_hooks, instance_reader: true, instance_writer: false, ) self._execute_with_rescue_before_hooks = [] class_attribute( :_execute_with_rescue_after_hooks, instance_reader: true, instance_writer: false, ) self._execute_with_rescue_after_hooks = [] class << self # Pass method names or/and a block to be executed before yield # # @param method_names [Array] # instance methods names to be run before yield # @param block [Proc] # a block to be executed with no argument # in the instance before yield # It will be appended after method_names if both given # # @note These hooks are inherited # # @example Add a hook to begin some logging # add_execute_with_rescue_before_hooks(:log_start) # # @raise [ArgumentError] # if neither method_names and block is given def add_execute_with_rescue_before_hooks(*method_names, &block) _validate_execute_with_rescue_hook!(method_names, block) # Must use setter to avoid changing parent setting self._execute_with_rescue_before_hooks = [ _execute_with_rescue_before_hooks, # Add method names first, block later method_names, block, ].flatten.compact end alias_method( :add_execute_with_rescue_before_hook, :add_execute_with_rescue_before_hooks, ) # Pass method names or/and a block to be executed after yield # Similar to add_execute_with_rescue_before_hooks # # @see add_execute_with_rescue_before_hooks def add_execute_with_rescue_after_hooks(*method_names, &block) _validate_execute_with_rescue_hook!(method_names, block) # Must use setter to avoid changing parent setting self._execute_with_rescue_after_hooks = [ _execute_with_rescue_after_hooks, # Add method names first, block later method_names, block, ].flatten.compact end alias_method( :add_execute_with_rescue_after_hook, :add_execute_with_rescue_after_hooks, ) # @api private # @discuss # Should this moved into another module? # (without being mixed in) def _validate_execute_with_rescue_hook!(method_names, block) fail ArgumentError if method_names.empty? && block.nil? fail ExecuteWithRescue::Errors::UnsupportedHookValue unless method_names.all? { |m| m.is_a?(Symbol) } end end end private # Wrapper method for rescuing known errors # after you have call `rescue_from` at class level # This saves you from typing: # ``` # begin # # Some code that might cause exception # rescue # rescue_with_handler(exception) || raise # end # ```` # Remember to `next` instead of `return` if you want to terminate # # You can use `alias_method` to create a shorter alias, I use `execute` # But some gem might use that name already, so be careful # # @api # # @param block [Proc] # a block to be executed # # @note # Use `next` for termination, since `return` in block does not work # @note # Although we rescue Exception here, # but normally we should NOT handle them without re-raise # # @example Use with gem `interactor` # class DoSomething # include Interactor # include ExecuteWithRescue::Mixins::Core # # def perform # execute_with_rescue do # # Do something # end # end # end # # @raise [LocalJumpError] # When you call return in block def execute_with_rescue _run_execute_with_rescue_before_hooks yield rescue Exception => exception rescue_with_handler(exception) || fail ensure _run_execute_with_rescue_after_hooks end # @api private def _run_execute_with_rescue_before_hooks _execute_with_rescue_before_hooks.each do |before_hook| _run_execute_with_rescue_hook(before_hook) end end # @api private def _run_execute_with_rescue_after_hooks _execute_with_rescue_after_hooks.reverse_each do |after_hook| _run_execute_with_rescue_hook(after_hook) end end # @api private def _run_execute_with_rescue_hook(method_name_or_block) case method_name_or_block when Symbol _run_execute_with_rescue_hook_with_symbol(method_name_or_block) when Proc # block are converted to Proc as argument instance_eval(&method_name_or_block) else # This should not happen unless someone tamper the class attribute # without using the provided methods fail ExecuteWithRescue::Errors::UnsupportedHookValue end end def _run_execute_with_rescue_hook_with_symbol(method_name) send(method_name) rescue NoMethodError fail ExecuteWithRescue::Errors::NoHookMethod, "method `#{method_name}` does not exists" end end end end