amqp-events 0.0.2 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|