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 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