faye-rails 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md ADDED
@@ -0,0 +1,117 @@
1
+ # faye-rails [![Build Status](https://secure.travis-ci.org/RLovelett/faye-rails.png)](http://travis-ci.org/RLovelett/faye-rails)
2
+
3
+ faye-rails is a Ruby gem which handles embedding Faye's rack-based server into the rails stack and providing it with access to controllers and views based on bindings and observers.
4
+
5
+ # Embedded server
6
+
7
+ Due to the limitations of most Rack-based web servers available Faye can only be run on Thin, however if you are using thin, then you can add as many Faye servers as you want to the Rails router like so:
8
+
9
+ App::Application.routes.draw do
10
+ faye_server '/faye', :timeout => 25
11
+ end
12
+
13
+ You can also pass a block to `faye_server` which will be executed in the context of the Faye server, thus you can call any methods on `Faye::RackAdapter` from within the block:
14
+
15
+ App::Application.routes.draw do
16
+ faye_server '/faye', :timeout => 25 do
17
+ class MockExtension
18
+ def incoming(message, callback)
19
+ callback.call(message)
20
+ end
21
+ end
22
+ add_extension(MockExtension.new)
23
+ end
24
+ end
25
+
26
+ If you really want to, you can ask Faye to start it's own listening Thin server on an arbitrary port:
27
+
28
+ App::Application.routes.draw do
29
+ faye_server '/faye', :timeout => 25 do
30
+ listen(9292)
31
+ end
32
+ end
33
+
34
+ You can also do some rudimentary routing using the map method:
35
+
36
+ App::Application.routes.draw do
37
+ faye_server '/faye', :timeout => 25 do
38
+ map '/widgets/**' => WidgetsController
39
+ map :default => :block
40
+ end
41
+ end
42
+
43
+ You can find more details on the #map method in the [rdoc](http://rubydoc.info/github/jamesotron/faye-rails/master/FayeRails/RackAdapter)
44
+
45
+ # Controller
46
+
47
+ faye-rails includes a controller for handling the binding between model events and channels with it's own DSL for managing channel-based events.
48
+
49
+ class WidgetController < FayeRails::Controller
50
+ end
51
+
52
+ ## Model observers
53
+
54
+ You can subscribe to changes in models using the controller's observer DSL:
55
+
56
+ class WidgetController < FayeRails::Controller
57
+ observe Widget, :after_create do |new_widget|
58
+ WidgetController.publish('/widgets', new_widget.attributes)
59
+ end
60
+ end
61
+
62
+ The available callbacks are derived from the ActiveRecord callback stack. See [ActiveRecord::Callbacks](http://api.rubyonrails.org/classes/ActiveRecord/Callbacks.html) for more information regarding the callback queue.
63
+
64
+ See the [rdoc](http://rubydoc.info/github/jamesotron/faye-rails/master/FayeRails/Controller.observe) for more information.
65
+
66
+ ## Channel DSL
67
+
68
+ The controller DSL elegantly wraps channel-based aspects of the Faye API so that you can easily group code based on specific channels.
69
+
70
+ ### Monitoring
71
+
72
+ You can make use of Faye's [monitoring API](http://faye.jcoglan.com/ruby/monitoring.html) by adding calls to `monitor` within the channel block. You are able to monitor `:subscribe`, `:unsubscribe` and `:publish` events. Blocks are executed within the context of a `FayeRails::Controller::Monitor` instance which will give you access to `#client_id`, `#channel` and `#data` (`#data` only having a value on `:publish` events).
73
+
74
+ class WidgetController < FayeRails::Controller
75
+ channel '/widgets' do
76
+ monitor :subscribe do
77
+ puts "Client #{client_id} subscribed to #{channel}."
78
+ end
79
+ monitor :unsubscribe do
80
+ puts "Client #{client_id} unsubscribed from #{channel}."
81
+ end
82
+ monitor :publish do
83
+ puts "Client #{client_id} published #{data.inspect} to #{channel}."
84
+ end
85
+ end
86
+ end
87
+
88
+ ### Filtering
89
+
90
+ You can quickly and easily filter incoming and outgoing messages for your specific channel using the controller's filter API, which wraps Faye's [extensions API](http://faye.jcoglan.com/ruby/extensions.html) in a concise and channel-specific way.
91
+
92
+ class WidgetController < FayeRails::Controller
93
+ channel '/widgets' do
94
+ filter :in do
95
+ puts "Inbound message #{message}."
96
+ pass
97
+ end
98
+ end
99
+ end
100
+
101
+ You can add filters for `:in`, `:out` and `:any`, which will allow you to filter messages entering the server, exiting the server or both. The block passed to the `filter` is executed in the context of a `FayeRails::Filter::DSL` instance, which gives you access to the `#message` method, which contains the entire message payload from the client (including meta information you wouldn't see other ways). You also have access to the `#pass`, `#modify`, `#block` and `#drop` methods which are sugar around Faye's callback system - which is accessible via the `#callback` method if you want to do it that way. Check out the [FayeRails::Filter::DSL rdoc](http://rubydoc.info/github/jamesotron/faye-rails/master/FayeRails/Filter/DSL) for more information. Please note that all filters must call `callback.call` either via the sugar methods or directly to ensure that requests are not lost (not to mention potential memory leaks).
102
+
103
+ ### Subscribing
104
+
105
+ You can easily subscribe to a channel using the 'subscribe' method inside your channel block, like so:
106
+
107
+ class WidgetController < FayeRails::Controller
108
+ channel '/widgets' do
109
+ subscribe do
110
+ puts "Received on channel #{channel}: #{message.inspect}"
111
+ end
112
+ end
113
+ end
114
+
115
+ # Thanks.
116
+
117
+ Thanks to James Coglan for the excellent Faye Bayeux implementetation and great support for Faye users.
data/lib/faye-rails.rb ADDED
@@ -0,0 +1,42 @@
1
+ require 'faye'
2
+ require 'faye-rails/version'
3
+ require 'faye-rails/routing_hooks'
4
+ require 'faye-rails/server_list'
5
+
6
+ module FayeRails
7
+ ROOT = File.expand_path(File.dirname(__FILE__))
8
+
9
+ if defined? ::Rails
10
+ class Engine < ::Rails::Engine
11
+ end
12
+ end
13
+
14
+ autoload :Controller, File.join(ROOT, 'faye-rails', 'controller')
15
+ autoload :RackAdapter, File.join(ROOT, 'faye-rails', 'rack_adapter')
16
+ autoload :Filter, File.join(ROOT, 'faye-rails', 'filter')
17
+
18
+ def self.servers
19
+ @servers ||= ServerList.new
20
+ end
21
+
22
+ def self.server(where=nil)
23
+ if where
24
+ servers.at(where).first
25
+ else
26
+ servers.first
27
+ end
28
+ end
29
+
30
+ def self.clients
31
+ servers.map(&:get_client)
32
+ end
33
+
34
+ def self.client(where=nil)
35
+ if where
36
+ servers.at(where).first.get_client
37
+ else
38
+ servers.first.get_client
39
+ end
40
+ end
41
+
42
+ end
@@ -0,0 +1,53 @@
1
+ module FayeRails
2
+ class Controller
3
+ autoload :Channel, File.join(FayeRails::ROOT, 'faye-rails', 'controller', 'channel')
4
+ autoload :Monitor, File.join(FayeRails::ROOT, 'faye-rails', 'controller', 'monitor')
5
+ autoload :Message, File.join(FayeRails::ROOT, 'faye-rails', 'controller', 'message')
6
+ autoload :ObserverFactory, File.join(FayeRails::ROOT, 'faye-rails', 'controller', 'observer_factory')
7
+
8
+ attr :channels, :model
9
+
10
+ # Observe a model for any of the ActiveRecord::Callbacks
11
+ # as of v3.2.6 they are:
12
+ # before_validation
13
+ # after_validation
14
+ # before_save
15
+ # before_create
16
+ # after_create
17
+ # after_save
18
+ # after_commit
19
+ # http://api.rubyonrails.org/classes/ActiveRecord/Callbacks.html
20
+ # action defaults to after_create
21
+ def self.observe(model_klass, action = :after_create, &block)
22
+ # Dynamically create a new observe class
23
+ ObserverFactory.define(model_klass, action, &block)
24
+ end
25
+
26
+ def observe(model_klass, action = :after_create, &block)
27
+ # Dynamically create a new observe class
28
+ ObserverFactory.define(model_klass, action, &block)
29
+ end
30
+
31
+ # Bind a number of events to a specific channel.
32
+ def self.channel(channel, endpoint=nil, &block)
33
+ channel = Channel.new(channel, endpoint)
34
+ channel.instance_eval(&block)
35
+ (@channels ||= []) << channel
36
+ end
37
+
38
+ def channel(channel, endpoint=nil, &block)
39
+ channel = Channel.new(channel, endpoint)
40
+ channel.instance_eval(&block)
41
+ (@channels ||= []) << channel
42
+ end
43
+
44
+ def self.publish(channel, message, endpoint=nil)
45
+ FayeRails.client(endpoint).publish(channel, message)
46
+ end
47
+
48
+ def publish(channel, message, endpoint=nil)
49
+ self.class.publish(channel, message, endpoint)
50
+ end
51
+
52
+ end
53
+ end
@@ -0,0 +1,61 @@
1
+ module FayeRails
2
+ class Controller
3
+ class Channel
4
+
5
+ attr_reader :channel, :endpoint
6
+
7
+ def initialize(channel, endpoint=nil)
8
+ @channel = channel
9
+ @endpoint = endpoint
10
+ end
11
+
12
+ def client
13
+ FayeRails.client(endpoint)
14
+ end
15
+
16
+ def publish(message)
17
+ FayeRails.client(endpoint).publish(channel, message)
18
+ end
19
+
20
+ def monitor(event, &block)
21
+ raise ArgumentError, "Unknown event #{event.inspect}" unless [:subscribe,:unsubscribe,:publish].member? event
22
+
23
+ FayeRails.server(endpoint).bind(event) do |*args|
24
+ Monitor.new.tap do |m|
25
+ m.client_id = args.shift
26
+ m.channel = args.shift
27
+ m.data = args.shift
28
+ m.instance_eval(&block) if m.channel == channel
29
+ end
30
+ end
31
+ end
32
+
33
+ def filter(direction=:any, &block)
34
+ filter = FayeRails::Filter.new(channel, direction, block)
35
+ server = FayeRails.server(endpoint)
36
+ server.add_extension(filter)
37
+ filter.server = server
38
+ filter
39
+ end
40
+
41
+ def subscribe(&block)
42
+ EM.schedule do
43
+ @subscription = FayeRails.client(endpoint).subscribe(channel) do |message|
44
+ Message.new.tap do |m|
45
+ m.message = message
46
+ m.channel = channel
47
+ m.instance_eval(&block)
48
+ end
49
+ end
50
+ end
51
+ end
52
+
53
+ def unsubscribe
54
+ EM.schedule do
55
+ FayeRails.client(endpoint).unsubscribe(@subscription)
56
+ end
57
+ end
58
+
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,10 @@
1
+ module FayeRails
2
+ class Controller
3
+ class Message
4
+
5
+ attr_accessor :message
6
+ attr_accessor :channel
7
+
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,9 @@
1
+ module FayeRails
2
+ class Controller
3
+ class Monitor
4
+
5
+ attr_accessor :client_id, :channel, :data
6
+
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,54 @@
1
+ require 'active_record'
2
+
3
+ module FayeRails
4
+ class Controller
5
+
6
+ # Module creates ActiveRecord::Observer instances
7
+ module ObserverFactory
8
+
9
+ # Create
10
+ def self.define(klass, method_name, &block)
11
+ # Make a name for the observer
12
+ klass_observer_name = "#{klass.to_s}Observer"
13
+
14
+ # Load the observer if one exists
15
+ klass_observer = ObserverFactory.observer(klass_observer_name)
16
+
17
+ new_observer = klass_observer.nil?
18
+
19
+ # Create a new observer if one does not exist
20
+ klass_observer = Object.const_set(klass_observer_name, Class.new(ActiveRecord::Observer) do
21
+ # TODO Work around this hack.
22
+ # Have to define all of the available methods when creating the Observer class for the
23
+ # first time. The methods can then be overriden by the observe DSL. However if they
24
+ # are not first defined then they will not be registerable.
25
+ [:before_validation, :after_validation, :before_save, :before_create, :after_create, :after_save, :after_commit].each do |arg|
26
+ send :define_method, arg do |temp|
27
+ end
28
+ end
29
+ end) if new_observer
30
+
31
+ # Add the method to the observer
32
+ klass_observer.instance_eval do
33
+ define_method(method_name, &block)
34
+ end
35
+
36
+ # Add the observer if needed
37
+ if new_observer
38
+ ActiveRecord::Base.observers << klass_observer
39
+ end
40
+
41
+ ActiveRecord::Base.instantiate_observers
42
+ end
43
+
44
+ def self.observer(class_name)
45
+ klass = Module.const_get(class_name)
46
+ return klass if klass.is_a?(Class)
47
+ return nil
48
+ rescue
49
+ return nil
50
+ end
51
+
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,179 @@
1
+ module FayeRails
2
+ class Filter
3
+
4
+ attr_accessor :server
5
+ attr_reader :channel
6
+ attr_writer :logger
7
+
8
+ # Create a new FayeRails::Filter which can be passed to
9
+ # Faye::RackAdapter#add_extension.
10
+ #
11
+ # @param channel
12
+ # Optional channel name to limit messages to.
13
+ # @param direction
14
+ # :in, :out or :any.
15
+ # @param block
16
+ # A proc object to be called when filtering messages.
17
+ def initialize(channel='/**', direction=:any, block)
18
+ @channel = channel
19
+ @block = block
20
+ raise ArgumentError, "Block cannot be nil" unless block
21
+ if (direction == :in) || (direction == :any)
22
+ @in_filter = DSL
23
+ end
24
+ if (direction == :out) || (direction == :any)
25
+ @out_filter = DSL
26
+ end
27
+ end
28
+
29
+ def respond_to?(method)
30
+ if (method == :incoming)
31
+ !!@in_filter
32
+ elsif (method == :outgoing)
33
+ !!@out_filter
34
+ else
35
+ super
36
+ end
37
+ end
38
+
39
+ def logger
40
+ if defined?(::Rails)
41
+ @logger ||= Rails.logger
42
+ end
43
+ end
44
+
45
+ def incoming(message, callback)
46
+ @in_filter.new(@block, message, channel, callback, :incoming) if @in_filter
47
+ end
48
+
49
+
50
+ def outgoing(message, callback)
51
+ @out_filter.new(@block, message, channel, callback, :outgoing) if @out_filter
52
+ end
53
+
54
+ def destroy
55
+ if server
56
+ server.remove_extension(self)
57
+ end
58
+ end
59
+
60
+ class DSL
61
+
62
+ # A small wrapper class around filter blocks to
63
+ # add some sugar to ease filter (Faye extension)
64
+ # creation.
65
+
66
+ attr_reader :channel, :message, :callback, :original_message, :direction
67
+
68
+ # Called by FayeRails::Filter when Faye passes
69
+ # messages in for evaluation.
70
+ # @param block
71
+ # The block you wish to execute whenever a matching
72
+ # message is recieved.
73
+ # @param channel
74
+ # optional: if present then the block will only be called for matching messages, otherwise all messages will be passed.
75
+ def initialize(block, message, channel='/**', callback, direction)
76
+ raise ArgumentError, "Block cannot be nil" unless block
77
+ @channel = channel
78
+ @original_message = message.dup
79
+ @message = message
80
+ @callback = callback
81
+ @direction = direction
82
+
83
+ if channel_matches?(@channel, @original_message['channel']) ||
84
+ (subscribing? && subscription?(@channel)) ||
85
+ (unsubscribing? && subscription?(@channel))
86
+ instance_eval(&block)
87
+ else
88
+ pass
89
+ end
90
+ end
91
+
92
+ # Easier than testing message['channel'] every time
93
+ def subscribing?
94
+ message['channel'] == '/meta/subscribe'
95
+ end
96
+
97
+ def unsubscribing?
98
+ message['channel'] == '/meta/unsubscribe'
99
+ end
100
+
101
+ def meta?
102
+ message['channel'][0..5] == '/meta/'
103
+ end
104
+
105
+ def service?
106
+ message['channel'][0..8] == '/service/'
107
+ end
108
+
109
+ def incoming?
110
+ direction == :incoming
111
+ end
112
+ alias in? incoming?
113
+
114
+ def outgoing?
115
+ direction == :outgoing
116
+ end
117
+ alias out? outgoing?
118
+
119
+ def data
120
+ message['data']
121
+ end
122
+
123
+ def data?
124
+ !!data
125
+ end
126
+
127
+ def client_id?(x=nil)
128
+ if !!x
129
+ message['client_id'] == x
130
+ else
131
+ !!message['client_id']
132
+ end
133
+ end
134
+
135
+ def channel_matches?(glob,test)
136
+ File.fnmatch? glob, test
137
+ end
138
+
139
+ def subscription?(channel)
140
+ message['subscription'] && channel_matches?(channel, message['subscription'])
141
+ end
142
+
143
+ # Syntactic sugar around callback.call which passes
144
+ # back the original message unmodified.
145
+ def pass
146
+ return callback.call(original_message)
147
+ end
148
+
149
+ # Syntactic sugar around callback.call which passes
150
+ # the passed argument back to Faye in place of the
151
+ # original message.
152
+ # @param new_message
153
+ # Replacement message to send back to Faye.
154
+ def modify(new_message)
155
+ return callback.call(new_message)
156
+ end
157
+
158
+ # Syntactic sugar around callback.call which adds
159
+ # an error message to the message and passes it back
160
+ # to Faye, which will send back a rejection message to
161
+ # the sending client.
162
+ # @param reason
163
+ # The error message to be sent back to the client.
164
+ def block(reason="Message blocked by filter")
165
+ new_message = message
166
+ new_message['error'] = reason
167
+ return callback.call(new_message)
168
+ end
169
+
170
+ # Syntactic sugar around callback.call which returns
171
+ # nil to Faye - effectively dropping the message.
172
+ def drop
173
+ return callback.call(nil)
174
+ end
175
+
176
+ end
177
+
178
+ end
179
+ end