houston/houston-core

View on GitHub
lib/houston/boot/observer.rb

Summary

Maintainability
A
0 mins
Test Coverage
module Houston
  class Observer
    attr_accessor :async

    class UnregisteredEventError < ArgumentError; end
    class MissingParamError < ArgumentError; end
    class UnregisteredParamError < ArgumentError; end

    def initialize
      @async = true
      clear!
    end

    def on(event, options={}, &block)
      assert_registered! event
      observers_of(event).push Callback.new(self, event, options, block)
      nil
    end

    def once(event, options={}, &block)
      assert_registered! event
      observers_of(event).push CallbackOnce.new(self, event, options, block)
      nil
    end

    def off(callback, &block)
      if block_given?
        event = callback
        callback = observers_of(event).detect { |callback| callback.block == block }
        return nil unless callback
      end
      observers_of(callback.event).delete callback
      nil
    end

    def observed?(event)
      assert_registered! event
      observers_of(event).any?
    end

    def fire(event, params={})
      assert_registered! event

      unless params.is_a?(Hash)
        raise ArgumentError, "params must be a Hash" unless params.respond_to?(:to_h)
        params = params.to_h
      end

      assert_registered_params! event, params
      assert_serializable! params
      params = ReadonlyHash.new(params)

      observers_of(event).each do |callback|
        callback.call params
      end
      observers_of(:*).each do |callback|
        callback.call event, params
      end
      nil
    end

    def clear!
      @observers = {}
    end

  private
    attr_reader :observers

    def observers_of(event)
      observers[event] ||= Concurrent::Array.new
    end

    def assert_registered!(event_name)
      return if event_name == :*
      return if Houston.events.registered?(event_name)
      raise UnregisteredEventError, "#{event_name.inspect} is not a registered event"
    end

    def assert_registered_params!(event_name, params)
      event = Houston.events[event_name]

      missing_params = event.params - params.keys.map(&:to_s)
      unregistered_params = params.keys.map(&:to_s) - event.params
      if missing_params.any?
        raise MissingParamError, "#{missing_params.first.inspect} is a required param of the event #{event_name.inspect}"
      end
      if unregistered_params.any?
        raise UnregisteredParamError, "#{unregistered_params.first.inspect} is a not a registered param of the event #{event_name.inspect}"
      end
    end

    def assert_serializable!(params)
      Houston::Serializer.new.assert_serializable!(params)
    end



    class Callback
      attr_reader :observer, :event, :block

      def initialize(observer, event, options, block)
        @observer = observer
        @event = event
        @invoke_async = options.fetch(:async, nil)
        @raise_exceptions = options.fetch(:raise, false)
        @block = block
      end

      def invoke_async?
        return @invoke_async unless @invoke_async.nil?
        Houston.observer.async
      end

      def raise_exceptions?
        @raise_exceptions
      end

      def call(*args)
        Houston.async(invoke_async?) do
          begin
            block.call(*args)

          rescue Exception # rescues StandardError by default; but we want to rescue and report all errors
            raise if raise_exceptions?

            $!.additional_information[:event] = event
            $!.additional_information[:async] = invoke_async?
            $!.additional_information[:raise_exceptions] = raise_exceptions?
            Houston.report_exception($!)
          end
        end
      end

    end

    class CallbackOnce < Callback
      def call(*args)
        observer.off self
        super
      end
    end

  end



  class ReadonlyHash

    def initialize(hash)
      @hash = hash.symbolize_keys
      @hash.keys.each do |key|
        define_singleton_method(key) { @hash[key] }
      end
    end

    def [](key)
      @hash[key.to_sym]
    end

    def count
      @hash.count
    end

    def to_h
      @hash.dup
    end

  end
end