# encoding: utf-8 module Fidgit # Adds simple event handling methods to an object (subscribe/publish pattern). # # @example # class JumpingBean # include Event # event :jump # end # # bean = JumpingBean.new # bean.subscribe :jump do # puts "Whee!" # end # # bean.subscribe :jump do |object, direction, distance| # puts "#{object.class.name} jumped #{distance} metres #{direction}" # end # # bean.publish :jump, :up, 4 # # Whee! # # JumpingBean jumped 4 metres up # module Event # Created and returned by {Event#subscribe} and can be used to unsubscribe from the event. class Subscription attr_reader :publisher, :event, :handler def initialize(publisher, event, handler) raise TypeError unless publisher.is_a? Event raise TypeError unless event.is_a? Symbol raise TypeError unless handler.is_a? Proc or handler.is_a? Method @publisher, @event, @handler = publisher, event, handler end def unsubscribe @publisher.unsubscribe self end end class << self def new_event_handlers # Don't use Set, since it is not guaranteed to be ordered. Hash.new {|h, k| h[k] = [] } end end # @return [Subscription] Definition of this the handler created by this subscription, to be used with {#unsubscribe} def subscribe(event, method = nil, &block) raise ArgumentError, "Expected method or block for event handler" unless !block.nil? ^ !method.nil? raise ArgumentError, "#{self.class} does not handle #{event.inspect}" unless events.include? event @_event_handlers ||= Event.new_event_handlers handler = method || block @_event_handlers[event] << handler Subscription.new self, event, handler end # @overload unsubscribe(subscription) # Unsubscribe from a #{Subscription}, as returned from {#subscribe} # @param subscription [Subscription] # @return [Boolean] true if the handler was able to be deleted. # # @overload unsubscribe(handler) # Unsubscribe from first event this handler has been used to subscribe to.. # @param handler [Block, Method] Event handler used. # @return [Boolean] true if the handler was able to be deleted. # # @overload unsubscribe(event, handler) # Unsubscribe from specific handler on particular event. # @param event [Symbol] Name of event originally subscribed to. # @param handler [Block, Method] Event handler used. # @return [Boolean] true if the handler was able to be deleted. # def unsubscribe(*args) @_event_handlers ||= Event.new_event_handlers case args.size when 1 case args.first when Subscription # Delete specific event handler. subscription = args.first raise ArgumentError, "Incorrect publisher for #{Subscription}: #{subscription.publisher}" unless subscription.publisher == self unsubscribe subscription.event, subscription.handler when Proc, Method # Delete first events that use the handler. handler = args.first !!@_event_handlers.find {|_, handlers| handlers.delete handler } else raise TypeError, "handler must be a #{Subscription}, Block or Method: #{args.first}" end when 2 event, handler = args raise TypeError, "event name must be a Symbol: #{event}" unless event.is_a? Symbol raise TypeError, "handler name must be a Proc/Method: #{handler}" unless handler.is_a? Proc or handler.is_a? Method !!@_event_handlers[event].delete(handler) else raise ArgumentError, "Requires 1..2 arguments, but received #{args.size} arguments" end end # Publish an event to all previously added handlers in the order they were added. # It will automatically call the publishing object with the method named after the event if it is defined # (this will be done before the manually added handlers are called). # # If any handler returns :handled, then no further handlers will be called. # # @param [Symbol] event Name of the event to publish. # @param [Array] args Arguments to pass to the event handlers. # @return [Symbol, nil] :handled if any handler handled the event or nil if none did. def publish(event, *args) raise ArgumentError, "#{self.class} does not handle #{event.inspect}" unless events.include? event # Do nothing if the object is disabled. return if respond_to?(:enabled?) and not enabled? if respond_to? event return :handled if send(event, self, *args) == :handled end if defined? @_event_handlers @_event_handlers[event].reverse_each do |handler| return :handled if handler.call(self, *args) == :handled end end nil end # The list of events that this object can publish/subscribe. def events self.class.events end # Add singleton methods to the class that includes Event. def self.included(base) class << base def events # Copy the events already set up for your parent. @events ||= if superclass.respond_to? :events superclass.events.dup else [] end end def event(event) events.push event.to_sym unless events.include? event event end end end end end