amqp-events 0.0.2 → 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- data/HISTORY +4 -0
- data/Rakefile +1 -7
- data/VERSION +1 -1
- data/lib/amqp-events.rb +15 -2
- data/lib/amqp-events/event.rb +140 -0
- data/lib/amqp-events/event_manager.rb +19 -0
- data/lib/amqp-events/events.rb +47 -126
- data/spec/amqp-events/event_manager_spec.rb +63 -0
- data/spec/amqp-events/event_spec.rb +74 -0
- data/spec/amqp-events/events_spec.rb +89 -0
- data/spec/cs_spec.rb +2 -4
- data/spec/shared_examples.rb +282 -0
- data/spec/spec_helper.rb +40 -11
- data/tasks/spec.rake +3 -7
- metadata +37 -15
- data/spec/amqp-events_spec.rb +0 -214
- data/spec/spec.opts +0 -2
data/HISTORY
CHANGED
data/Rakefile
CHANGED
@@ -11,13 +11,7 @@ require 'version'
|
|
11
11
|
CLASS_NAME = AMQP::Events
|
12
12
|
VERSION = CLASS_NAME::VERSION
|
13
13
|
|
14
|
-
|
15
|
-
require 'rake'
|
16
|
-
rescue LoadError
|
17
|
-
require 'rubygems'
|
18
|
-
gem 'rake', '~> 0.8.3.1'
|
19
|
-
require 'rake'
|
20
|
-
end
|
14
|
+
require 'rake'
|
21
15
|
|
22
16
|
# Load rakefile tasks
|
23
17
|
Dir['tasks/*.rake'].sort.each { |file| load file }
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.0.
|
1
|
+
0.0.3
|
data/lib/amqp-events.rb
CHANGED
@@ -17,10 +17,23 @@ module AMQP
|
|
17
17
|
Pathname.glob(name.to_s).sort.each { |rb| require rb }
|
18
18
|
end
|
19
19
|
end
|
20
|
+
|
21
|
+
if RUBY_PLATFORM =~ /mingw|mswin|windows/ #Windows!
|
22
|
+
require 'uuid'
|
23
|
+
UUID = UUID
|
24
|
+
else
|
25
|
+
require 'em/pure_ruby' unless defined? EventMachine::UuidGenerator
|
26
|
+
UUID = EventMachine::UuidGenerator
|
27
|
+
end
|
28
|
+
|
29
|
+
class HandlerError < TypeError
|
30
|
+
end
|
31
|
+
|
32
|
+
class EventError < TypeError
|
33
|
+
end
|
20
34
|
end
|
21
35
|
end
|
22
36
|
|
23
37
|
# Require all ruby source files located under directory lib/amqp-events
|
24
38
|
# If you need files in specific order, you should specify it here before the glob
|
25
|
-
AMQP::Events.require_libs %W[**/*]
|
26
|
-
|
39
|
+
AMQP::Events.require_libs %W[events event **/*]
|
@@ -0,0 +1,140 @@
|
|
1
|
+
module AMQP
|
2
|
+
module Events
|
3
|
+
|
4
|
+
# Represent Event that can be subscribed to (by subscribers/observers)
|
5
|
+
# All subscribed observers will be called when this Event 'fires'.
|
6
|
+
#
|
7
|
+
# TODO: Mutexes to synchronize @subscribers update/event fire ?
|
8
|
+
# http://github.com/snuxoll/ruby-event/blob/master/lib/ruby-event/event.rb
|
9
|
+
# TODO: Meta-methods that allow Events to fire on method invocations:
|
10
|
+
# http://github.com/nathankleyn/ruby_events/blob/85f8e6027fea22e9d828c91960ce2e4099a9a52f/lib/ruby_events.rb
|
11
|
+
# TODO: Add exception handling and subscribe/unsubscribe notifications:
|
12
|
+
# http://github.com/matsadler/rb-event-emitter/blob/master/lib/events.rb
|
13
|
+
class Event
|
14
|
+
|
15
|
+
class << self
|
16
|
+
protected :new
|
17
|
+
|
18
|
+
# Creates Event of appropriate subclass, depending on arguments
|
19
|
+
def create host, name, opts={}, &block
|
20
|
+
opts.empty? ? Event.new(host, name, &block ) : ExternalEvent.new(host, name, opts, &block)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
attr_reader :host, :name, :subscribers
|
25
|
+
alias_method :listeners, :subscribers
|
26
|
+
|
27
|
+
def initialize(host, name)
|
28
|
+
@host = host
|
29
|
+
@name = name.to_sym
|
30
|
+
@subscribers = {}
|
31
|
+
end
|
32
|
+
|
33
|
+
# You can subscribe anything callable to the event, such as lambda/proc,
|
34
|
+
# method(:method_name), attached block or a custom Handler. The only requirements,
|
35
|
+
# it should respond to a #call and accept arguments given to Event#fire.
|
36
|
+
#
|
37
|
+
# You can give optional name to your subscriber. If you do, you can later
|
38
|
+
# unsubscribe it using this name. If you do not give subscriber a name, it will
|
39
|
+
# be auto-generated using its #name method and uuid.
|
40
|
+
#
|
41
|
+
# You can unsubscribe your subscriber later, provided you know its name.
|
42
|
+
#
|
43
|
+
# :call-seq:
|
44
|
+
# event.subscribe("subscriber_name", proc{|*args| "Subscriber 1"})
|
45
|
+
# event.subscribe("subscriber_name", method(:method_name))
|
46
|
+
# event.subscribe(method(:method_name) # Implicit subscriber name == :method_name
|
47
|
+
# event.subscribe("subscriber_name") {|*args| "Named subscriber block" }
|
48
|
+
# event += method(:method_name) # C# compatible syntax, just without useless "delegates"
|
49
|
+
#
|
50
|
+
def subscribe(*args, &block)
|
51
|
+
subscriber = block ? block : args.pop
|
52
|
+
name = args.empty? ? generate_subscriber_name(subscriber) : args.first.to_sym
|
53
|
+
|
54
|
+
raise HandlerError.new "Handler #{subscriber.inspect} does not respond to #call" unless subscriber.respond_to? :call
|
55
|
+
raise HandlerError.new "Handler name #{name} already in use" if @subscribers.has_key? name
|
56
|
+
@subscribers[name] = subscriber
|
57
|
+
|
58
|
+
self # This allows C#-like syntax : my_event += subscriber
|
59
|
+
end
|
60
|
+
|
61
|
+
# Unsubscribe existing subscriber by name
|
62
|
+
def unsubscribe(name)
|
63
|
+
raise HandlerError.new "Unable to unsubscribe handler #{name}" unless @subscribers.has_key? name
|
64
|
+
@subscribers.delete(name)
|
65
|
+
|
66
|
+
self # This allows C#-like syntax : my_event -= subscriber
|
67
|
+
end
|
68
|
+
|
69
|
+
# TODO: make fire async: just fire and continue, instead of waiting for all subscribers to return,
|
70
|
+
# as it is right now. AMQP callbacks and EM:Deferrable?
|
71
|
+
def fire(*args)
|
72
|
+
@subscribers.each do |key, subscriber|
|
73
|
+
subscriber.call *args
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
alias_method :listen, :subscribe
|
78
|
+
alias_method :+, :subscribe
|
79
|
+
alias_method :remove, :unsubscribe
|
80
|
+
alias_method :-, :unsubscribe
|
81
|
+
alias_method :call, :fire
|
82
|
+
|
83
|
+
# Clears all the subscribers for a given Event
|
84
|
+
def clear
|
85
|
+
@subscribers.clear
|
86
|
+
end
|
87
|
+
|
88
|
+
def == (other)
|
89
|
+
case other
|
90
|
+
when Event
|
91
|
+
super
|
92
|
+
when nil
|
93
|
+
@subscribers.empty?
|
94
|
+
else
|
95
|
+
false
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
private
|
100
|
+
def generate_subscriber_name(subscriber)
|
101
|
+
"#{subscriber.respond_to?(:name) ? subscriber.name : 'subscriber'}-#{UUID.generate}".to_sym
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
|
106
|
+
# Represent external Event that can be subscribed to (by subscribers/observers).
|
107
|
+
# External means, it happens outside of this Ruby process, and is delivered through Transport.
|
108
|
+
# When Transport informs ExternalEvent that it happened (someplace else),
|
109
|
+
# ExternalEvent 'fires' and makes sure that all subscribers are called.
|
110
|
+
#
|
111
|
+
# Any evented object (host) that defines ExternalEvent should provide it with transport either
|
112
|
+
# explicitly (via option :transport) or expose its own #transport.
|
113
|
+
#
|
114
|
+
class ExternalEvent < Event
|
115
|
+
attr_reader :transport
|
116
|
+
|
117
|
+
def initialize(host, name, opts)
|
118
|
+
@routing = opts[:routing]
|
119
|
+
@transport = opts[:transport] || host.transport rescue nil
|
120
|
+
raise EventError.new "Unable to create ExternalEvent #{name.inspect} without routing" unless @routing
|
121
|
+
raise EventError.new "Unable to create ExternalEvent #{name.inspect} without transport" unless @transport
|
122
|
+
super host, name
|
123
|
+
end
|
124
|
+
|
125
|
+
# Subscribe to external event... Uses @host's transport for actual subscription
|
126
|
+
def subscribe(*args, &block)
|
127
|
+
super *args, &block
|
128
|
+
@transport.subscribe(@routing) {|routing, data| fire(routing, data) } if @subscribers.size == 1
|
129
|
+
self # This allows C#-like syntax : my_event += subscriber
|
130
|
+
end
|
131
|
+
|
132
|
+
# Unsubscribe from external event... Cancels @host's transport subscription if no subscribers left
|
133
|
+
def unsubscribe(name)
|
134
|
+
super name
|
135
|
+
@transport.unsubscribe(@routing) if @subscribers.empty?
|
136
|
+
self # This allows C#-like syntax : my_event -= subscriber_name
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
#require 'mq'
|
2
|
+
|
3
|
+
module AMQP
|
4
|
+
module Events
|
5
|
+
|
6
|
+
# Exposes external Events (received via transport) as its own local Events:
|
7
|
+
#
|
8
|
+
class EventManager
|
9
|
+
include AMQP::Events
|
10
|
+
|
11
|
+
attr_accessor :transport
|
12
|
+
|
13
|
+
def initialize(transport)
|
14
|
+
@transport = transport
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/lib/amqp-events/events.rb
CHANGED
@@ -1,157 +1,78 @@
|
|
1
1
|
module AMQP
|
2
2
|
module Events
|
3
|
-
if RUBY_PLATFORM =~ /mingw|mswin|windows/ #Windows!
|
4
|
-
require 'uuid'
|
5
|
-
UUID = UUID
|
6
|
-
else
|
7
|
-
require 'em/pure_ruby'
|
8
|
-
UUID = EventMachine::UuidGenerator
|
9
|
-
end
|
10
|
-
|
11
|
-
class HandlerError < TypeError
|
12
|
-
end
|
13
|
-
|
14
|
-
# TODO: Mutexes to synchronize @subscribers update ?
|
15
|
-
# http://github.com/snuxoll/ruby-event/blob/master/lib/ruby-event/event.rb
|
16
|
-
# TODO: Meta-methods that allow Events to fire on method invocations:
|
17
|
-
# http://github.com/nathankleyn/ruby_events/blob/85f8e6027fea22e9d828c91960ce2e4099a9a52f/lib/ruby_events.rb
|
18
|
-
# TODO: Add exception handling and subscribe/unsubscribe notifications:
|
19
|
-
# http://github.com/matsadler/rb-event-emitter/blob/master/lib/events.rb
|
20
|
-
class Event
|
21
|
-
|
22
|
-
attr_reader :name, :subscribers
|
23
|
-
alias_method :listeners, :subscribers
|
24
|
-
|
25
|
-
def initialize(name)
|
26
|
-
@name = name
|
27
|
-
@subscribers = {}
|
28
|
-
end
|
29
|
-
|
30
|
-
# You can subscribe anything callable to the event, such as lambda/proc,
|
31
|
-
# method(:method_name), attached block or a custom Handler. The only requirements,
|
32
|
-
# it should respond to a #call and accept arguments given to Event#fire.
|
33
|
-
#
|
34
|
-
# You can give optional name to your subscriber. If you do, you can later
|
35
|
-
# unsubscribe it using this name. If you do not give subscriber a name, it will
|
36
|
-
# be auto-generated using its #name method and uuid.
|
37
|
-
#
|
38
|
-
# You can unsubscribe your subscriber later, provided you know its name.
|
39
|
-
#
|
40
|
-
# :call-seq:
|
41
|
-
# event.subscribe("subscriber_name", proc{|*args| "Subscriber 1"})
|
42
|
-
# event.subscribe("subscriber_name", method(:method_name))
|
43
|
-
# event.subscribe(method(:method_name) # Implicit subscriber name == :method_name
|
44
|
-
# event.subscribe("subscriber_name") {|*args| "Named subscriber block" }
|
45
|
-
# event += method(:method_name) # C# compatible syntax, just without useless "delegates"
|
46
|
-
#
|
47
|
-
def subscribe(*args, &block)
|
48
|
-
subscriber = block ? block : args.pop
|
49
|
-
name = args.empty? ? generate_subscriber_name(subscriber) : args.first
|
50
|
-
|
51
|
-
raise HandlerError.new "Handler #{subscriber.inspect} does not respond to #call" unless subscriber.respond_to? :call
|
52
|
-
raise HandlerError.new "Handler name #{name} already in use" if @subscribers.has_key? name
|
53
|
-
@subscribers[name] = subscriber
|
54
|
-
|
55
|
-
self # This allows C#-like syntax : my_event += subscriber
|
56
|
-
end
|
57
|
-
|
58
|
-
alias_method :listen, :subscribe
|
59
|
-
alias_method :+, :subscribe
|
60
|
-
|
61
|
-
# Unsubscribe existing subscriber by name
|
62
|
-
def unsubscribe(name)
|
63
|
-
raise HandlerError.new "Unable to unsubscribe handler #{name}" unless @subscribers.has_key? name
|
64
|
-
@subscribers.delete(name)
|
65
|
-
|
66
|
-
self # This allows C#-like syntax : my_event -= subscriber
|
67
|
-
end
|
68
|
-
|
69
|
-
alias_method :remove, :unsubscribe
|
70
|
-
alias_method :-, :unsubscribe
|
71
|
-
|
72
|
-
# TODO: make fire async: just fire and continue, instead of waiting for all subscribers to return,
|
73
|
-
# as it is right now. AMQP callbacks and EM:Deferrable?
|
74
|
-
def fire(*args)
|
75
|
-
@subscribers.each do |key, subscriber|
|
76
|
-
subscriber.call *args
|
77
|
-
end
|
78
|
-
end
|
79
|
-
|
80
|
-
alias_method :call, :fire
|
81
|
-
|
82
|
-
# Clears all the subscribers for a given Event
|
83
|
-
def clear
|
84
|
-
@subscribers.clear
|
85
|
-
end
|
86
3
|
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
when nil
|
92
|
-
@subscribers.empty?
|
93
|
-
else
|
94
|
-
false
|
95
|
-
end
|
4
|
+
def events
|
5
|
+
@events ||= self.class.instance_events.inject({}) do |events, (name, opts)|
|
6
|
+
events[name] = Event.create(self, name, opts)
|
7
|
+
events
|
96
8
|
end
|
9
|
+
end
|
97
10
|
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
11
|
+
def event(name, opts = {})
|
12
|
+
sym_name = name.to_sym
|
13
|
+
self.class.event(sym_name, opts)
|
14
|
+
events[sym_name] ||= Event.create(self, sym_name, opts)
|
102
15
|
end
|
103
16
|
|
104
|
-
|
105
|
-
|
17
|
+
# object#subscribe(:Event) is a sugar-coat for object.Event#subscribe
|
18
|
+
def subscribe(event, *args, &block)
|
19
|
+
event(event).subscribe(*args, &block)
|
106
20
|
end
|
107
21
|
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
events[
|
22
|
+
# object#unsubscribe(:Event) is a sugar-coat for object.Event#unsubscribe
|
23
|
+
def unsubscribe(event, *args, &block)
|
24
|
+
raise HandlerError.new "Unable to unsubscribe, there is no event #{event}" unless events[event.to_sym]
|
25
|
+
events[event.to_sym].unsubscribe(*args, &block)
|
112
26
|
end
|
113
27
|
|
28
|
+
alias_method :listen, :subscribe
|
29
|
+
alias_method :remove, :unsubscribe
|
30
|
+
|
114
31
|
# Once included into a class/module, gives this module .event macros for declaring events
|
115
32
|
def self.included(host)
|
116
33
|
|
117
34
|
host.instance_exec do
|
118
35
|
def instance_events
|
119
|
-
@instance_events ||=
|
36
|
+
@instance_events ||= {}
|
120
37
|
end
|
121
38
|
|
122
|
-
def event
|
39
|
+
def event(name, opts = {})
|
40
|
+
sym_name = name.to_sym
|
123
41
|
|
124
|
-
instance_events
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
42
|
+
if instance_events.has_key? sym_name
|
43
|
+
raise EventError.new "Unable to redefine Event #{name} with options #{opts.inspect}" if instance_events[sym_name] != opts
|
44
|
+
else
|
45
|
+
instance_events[sym_name] = opts
|
46
|
+
|
47
|
+
# Defines instance method that has the same name as the Event being declared.
|
48
|
+
# Calling it without arguments returns Event object itself
|
49
|
+
# Calling it with block adds unnamed subscriber for Event
|
50
|
+
# Calling it with arguments fires the Event
|
51
|
+
# Such a messy interface provides some compatibility with C# events behavior
|
52
|
+
define_method name do |*args, &block|
|
53
|
+
events[sym_name] ||= Event.create(self, sym_name, opts)
|
54
|
+
if args.empty?
|
55
|
+
if block
|
56
|
+
events[sym_name].subscribe &block
|
57
|
+
else
|
58
|
+
events[sym_name]
|
59
|
+
end
|
135
60
|
else
|
136
|
-
events[
|
61
|
+
events[sym_name].fire(*args)
|
137
62
|
end
|
138
|
-
else
|
139
|
-
events[name].fire(*args)
|
140
63
|
end
|
141
|
-
end
|
142
64
|
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
events[name]
|
147
|
-
|
148
|
-
raise Events::SubscriberTypeError.new "Attempted assignment #{event.inspect} is not an Event"
|
65
|
+
# Only supports assignment of the Event to self
|
66
|
+
# Needed to support C#-like syntax : my_event += subscriber
|
67
|
+
define_method "#{name}=" do |event|
|
68
|
+
raise EventError.new("Wrong assignment of #{event.inspect} to #{events[name.to_sym].inspect}"
|
69
|
+
) unless event.equal? events[name.to_sym]
|
149
70
|
end
|
150
71
|
end
|
72
|
+
sym_name # .event macro returns defined sym name
|
151
73
|
end
|
152
74
|
end
|
153
75
|
|
154
76
|
end
|
155
|
-
|
156
77
|
end
|
157
78
|
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe AMQP::Events::EventManager, " as class" do
|
4
|
+
subject { described_class }
|
5
|
+
|
6
|
+
it_should_behave_like 'evented class'
|
7
|
+
|
8
|
+
# its(:instance_events) { should include :ExternalEventReceived}
|
9
|
+
|
10
|
+
end
|
11
|
+
|
12
|
+
describe AMQP::Events::EventManager, " when initialized" do
|
13
|
+
before { @transport ||= mock('transport').as_null_object }
|
14
|
+
subject { described_class.new @transport }
|
15
|
+
|
16
|
+
it_should_behave_like 'evented object'
|
17
|
+
specify { should respond_to :transport }
|
18
|
+
its(:transport) { should_not be nil }
|
19
|
+
|
20
|
+
it "should allow external events to be defined" do
|
21
|
+
res = subject.event :ExternalBar, routing: '#.bar.#'
|
22
|
+
res.should be_an AMQP::Events::ExternalEvent
|
23
|
+
end
|
24
|
+
|
25
|
+
context 'with a mix of external and internal Events' do
|
26
|
+
before do
|
27
|
+
@event_manager = described_class.new @transport
|
28
|
+
@event_manager.event :Foo
|
29
|
+
@event_manager.event :ExternalBar, routing: '#.bar.#'
|
30
|
+
end
|
31
|
+
subject { @event_manager }
|
32
|
+
|
33
|
+
its(:events) { should_not be_empty }
|
34
|
+
its(:events) { should have_key :Foo }
|
35
|
+
its(:events) { should have_key :ExternalBar }
|
36
|
+
|
37
|
+
context 'any of its defined external Events' do
|
38
|
+
subject { @event_manager.ExternalBar }
|
39
|
+
specify {should be_an AMQP::Events::ExternalEvent}
|
40
|
+
it_should_behave_like 'event'
|
41
|
+
end
|
42
|
+
|
43
|
+
context 'any of its defined internal Events' do
|
44
|
+
subject { @event_manager.Foo }
|
45
|
+
specify {should be_an AMQP::Events::Event}
|
46
|
+
it_should_behave_like 'event'
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
context 'subscribing to EventManager`s Events' do
|
51
|
+
it "should allow objects to subscribe to its internal Events (without engaging Transport)" do
|
52
|
+
event = subject.subscribe(:Burp) { |key, data| p key, data }
|
53
|
+
event.should be_an AMQP::Events::Event
|
54
|
+
@transport.should_not_receive :subscribe
|
55
|
+
end
|
56
|
+
|
57
|
+
it "should allow objects to subscribe to external Events (through Transport)" do
|
58
|
+
@transport.should_receive(:subscribe).with '#.log.#'
|
59
|
+
event = subject.event(:LogEvent, routing: '#.log.#').subscribe(:my_log) { |key, data| p key, data }
|
60
|
+
event.should be_an AMQP::Events::ExternalEvent
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|