Flowdock
method

merge_conditional_options

Importance_0
v5.2.3 - Show latest stable - 0 notes - Class: ActiveSupport::Callbacks::Callback
merge_conditional_options(chain, if_option:, unless_option:) public

No documentation

This method has no description. You can help the Ruby on Rails community by adding new notes.

Hide source
# File activesupport/lib/active_support/callbacks.rb, line 308
        def merge_conditional_options(chain, if_option,, unless_option))
          options = {
            if: @if.dup,
            unless: @unless.dup
          }

          options[:if].concat     Array(unless_option)
          options[:unless].concat Array(if_option)

          self.class.build chain, @filter, @kind, options
        end

        def matches?(_kind, _filter)
          @kind == _kind && filter == _filter
        end

        def duplicates?(other)
          case @filter
          when Symbol
            matches?(other.kind, other.filter)
          else
            false
          end
        end

        # Wraps code with filter
        def apply(callback_sequence)
          user_conditions = conditions_lambdas
          user_callback = CallTemplate.build(@filter, self)

          case kind
          when :before
            Filters::Before.build(callback_sequence, user_callback.make_lambda, user_conditions, chain_config, @filter)
          when :after
            Filters::After.build(callback_sequence, user_callback.make_lambda, user_conditions, chain_config)
          when :around
            callback_sequence.around(user_callback, user_conditions)
          end
        end

        def current_scopes
          Array(chain_config[:scope]).map { |s| public_send(s) }
        end

        private
          def check_conditionals(conditionals)
            if conditionals.any? { |c| c.is_a?(String) }
              raise ArgumentError,                 Passing string to be evaluated in :if and :unless conditional                options is not supported. Pass a symbol for an instance method,                or a lambda, proc or block, instead..squish
            end

            conditionals
          end

          def compute_identifier(filter)
            case filter
            when ::Proc
              filter.object_id
            else
              filter
            end
          end

          def conditions_lambdas
            @if.map { |c| CallTemplate.build(c, self).make_lambda } +
              @unless.map { |c| CallTemplate.build(c, self).inverted_lambda }
          end
      end

      # A future invocation of user-supplied code (either as a callback,
      # or a condition filter).
      class CallTemplate # :nodoc:
        def initialize(target, method, arguments, block)
          @override_target = target
          @method_name = method
          @arguments = arguments
          @override_block = block
        end

        # Return the parts needed to make this call, with the given
        # input values.
        #
        # Returns an array of the form:
        #
        #   [target, block, method, *arguments]
        #
        # This array can be used as such:
        #
        #   target.send(method, *arguments, &block)
        #
        # The actual invocation is left up to the caller to minimize
        # call stack pollution.
        def expand(target, value, block)
          result = @arguments.map { |arg|
            case arg
            when :value; value
            when :target; target
            when :block; block || raise(ArgumentError)
            end
          }

          result.unshift @method_name
          result.unshift @override_block || block
          result.unshift @override_target || target

          # target, block, method, *arguments = result
          # target.send(method, *arguments, &block)
          result
        end

        # Return a lambda that will make this call when given the input
        # values.
        def make_lambda
          lambda do |target, value, &block|
            target, block, method, *arguments = expand(target, value, block)
            target.send(method, *arguments, &block)
          end
        end

        # Return a lambda that will make this call when given the input
        # values, but then return the boolean inverse of that result.
        def inverted_lambda
          lambda do |target, value, &block|
            target, block, method, *arguments = expand(target, value, block)
            ! target.send(method, *arguments, &block)
          end
        end

        # Filters support:
        #
        #   Symbols:: A method to call.
        #   Procs::   A proc to call with the object.
        #   Objects:: An object with a <tt>before_foo</tt> method on it to call.
        #
        # All of these objects are converted into a CallTemplate and handled
        # the same after this point.
        def self.build(filter, callback)
          case filter
          when Symbol
            new(nil, filter, [], nil)
          when Conditionals::Value
            new(filter, :call, [:target, :value], nil)
          when ::Proc
            if filter.arity > 1
              new(nil, :instance_exec, [:target, :block], filter)
            elsif filter.arity > 0
              new(nil, :instance_exec, [:target], filter)
            else
              new(nil, :instance_exec, [], filter)
            end
          else
            method_to_call = callback.current_scopes.join("_")

            new(filter, method_to_call, [:target], nil)
          end
        end
      end

      # Execute before and after filters in a sequence instead of
      # chaining them with nested lambda calls, see:
      # https://github.com/rails/rails/issues/18011
      class CallbackSequence # :nodoc:
        def initialize(nested = nil, call_template = nil, user_conditions = nil)
          @nested = nested
          @call_template = call_template
          @user_conditions = user_conditions

          @before = []
          @after = []
        end

        def before(&before)
          @before.unshift(before)
          self
        end

        def after(&after)
          @after.push(after)
          self
        end

        def around(call_template, user_conditions)
          CallbackSequence.new(self, call_template, user_conditions)
        end

        def skip?(arg)
          arg.halted || !@user_conditions.all? { |c| c.call(arg.target, arg.value) }
        end

        def nested
          @nested
        end

        def final?
          !@call_template
        end

        def expand_call_template(arg, block)
          @call_template.expand(arg.target, arg.value, block)
        end

        def invoke_before(arg)
          @before.each { |b| b.call(arg) }
        end

        def invoke_after(arg)
          @after.each { |a| a.call(arg) }
        end
      end

      class CallbackChain #:nodoc:#
        include Enumerable

        attr_reader :name, :config

        def initialize(name, config)
          @name = name
          @config = {
            scope: [:kind],
            terminator: default_terminator
          }.merge!(config)
          @chain = []
          @callbacks = nil
          @mutex = Mutex.new
        end

        def each(&block); @chain.each(&block); end
        def index(o);     @chain.index(o); end
        def empty?;       @chain.empty?; end

        def insert(index, o)
          @callbacks = nil
          @chain.insert(index, o)
        end

        def delete(o)
          @callbacks = nil
          @chain.delete(o)
        end

        def clear
          @callbacks = nil
          @chain.clear
          self
        end

        def initialize_copy(other)
          @callbacks = nil
          @chain     = other.chain.dup
          @mutex     = Mutex.new
        end

        def compile
          @callbacks || @mutex.synchronize do
            final_sequence = CallbackSequence.new
            @callbacks ||= @chain.reverse.inject(final_sequence) do |callback_sequence, callback|
              callback.apply callback_sequence
            end
          end
        end

        def append(*callbacks)
          callbacks.each { |c| append_one(c) }
        end

        def prepend(*callbacks)
          callbacks.each { |c| prepend_one(c) }
        end

        protected
          def chain; @chain; end

        private

          def append_one(callback)
            @callbacks = nil
            remove_duplicates(callback)
            @chain.push(callback)
          end

          def prepend_one(callback)
            @callbacks = nil
            remove_duplicates(callback)
            @chain.unshift(callback)
          end

          def remove_duplicates(callback)
            @callbacks = nil
            @chain.delete_if { |c| callback.duplicates?(c) }
          end

          def default_terminator
            Proc.new do |target, result_lambda|
              terminate = true
              catch(:abort) do
                result_lambda.call
                terminate = false
              end
              terminate
            end
          end
      end

      module ClassMethods
        def normalize_callback_params(filters, block) # :nodoc:
          type = CALLBACK_FILTER_TYPES.include?(filters.first) ? filters.shift : :before
          options = filters.extract_options!
          filters.unshift(block) if block
          [type, filters, options.dup]
        end

        # This is used internally to append, prepend and skip callbacks to the
        # CallbackChain.
        def __update_callbacks(name) #:nodoc:
          ([self] + ActiveSupport::DescendantsTracker.descendants(self)).reverse_each do |target|
            chain = target.get_callbacks name
            yield target, chain.dup
          end
        end

        # Install a callback for the given event.
        #
        #   set_callback :save, :before, :before_method
        #   set_callback :save, :after,  :after_method, if: :condition
        #   set_callback :save, :around, ->(r, block) { stuff; result = block.call; stuff }
        #
        # The second argument indicates whether the callback is to be run +:before+,
        # +:after+, or +:around+ the event. If omitted, +:before+ is assumed. This
        # means the first example above can also be written as:
        #
        #   set_callback :save, :before_method
        #
        # The callback can be specified as a symbol naming an instance method; as a
        # proc, lambda, or block; or as an object that responds to a certain method
        # determined by the <tt>:scope</tt> argument to +define_callbacks+.
        #
        # If a proc, lambda, or block is given, its body is evaluated in the context
        # of the current object. It can also optionally accept the current object as
        # an argument.
        #
        # Before and around callbacks are called in the order that they are set;
        # after callbacks are called in the reverse order.
        #
        # Around callbacks can access the return value from the event, if it
        # wasn't halted, from the +yield+ call.
        #
        # ===== Options
        #
        # * <tt>:if</tt> - A symbol or an array of symbols, each naming an instance
        #   method or a proc; the callback will be called only when they all return
        #   a true value.
        # * <tt>:unless</tt> - A symbol or an array of symbols, each naming an
        #   instance method or a proc; the callback will be called only when they
        #   all return a false value.
        # * <tt>:prepend</tt> - If +true+, the callback will be prepended to the
        #   existing chain rather than appended.
        def set_callback(name, *filter_list, &block)
          type, filters, options = normalize_callback_params(filter_list, block)

          self_chain = get_callbacks name
          mapped = filters.map do |filter|
            Callback.build(self_chain, filter, type, options)
          end

          __update_callbacks(name) do |target, chain|
            options[:prepend] ? chain.prepend(*mapped) : chain.append(*mapped)
            target.set_callbacks name, chain
          end
        end

        # Skip a previously set callback. Like +set_callback+, <tt>:if</tt> or
        # <tt>:unless</tt> options may be passed in order to control when the
        # callback is skipped.
        #
        #   class Writer < Person
        #      skip_callback :validate, :before, :check_membership, if: -> { age > 18 }
        #   end
        #
        # An <tt>ArgumentError</tt> will be raised if the callback has not
        # already been set (unless the <tt>:raise</tt> option is set to <tt>false</tt>).
        def skip_callback(name, *filter_list, &block)
          type, filters, options = normalize_callback_params(filter_list, block)

          options[:raise] = true unless options.key?(:raise)

          __update_callbacks(name) do |target, chain|
            filters.each do |filter|
              callback = chain.find { |c| c.matches?(type, filter) }

              if !callback && options[:raise]
                raise ArgumentError, "#{type.to_s.capitalize} #{name} callback #{filter.inspect} has not been defined"
              end

              if callback && (options.key?(:if) || options.key?(:unless))
                new_callback = callback.merge_conditional_options(chain, if_option: options[:if], unless_option: options[:unless])
                chain.insert(chain.index(callback), new_callback)
              end

              chain.delete(callback)
            end
            target.set_callbacks name, chain
          end
        end

        # Remove all set callbacks for the given event.
        def reset_callbacks(name)
          callbacks = get_callbacks name

          ActiveSupport::DescendantsTracker.descendants(self).each do |target|
            chain = target.get_callbacks(name).dup
            callbacks.each { |c| chain.delete(c) }
            target.set_callbacks name, chain
          end

          set_callbacks(name, callbacks.dup.clear)
        end

        # Define sets of events in the object life cycle that support callbacks.
        #
        #   define_callbacks :validate
        #   define_callbacks :initialize, :save, :destroy
        #
        # ===== Options
        #
        # * <tt>:terminator</tt> - Determines when a before filter will halt the
        #   callback chain, preventing following before and around callbacks from
        #   being called and the event from being triggered.
        #   This should be a lambda to be executed.
        #   The current object and the result lambda of the callback will be provided
        #   to the terminator lambda.
        #
        #     define_callbacks :validate, terminator: ->(target, result_lambda) { result_lambda.call == false }
        #
        #   In this example, if any before validate callbacks returns +false+,
        #   any successive before and around callback is not executed.
        #
        #   The default terminator halts the chain when a callback throws +:abort+.
        #
        # * <tt>:skip_after_callbacks_if_terminated</tt> - Determines if after
        #   callbacks should be terminated by the <tt>:terminator</tt> option. By
        #   default after callbacks are executed no matter if callback chain was
        #   terminated or not. This option has no effect if <tt>:terminator</tt>
        #   option is set to +nil+.
        #
        # * <tt>:scope</tt> - Indicates which methods should be executed when an
        #   object is used as a callback.
        #
        #     class Audit
        #       def before(caller)
        #         puts 'Audit: before is called'
        #       end
        #
        #       def before_save(caller)
        #         puts 'Audit: before_save is called'
        #       end
        #     end
        #
        #     class Account
        #       include ActiveSupport::Callbacks
        #
        #       define_callbacks :save
        #       set_callback :save, :before, Audit.new
        #
        #       def save
        #         run_callbacks :save do
        #           puts 'save in main'
        #         end
        #       end
        #     end
        #
        #   In the above case whenever you save an account the method
        #   <tt>Audit#before</tt> will be called. On the other hand
        #
        #     define_callbacks :save, scope: [:kind, :name]
        #
        #   would trigger <tt>Audit#before_save</tt> instead. That's constructed
        #   by calling <tt>#{kind}_#{name}</tt> on the given instance. In this
        #   case "kind" is "before" and "name" is "save". In this context +:kind+
        #   and +:name+ have special meanings: +:kind+ refers to the kind of
        #   callback (before/after/around) and +:name+ refers to the method on
        #   which callbacks are being defined.
        #
        #   A declaration like
        #
        #     define_callbacks :save, scope: [:name]
        #
        #   would call <tt>Audit#save</tt>.
        #
        # ===== Notes
        #
        # +names+ passed to +define_callbacks+ must not end with
        # <tt>!</tt>, <tt>?</tt> or <tt>=</tt>.
        #
        # Calling +define_callbacks+ multiple times with the same +names+ will
        # overwrite previous callbacks registered with +set_callback+.
        def define_callbacks(*names)
          options = names.extract_options!

          names.each do |name|
            name = name.to_sym

            set_callbacks name, CallbackChain.new(name, options)

            module_eval               def _run_#{name}_callbacks(&block)                run_callbacks #{name.inspect}, &block              end              def self._#{name}_callbacks                get_callbacks(#{name.inspect})              end              def self._#{name}_callbacks=(value)                set_callbacks(#{name.inspect}, value)              end              def _#{name}_callbacks                __callbacks[#{name.inspect}]              end, __FILE__, __LINE__ + 1
          end
        end

        protected

          def get_callbacks(name) # :nodoc:
            __callbacks[name.to_sym]
          end

          def set_callbacks(name, callbacks) # :nodoc:
            self.__callbacks = __callbacks.merge(name.to_sym => callbacks)
          end
      end
  end
Register or log in to add new notes.