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 CHANGED
@@ -9,3 +9,7 @@
9
9
  == 0.0.2 / 2010-10-22
10
10
 
11
11
  * C# example extended
12
+
13
+ == 0.0.3 / 2010-10-28
14
+
15
+ * Project updated for RSpec 2
data/Rakefile CHANGED
@@ -11,13 +11,7 @@ require 'version'
11
11
  CLASS_NAME = AMQP::Events
12
12
  VERSION = CLASS_NAME::VERSION
13
13
 
14
- begin
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.2
1
+ 0.0.3
@@ -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
@@ -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
- def == (other)
88
- case other
89
- when Event
90
- super
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
- private
99
- def generate_subscriber_name(subscriber)
100
- "#{subscriber.respond_to?(:name) ? subscriber.name : 'subscriber'}-#{UUID.generate}".to_sym
101
- end
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
- def events
105
- @events ||= self.class.instance_events.inject({}) { |hash, name| hash[name]=Event.new(name); hash }
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
- def event(name)
109
- sym_name = name.to_sym
110
- self.class.event(sym_name)
111
- events[sym_name] ||= Event.new(sym_name)
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 (name)
39
+ def event(name, opts = {})
40
+ sym_name = name.to_sym
123
41
 
124
- instance_events << name.to_sym
125
- # Defines instance method that has the same name as the Event being declared.
126
- # Calling it without arguments returns Event object itself
127
- # Calling it with block adds unnamed subscriber for Event
128
- # Calling it with arguments fires the Event
129
- # Such a messy interface provides some compatibility with C# events behavior
130
- define_method name do |*args, &block|
131
- events[name] ||= Event.new(name)
132
- if args.empty?
133
- if block
134
- events[name].subscribe &block
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[name]
61
+ events[sym_name].fire(*args)
137
62
  end
138
- else
139
- events[name].fire(*args)
140
63
  end
141
- end
142
64
 
143
- # Needed to support C#-like syntax : my_event -= subscriber
144
- define_method "#{name}=" do |event|
145
- if event.kind_of? Event
146
- events[name] = event
147
- else
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