lib/websocket_rails/event_map.rb
module WebsocketRails
# Provides a DSL for mapping client events to controller actions.
#
# == Example events.rb file
# # located in config/initializers/events.rb
# WebsocketRails::EventMap.describe do
# subscribe :client_connected, to: ChatController, with_method: :client_connected
# end
#
# A single event can be mapped to any number of controller actions.
#
# subscribe :new_message, :to => ChatController, :with_method => :rebroadcast_message
# subscribe :new_message, :to => LogController, :with_method => :log_message
#
# Events can be nested underneath namesapces.
#
# namespace :product do
# subscribe :new, :to => ProductController, :with_method => :new
# end
class EventMap
def self.describe(&block)
WebsocketRails.config.route_block = block
end
attr_reader :namespace
def initialize(dispatcher)
@dispatcher = dispatcher
@namespace = DSL.new(dispatcher).evaluate WebsocketRails.config.route_block
@namespace = DSL.new(dispatcher,@namespace).evaluate InternalEvents.events
end
def routes_for(event, &block)
@namespace.routes_for event, &block
end
# Proxy the reload_controllers! method to the global namespace.
def reload_controllers!
@namespace.reload_controllers!
end
# Provides the DSL methods available to the Event routes file
class DSL
def initialize(dispatcher,namespace=nil)
if namespace
@namespace = namespace
else
@namespace = Namespace.new :global, dispatcher
end
end
def evaluate(route_block)
instance_eval &route_block unless route_block.nil?
@namespace
end
def subscribe(event_name,options)
@namespace.store event_name, options
end
def namespace(new_namespace,&block)
@namespace = @namespace.find_or_create new_namespace
instance_eval &block if block.present?
@namespace = @namespace.parent
end
def private_channel(channel)
WebsocketRails[channel].make_private
end
end
# Stores route map for nested namespaces
class Namespace
include Logging
attr_reader :name, :controllers, :actions, :namespaces, :parent
def initialize(name,dispatcher,parent=nil)
@name = name
@parent = parent
@dispatcher = dispatcher
@actions = Hash.new {|h,k| h[k] = Array.new}
@controllers = Hash.new
@namespaces = Hash.new
end
def find_or_create(namespace)
unless child = namespaces[namespace]
child = Namespace.new namespace, @dispatcher, self
namespaces[namespace] = child
end
child
end
# Stores controller/action pairs for events subscribed under
# this namespace.
def store(event_name,options)
klass, action = TargetValidator.validate_target options
actions[event_name] << [klass,action]
end
# Iterates through the namespace tree and yields all
# controller/action pairs stored for the target event.
def routes_for(event, event_namespace=nil, &block)
# Grab the first level namespace from the namespace array
# and remove it from the copy.
event_namespace = copy_event_namespace( event, event_namespace ) || return
namespace = event_namespace.shift
# If the namespace matches the current namespace and we are
# at the last namespace level, yield any controller/action
# pairs for this event.
#
# If the namespace does not match, search the list of child
# namespaces stored at this level for a match and delegate
# to it's #routes_for method, passing along the current
# copy of the event's namespace array.
if namespace == @name and event_namespace.empty?
actions[event.name].each do |klass,action|
block.call klass, action
end
else
child_namespace = event_namespace.first
child = namespaces[child_namespace]
child.routes_for event, event_namespace, &block unless child.nil?
end
end
private
def copy_event_namespace(event, namespace=nil)
namespace = event.namespace.dup if namespace.nil?
namespace
end
end
end
class TargetValidator
# Parses the target and extracts controller/action pair or raises an error if target is invalid
def self.validate_target(target)
case target
when Hash
validate_hash_target target
when String
validate_string_target target
else
raise('Must specify the event target either as a string product#new_product or as a Hash to: ProductController, with_method: :new_product')
end
end
private
# Parses the target as a Hash, expecting keys to: and with_method:
def self.validate_hash_target(target)
klass = target[:to] || raise("Must specify a class for to: option in event route")
action = target[:with_method] || raise("Must specify a method for with_method: option in event route")
[klass, action]
end
# Parses the target as a String, expecting it to be in the format "product#new_product"
def self.validate_string_target(target)
strings = target.split('#')
raise('The string must be in a format like product#new_product') unless strings.count == 2
klass = constantize_controller strings[0]
action = strings[1].to_sym
[klass, action]
end
def self.constantize_controller(controller_string)
strings = (controller_string << '_controller').split('/')
strings.map{|string| string.camelize}.join('::').constantize
end
end
end