adhearsion/has-guarded-handlers

View on GitHub
lib/has_guarded_handlers.rb

Summary

Maintainability
C
7 hrs
Test Coverage
require "has_guarded_handlers/version"
require 'securerandom'

#
# HasGuardedHandlers allows an object's API to provide flexible handler registration, storage and matching to arbitrary events.
#
# HasGuardedHandlers is a module that should be mixed into some object which needs to emit events.
#
# See the README for more usage info.
#
# @author Ben Langfeld <ben@langfeld.me>
#
# @example Simple usage
#
#   require 'has_guarded_handlers'
#   
#   class A
#     include HasGuardedHandlers
#   end
#   
#   a = A.new
#   a.register_handler :event do |event|
#     puts "Handled the event #{event.inspect}"
#   end
#   
#   a.trigger_handler :event, "Foo!"
#
# @example Guarding event handlers
#
#   require 'has_guarded_handlers'
#   
#   class A
#     include HasGuardedHandlers
#   end
#   
#   a = A.new
#   a.register_handler :event, :type => :foo do |event|
#     puts "Handled the event of type #{event.type} with value #{event.value}"
#   end
#   
#   Event = Class.new Struct.new(:type, :value)
#   
#   a.trigger_handler :event, Event.new(:foo, 'bar')
#
module HasGuardedHandlers
  # Register a handler
  #
  # @param [Symbol, nil] type a classification to separate handlers/events into channels. Omitting the classification will create a handler for all events.
  # @param [guards] guards take a look at the guards documentation
  #
  # @yield [Object] trigger_object the incoming event
  #
  # @return [String] handler ID for later manipulation
  def register_handler(type = nil, *guards, &handler)
    register_handler_with_options type, {}, *guards, &handler
  end

  # Register a temporary handler. Once triggered, the handler will be de-registered
  #
  # @param [Symbol, nil] type a classification to separate handlers/events into channels Omitting the classification will create a handler for all events.
  # @param [guards] guards take a look at the guards documentation
  #
  # @yield [Object] trigger_object the incoming event
  #
  # @return [String] handler ID for later manipulation
  def register_tmp_handler(type = nil, *guards, &handler)
    register_handler_with_options type, {:tmp => true}, *guards, &handler
  end

  # Register a handler with a specified priority
  #
  # @param [Symbol, nil] type a classification to separate handlers/events into channels Omitting the classification will create a handler for all events.
  # @param [Integer] priority the priority of the handler. Higher priority executes first
  # @param [guards] guards take a look at the guards documentation
  #
  # @yield [Object] trigger_object the incoming event
  #
  # @return [String] handler ID for later manipulation
  def register_handler_with_priority(type = nil, priority = 0, *guards, &handler)
    register_handler_with_options type, {:priority => priority}, *guards, &handler
  end

  # Register a handler with a specified set of options
  #
  # @param [Symbol, nil] type a classification to separate handlers/events into channels Omitting the classification will create a handler for all events.
  # @param [Hash] options the options for the handler
  # @option options [Integer] :priority (0) the priority of the handler. Higher priority executes first
  # @option options [true, false] :tmp (false) Wether or not the handler should be considered temporary (single execution)
  # @param [guards] guards take a look at the guards documentation
  #
  # @yield [Object] trigger_object the incoming event
  #
  # @return [String] handler ID for later manipulation
  def register_handler_with_options(type = nil, options = {}, *guards, &handler)
    check_guards guards
    options[:priority] ||= 0
    new_handler_id.tap do |handler_id|
      guarded_handlers[type][options[:priority]] << [guards, handler, options[:tmp], handler_id]
    end
  end

  # Unregister a handler by ID
  #
  # @param [Symbol] type the handler classification used at registration
  # @param [String] handler_id the handler ID returned by registration
  def unregister_handler(type, handler_id)
    delete_handler_if(type) { |_, _, _, id| id == handler_id }
  end

  # Clear handlers with given guards
  #
  # @param [Symbol, nil] type remove filters for a specific handler
  # @param [guards] guards take a look at the guards documentation
  def clear_handlers(type = nil, *guards)
    if type.nil?
      @handlers = nil
    else
      delete_handler_if(type) { |g, _| g == guards }
    end
  end

  # Trigger a handler classification with an event object
  #
  # @param [Symbol, nil] type a classification to separate handlers/events into channels
  # @param [Object] event the event object to yield to the handler block
  # @param [Hash] options
  # @option options [true, false] :broadcast Enables broadcast mode, where the return value or raising of handlers does not halt the handler chain. Defaults to false.
  # @option options [Proc] :exception_callback Allows handling exceptions when broadcast mode is available via a callback.
  def trigger_handler(type, event, options = {})
    broadcast = options[:broadcast] || false
    return unless handler = handlers_of_type(type)
    called = false
    catch :halt do
      h = handler.find do |guards, handler, tmp|
        called = true
        val = catch(:pass) do
          if guarded?(guards, event)
            called = false
          else
            begin
              call_handler handler, guards, event
            rescue => e
              if broadcast
                options[:exception_callback].call(e) if options[:exception_callback]
              else
                raise
              end
            end
            true unless broadcast
          end
        end
        delete_handler_if(type) { |_, h, _| h.equal? handler } if tmp && called
        val
      end
    end
    !!called
  end

  private

  def call_handler(handler, guards, event)
    handler.call event
  end

  def delete_handler_if(type, &block) # :nodoc:
    guarded_handlers[type].each_pair do |priority, handlers|
      handlers.delete_if(&block)
    end
  end

  def handlers_of_type(type) # :nodoc:
    return unless hash = guarded_handlers[type]
    values = []
    hash.keys.sort.reverse.each do |key|
      values += hash[key]
    end
    global_handlers = guarded_handlers[nil]
    global_handlers.keys.sort.reverse.each do |key|
      values += global_handlers[key]
    end
    values
  end

  def new_handler_id # :nodoc:
    SecureRandom.uuid
  end

  # If any of the guards returns FALSE this returns true
  # the logic is reversed to allow short circuiting
  # (why would anyone want to loop over more values than necessary?)
  #
  # @private
  def guarded?(guards, event) # :nodoc:
    guards.find do |guard|
      case guard
      when Class, Module
        !event.is_a? guard
      when Symbol
        !event.__send__ guard
      when Array
        # return FALSE if any item is TRUE
        !guard.detect { |condition| !guarded? [condition], event }
      when Hash
        # return FALSE unless any inequality is found
        guard.find do |method, test|
          value = event.__send__(*method)
          # last_match is the only method found unique to Regexp classes
          if test.class.respond_to?(:last_match)
            !(test =~ value.to_s)
          elsif test.is_a?(Array)
            !test.include? value
          else
            test != value
          end
        end
      when Proc
        !guard.call event
      end
    end
  end

  def check_guards(guards) # :nodoc:
    guards.each do |guard|
      case guard
      when Array
        guard.each { |g| check_guards [g] }
      when Class, Module, Symbol, Proc, Hash, String
        nil
      else
        raise "Bad guard: #{guard.inspect}"
      end
    end
  end

  def guarded_handlers # :nodoc:
    @handlers ||= Hash.new { |h, k| h[k] = Hash.new { |h, k| h[k] = [] } }
  end
end