amqp-events 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,24 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+ log
21
+ .idea
22
+ .bundle
23
+
24
+ ## PROJECT::SPECIFIC
data/HISTORY ADDED
@@ -0,0 +1,11 @@
1
+ == 0.0.0 / 2010-10-22
2
+
3
+ * Birthday!
4
+
5
+ == 0.0.1 / 2010-10-22
6
+
7
+ * Initial setup with C# specs
8
+
9
+ == 0.0.2 / 2010-10-22
10
+
11
+ * C# example extended
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Arvicco
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,115 @@
1
+ = amqp-events
2
+ by: Arvicco
3
+ url: http://github.com/arvicco/amqp-events
4
+
5
+ == SUMMARY:
6
+
7
+ Distributed Events/RPC system using AMQP as a transport.
8
+
9
+ This is still work in progress, consider this library a deep deep pre-alpha at this point...
10
+
11
+ == DESCRIPTION:
12
+
13
+ What is an event, anyway? The way I understand it, it's kind of a method in reverse: instead of you calling
14
+ it when you please with arguments and waiting for result, you humbly ask in to call YOU when it feels like.
15
+ And indeed, when something happens and event "fires", you (your subscriber) get(s) called with arguments.
16
+ This way, you can be sure that you'll be notified about external happenings, without the need to constantly
17
+ (and inefficiently) poll your objects of interest.
18
+
19
+ In order to coordinate activity of several daemons working on multiple hosts, I would like to use distributed events.
20
+ Events here are understood as one-way messages emitted by daemons "to whom it may concern", with specific routing
21
+ (categorization) and payload (event details, data). Other daemons may subscribe to events of specified category(ies)
22
+ and/or sent by specified emitter. Such subscribers will receive only requested events, and nothing else.
23
+
24
+ So, for example, you can make your daemon 'WorkerDaemon#1' emit 'LogEntry' event with routing like
25
+ 'log.worker_daemon.1.log_entry.error'. Somewhere else, you may have 'LogServer' daemon that subscribes to
26
+ ALL 'LogEntry' events from ALL other deamons - once received, they are processed and logged to safe place.
27
+ You may also have another 'Monitoring' daemon that subscribes only to errors from all (or a specific set of)
28
+ daemons, inspects the errors received from them and reacts as appropriate. Such approach is much more clean
29
+ and efficient than parsing log files for errors.
30
+
31
+ You can further build an asynchronous RPC on top of such distributed Event system without too much sweat.
32
+
33
+ AMQP seems like a natural choice for propagating such Events, Events map to AMQP messages and routing maps to
34
+ AMQP exchange/topics structure very well.
35
+
36
+ == IMPLEMENTATION:
37
+
38
+ This README will double as a Design Document for the project, so here goes implementation detail...
39
+
40
+ I see following layers of abstraction for this model (by Participant I mean any daemon using Event capabilities):
41
+ * Events - module adding Event capabilities to objects
42
+ * EventManager - used by Participant to emit Events and subscribe to external Events
43
+ * Transport - actual wire protocol to send/receive external Events, that is a library wrapping AMQP
44
+ * Serializer - used to dump Event content before sending over Transport and load content received from Transport
45
+ * ServiceManager - used by Participant to expose its services and consume services by other Participants
46
+
47
+ *Events* vision:
48
+ Internal Events. Any objects can declare Events of interest, and other objects can subscribe to them
49
+ (with a callable subscriber object, such as block, proc or method). Object can then either fire its declared Event
50
+ manually, or tie Event to a specific method invocation (fire on method).
51
+ When an Event fires, any object that subscribed to it receives a call to its registered subscriber with arguments
52
+ supplied to Event#fire (or to the invoked method that this Event was tied to). This is a bit similar to Ruby's
53
+ Observable mixin, but the difference is that multiple events may be declared by any object, not just a single type
54
+ of event fired by changed/notify.
55
+ External Events. Event system is extended beyond a single Ruby process with a help of EventManager. EventManager
56
+ represents a proxy to external Events (that are a fired by other daemons). Any object can subscribe to external Events
57
+ declared by EventManager using, again, a callable subscriber. This subscriber will receive
58
+
59
+ *EventManager* encapsulates interface to external Events. It is used by Participant to:
60
+ * emit Events (for general use or for specific <groups of> other Participants)
61
+ * subscribe to Events (both external and internal)
62
+
63
+ EventManager manages Participant’s subscriptions to external Events, and sends Events emitted by Participant
64
+ (with appropriate Routing) through Transport.
65
+
66
+ Essentially, Event is no different from “AMQP message”. It is called “Event” to emphasize its role in driving
67
+ behavior and changing internal state of Participants. Each Event has Routing and Payload.
68
+
69
+ *Routing* is in form of (root.type.categories.[emitter].severity.event_details). Participant emits Events with
70
+ specific Routing, anyone subscribing to this routing receives this message and has to process the payload.
71
+ A portion of routing may be “fixed” in Exchange/Queue name (exchange ‘root.events.system’ or
72
+ ‘root.data.stocks.tick.received’), another portion present as a topic, such as ‘my_host.driver.8156.info.quik.started’
73
+ or (with emitter omitted) ‘us.nyce.goog’. Emitter identification (if present), should come first in topic portion,
74
+ in the form of: host.participant_type.participant_id(process?uuid?)
75
+
76
+ *Payload* is serialized and its internal structure is specific to the type of Event.
77
+ Possible Event types (with severities) include:
78
+ * Notifications (debug, info, warning)
79
+ * Exceptions (error, critical, fatal)
80
+ * Data (ticks, orders, etc),
81
+ * RPC (command, return)
82
+ ...
83
+
84
+ *Transport* encapsulates external messaging middleware (AMQP library). Its role is to send data to external destination
85
+ (as defined by Routing) and deliver data received from external destination as instructed. It is used by EventManager
86
+ to send serialized Events and subscribe to external Events. It encapsulates knowledge about exchanges and queues, and
87
+ converts Routing requested by EventManager into actual combination of ‘exchange/queue name’ + ‘topic routing’.
88
+ Where exactly does it get knowledge of actual exchange names, formats, etc from? Interface should be something like
89
+ send(routing, message), subscribe(routing)...
90
+
91
+ *Serializer* (Message Formatter) encapsulates transformation of actual Event/message content to/from format used for
92
+ transportation. Serializer is used by EventManager to dump Event content before sending it over Transport and
93
+ load content received from transport. Serializer interface includes only dump and load methods.
94
+
95
+ === Questions:
96
+ Should Event#fire be sync or async? That is, should we wait for all subscribers to return after calling #fire?
97
+ If it is async, something like "event queue" should exist...
98
+ Should Seriaizer be called by EventManager or Transport?
99
+ Who should know what format is appropriate for a given Routing? Transport knows about physical routing
100
+ (exchange names, types), but EventManager knows about Event types and what content goes into what message.
101
+
102
+ == FEATURES/PROBLEMS:
103
+
104
+ This library is not mature enough for anything but experimental use...
105
+
106
+ == SYNOPSIS:
107
+
108
+ == REQUIREMENTS:
109
+
110
+ == INSTALL:
111
+
112
+ $ sudo gem install amqp-events
113
+
114
+ == LICENSE:
115
+ Copyright (c) 2010 Arvicco. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,24 @@
1
+ require 'pathname'
2
+ NAME = 'amqp-events'
3
+ BASE_PATH = Pathname.new(__FILE__).dirname
4
+ LIB_PATH = BASE_PATH + 'lib'
5
+ PKG_PATH = BASE_PATH + 'pkg'
6
+ DOC_PATH = BASE_PATH + 'rdoc'
7
+
8
+ $LOAD_PATH.unshift LIB_PATH.to_s
9
+ require 'version'
10
+
11
+ CLASS_NAME = AMQP::Events
12
+ VERSION = CLASS_NAME::VERSION
13
+
14
+ begin
15
+ require 'rake'
16
+ rescue LoadError
17
+ require 'rubygems'
18
+ gem 'rake', '~> 0.8.3.1'
19
+ require 'rake'
20
+ end
21
+
22
+ # Load rakefile tasks
23
+ Dir['tasks/*.rake'].sort.each { |file| load file }
24
+
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.2
data/bin/amqp-events ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'pathname'
4
+ require Pathname.new(__FILE__).dirname + '../lib/amqp-events'
5
+
6
+ # Put your code here
7
+
@@ -0,0 +1,9 @@
1
+ Feature: something something
2
+ In order to something something
3
+ A user something something
4
+ something something something
5
+
6
+ Scenario: something something
7
+ Given inspiration
8
+ When I create a sweet new gem
9
+ Then everyone should see how awesome I am
File without changes
@@ -0,0 +1,10 @@
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__) + '/../../lib')
2
+
3
+ require 'pathname'
4
+ require 'bundler'
5
+ Bundler.setup
6
+ Bundler.require :cucumber
7
+
8
+ require 'amqp-events'
9
+
10
+ BASE_PATH = Pathname.new(__FILE__).dirname + '../..'
@@ -0,0 +1,12 @@
1
+ module SystemHelper
2
+
3
+ def windows?
4
+ RUBY_PLATFORM =~ /mswin|windows|mingw/ || cygwin?
5
+ end
6
+
7
+ def cygwin?
8
+ RUBY_PLATFORM =~ /cygwin/
9
+ end
10
+ end
11
+
12
+ World(WinGui, SystemHelper)
@@ -0,0 +1,26 @@
1
+ require 'version'
2
+
3
+ module AMQP
4
+ module Events
5
+ require "bundler/setup"
6
+ # Bundler.require :group
7
+
8
+ # Requires ruby source file(s). Accepts either single filename/glob or Array of filenames/globs.
9
+ # Accepts following options:
10
+ # :*file*:: Lib(s) required relative to this file - defaults to __FILE__
11
+ # :*dir*:: Required lib(s) located under this dir name - defaults to gem name
12
+ #
13
+ def self.require_libs(libs, opts={})
14
+ file = Pathname.new(opts[:file] || __FILE__)
15
+ [libs].flatten.each do |lib|
16
+ name = file.dirname + (opts[:dir] || file.basename('.*')) + lib.gsub(/(?<!.rb)$/, '.rb')
17
+ Pathname.glob(name.to_s).sort.each { |rb| require rb }
18
+ end
19
+ end
20
+ end
21
+ end
22
+
23
+ # Require all ruby source files located under directory lib/amqp-events
24
+ # If you need files in specific order, you should specify it here before the glob
25
+ AMQP::Events.require_libs %W[**/*]
26
+
@@ -0,0 +1,157 @@
1
+ module AMQP
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
+
87
+ def == (other)
88
+ case other
89
+ when Event
90
+ super
91
+ when nil
92
+ @subscribers.empty?
93
+ else
94
+ false
95
+ end
96
+ end
97
+
98
+ private
99
+ def generate_subscriber_name(subscriber)
100
+ "#{subscriber.respond_to?(:name) ? subscriber.name : 'subscriber'}-#{UUID.generate}".to_sym
101
+ end
102
+ end
103
+
104
+ def events
105
+ @events ||= self.class.instance_events.inject({}) { |hash, name| hash[name]=Event.new(name); hash }
106
+ end
107
+
108
+ def event(name)
109
+ sym_name = name.to_sym
110
+ self.class.event(sym_name)
111
+ events[sym_name] ||= Event.new(sym_name)
112
+ end
113
+
114
+ # Once included into a class/module, gives this module .event macros for declaring events
115
+ def self.included(host)
116
+
117
+ host.instance_exec do
118
+ def instance_events
119
+ @instance_events ||= []
120
+ end
121
+
122
+ def event (name)
123
+
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
135
+ else
136
+ events[name]
137
+ end
138
+ else
139
+ events[name].fire(*args)
140
+ end
141
+ end
142
+
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"
149
+ end
150
+ end
151
+ end
152
+ end
153
+
154
+ end
155
+
156
+ end
157
+ end
data/lib/version.rb ADDED
@@ -0,0 +1,10 @@
1
+ require 'pathname'
2
+
3
+ module AMQP
4
+ module Events
5
+
6
+ VERSION_FILE = Pathname.new(__FILE__).dirname + '../VERSION' # :nodoc:
7
+ VERSION = VERSION_FILE.exist? ? VERSION_FILE.read.strip : nil
8
+
9
+ end
10
+ end
@@ -0,0 +1,214 @@
1
+ require 'spec_helper'
2
+
3
+ class EmptyTestClass
4
+ include AMQP::Events
5
+ end
6
+
7
+ class TestClassWithEvents
8
+ include AMQP::Events
9
+
10
+ event :Bar
11
+ event :Baz
12
+ end
13
+
14
+ shared_examples_for 'evented class' do
15
+ specify { should respond_to :instance_events }
16
+ its(:instance_events) { should be_an Array }
17
+ end
18
+
19
+ shared_examples_for 'evented object' do
20
+ specify { should respond_to :events }
21
+ its(:events) { should be_a Hash }
22
+ end
23
+
24
+ def subscribers_to_be_called(num)
25
+ @counter = 0
26
+
27
+ subject.Bar.subscribers.should have(num).subscribers
28
+ subject.Bar.listeners.should have(num).listeners
29
+
30
+ subject.Bar.fire "data" # fire Event, sending "data" to subscribers
31
+ @counter.should == num
32
+ end
33
+
34
+
35
+ module EventsTest
36
+ describe EmptyTestClass, ' that includes AMQPEvents::Events and is just instantiated' do
37
+ subject { EmptyTestClass }
38
+
39
+ it_should_behave_like 'evented class'
40
+ its(:instance_events) { should be_empty }
41
+
42
+ end
43
+
44
+ describe EmptyTestClass, ' when instantiated' do
45
+ subject { EmptyTestClass.new }
46
+
47
+ it_should_behave_like 'evented object'
48
+ its(:events) { should be_empty }
49
+
50
+ context 'creating new (class-wide) Events' do
51
+ it 'should create events on instance' do
52
+ subject.event :Blah
53
+ subject.events.should include :Blah
54
+ end
55
+ end
56
+ end
57
+
58
+ describe TestClassWithEvents, ' (predefined) that includes AMQPEvents::Events' do
59
+ subject { TestClassWithEvents }
60
+
61
+ it_should_behave_like 'evented class'
62
+ its(:instance_events) { should include :Bar }
63
+ its(:instance_events) { should include :Baz }
64
+
65
+ it 'should create new events' do
66
+ subject.event :Foo
67
+ subject.instance_events.should include :Foo
68
+ end
69
+ end
70
+
71
+ describe TestClassWithEvents, ' when instantiated' do
72
+
73
+ it_should_behave_like 'evented object'
74
+ its(:events) { should have_key :Bar }
75
+ its(:events) { should have_key :Baz }
76
+
77
+ context 'creating new (class-wide) Events' do
78
+ it 'should create events on instance' do
79
+ subject.event :Blah
80
+ subject.events.should include :Blah
81
+ # object effectively defines new Event for all similar instances... Should it be allowed?
82
+ subject.class.instance_events.should include :Blah
83
+ end
84
+ end
85
+
86
+ context "#subscribe to object's Event" do
87
+
88
+ it 'allows anyone to add block subscribers/listeners (multiple syntax)' do
89
+ subject.events[:Bar].subscribe(:bar1) { |*args| args.should == ["data"]; @counter += 1 }
90
+ subject.Bar.subscribe(:bar2) { |*args| args.should == ["data"]; @counter += 1 }
91
+ subject.Bar.listen(:bar3) { |*args| args.should == ["data"]; @counter += 1 }
92
+
93
+ subscribers_to_be_called 3
94
+ end
95
+
96
+ it 'allows anyone to add method subscribers/listeners (multiple syntax)' do
97
+ def self.subscriber_method(*args)
98
+ args.should == ["data"]
99
+ @counter += 1
100
+ end
101
+
102
+ subject.events[:Bar].subscribe(:bar1, method(:subscriber_method))
103
+ subject.Bar.subscribe(:bar2, method(:subscriber_method))
104
+ subject.Bar.listen(:bar3, method(:subscriber_method))
105
+ subject.Bar.listen(method(:subscriber_method))
106
+ subject.Bar += method(:subscriber_method)
107
+
108
+ subscribers_to_be_called 5
109
+ end
110
+
111
+ it 'allows anyone to add proc subscribers/listeners (multiple syntax)' do
112
+ subscriber_proc = proc do |*args|
113
+ args.should == ["data"]
114
+ @counter += 1
115
+ end
116
+
117
+ subject.events[:Bar].subscribe(:bar1, subscriber_proc)
118
+ subject.Bar.subscribe(:bar2, subscriber_proc)
119
+ subject.Bar.subscribe(:bar3, &subscriber_proc)
120
+ subject.Bar.subscribe subscriber_proc
121
+ subject.Bar.subscribe &subscriber_proc
122
+ subject.Bar.listen subscriber_proc
123
+ subject.Bar += subscriber_proc
124
+
125
+ subscribers_to_be_called 7
126
+ end
127
+
128
+ it "allows you to mix subscriber types for one Event" do
129
+ def self.subscriber_method(*args)
130
+ args.should == ["data"]
131
+ @counter += 1
132
+ end
133
+
134
+ subscriber_proc = proc do |*args|
135
+ args.should == ["data"]
136
+ @counter += 1
137
+ end
138
+
139
+ subject.Bar.subscribe { |*args| args.should == ["data"]; @counter += 1 }
140
+ subject.Bar += method :subscriber_method
141
+ subject.Bar += subscriber_proc
142
+
143
+ subscribers_to_be_called 3
144
+ end
145
+
146
+ it "raises exception if the given handler is not callable" do
147
+
148
+ [:subscriber_symbol, 1, [1, 2, 3], {me: 2}].each do |args|
149
+ expect { subject.Bar.subscribe(args) }.
150
+ to raise_error /Handler .* does not respond to #call/
151
+ expect { subject.Bar.subscribe(:good_name, args) }.
152
+ to raise_error /Handler .* does not respond to #call/
153
+ subscribers_to_be_called 0
154
+ end
155
+ end
156
+
157
+ it "raises exception when adding handler with duplicate name" do
158
+ subscriber_proc = proc do |*args|
159
+ args.should == ["data"]
160
+ @counter += 1
161
+ end
162
+
163
+ subject.Bar.listen(:bar1) { |*args| args.should == ["data"]; @counter += 1 }
164
+
165
+ expect { subject.Bar.listen(:bar1) { |*args| args.should == ["data"]; @counter += 1 } }.
166
+ to raise_error /Handler name bar1 already in use/
167
+ expect { subject.Bar.listen(:bar1, subscriber_proc) }.
168
+ to raise_error /Handler name bar1 already in use/
169
+ subscribers_to_be_called 1
170
+ end
171
+ end #subscribe
172
+
173
+ context "#unsubscribe from object's Event" do
174
+ it "allows you to unsubscribe from Events by name" do
175
+ def self.subscriber_method(*args)
176
+ args.should == ["data"]
177
+ @counter += 1
178
+ end
179
+
180
+ subscriber_proc = proc do |*args|
181
+ args.should == ["data"]
182
+ @counter += 1
183
+ end
184
+
185
+ subject.Bar.subscribe(:bar1) { |*args| args.should == ["data"]; @counter += 1 }
186
+ subject.Bar.subscribe(:bar2, method(:subscriber_method))
187
+ subject.Bar.subscribe(:bar3, subscriber_proc)
188
+
189
+ subject.Bar.unsubscribe(:bar1)
190
+ subject.Bar.unsubscribe(:bar2)
191
+ subject.Bar.unsubscribe(:bar3)
192
+
193
+ subscribers_to_be_called 0
194
+ end
195
+
196
+ it "raises exception if the name is unknown or wrong" do
197
+ subscriber_proc = proc do |*args|
198
+ args.should == ["data"]
199
+ @counter += 1
200
+ end
201
+
202
+ subject.Bar.subscribe(subscriber_proc)
203
+
204
+ expect { subject.Bar.unsubscribe(subscriber_proc) }.
205
+ to raise_error /Unable to unsubscribe handler/
206
+ expect { subject.Bar.unsubscribe('I-dont-know') }.
207
+ to raise_error /Unable to unsubscribe handler I-dont-know/
208
+
209
+ subscribers_to_be_called 1
210
+ end
211
+ end #unsubscribe
212
+ end # TestClassWithEvents, ' when instantiated'
213
+ end # module EventsTest
214
+
data/spec/cs_spec.rb ADDED
@@ -0,0 +1,97 @@
1
+ #require 'spec_helper'
2
+ require_relative '../lib/amqp-events/events'
3
+ module EventsTest
4
+ describe AMQP::Events, ' when running Second Change Event Example' do
5
+ before { @clock = SecondChangeEvent::Clock.new }
6
+ let(:messages) { [] }
7
+
8
+ it 'should replicate results of C# example' do
9
+ @n = 0
10
+ $stdout.should_receive(:puts) { |msg| messages << msg; true }.at_least(10).times
11
+
12
+ # #// Create the display and tell it to subscribe to the clock just created
13
+ dc = SecondChangeEvent::DisplayClock.new
14
+ dc.Subscribe(@clock)
15
+
16
+ #// Create a Log object and tell it to subscribe to the clock
17
+ lc = SecondChangeEvent::LogClock.new
18
+ lc.Subscribe(@clock)
19
+
20
+ #// Get the clock started
21
+ @clock.Run(1)
22
+
23
+ messages.count { |msg| msg =~/Current Time:/ }.should be_between 10, 11
24
+ messages.count { |msg| msg =~/Logging to file:/ }.should be_between 10, 11
25
+ messages.count { |msg| msg =~/Wow! Second passed!:/ }.should be_between 1, 2
26
+ end
27
+
28
+ end
29
+ end # module AMQPEventsTest
30
+
31
+ # This is a reproduction of "The Second Change Event Example" from:
32
+ # http://www.akadia.com/services/dotnet_delegates_and_events.html
33
+ # Now I need to change it into a test suite somehow...
34
+ module SecondChangeEvent
35
+ # /* ======================= Event Publisher =============================== */
36
+
37
+ # Our subject -- it is this class that other classes will observe. This class publishes one event:
38
+ # SecondChange. The observers subscribe to that event.
39
+ class Clock
40
+ include AMQP::Events
41
+
42
+ event :DeciSecondChange
43
+ event :SecondChange
44
+
45
+ # Set the clock running, it will raise an event for each new MILLI second!
46
+ # With timeout for testing
47
+ def Run(timeout=nil)
48
+ start = Time.now
49
+ while !timeout || timeout > Time.now - start do
50
+ time = Time.now
51
+
52
+ # If the (centi)second has changed, notify the subscribers
53
+ # MilliSecondChange(self, time) if time.usec/1000 != @millisec # Doesn't work, Windows timer is ~ 15 msec
54
+ DeciSecondChange(self, time) if time.usec/100_000 != @decisec
55
+ SecondChange(self, time) if time.sec != @sec
56
+
57
+ # Update the state
58
+ @decisec = time.usec/100_000
59
+ @sec = time.sec
60
+ end
61
+ end
62
+ end
63
+
64
+ # /* ======================= Event Subscribers =============================== */
65
+
66
+ # An observer. DisplayClock subscribes to the clock's events.
67
+ # The job of DisplayClock is to display the current time
68
+ class DisplayClock
69
+ # Given a clock, subscribe to its SecondChange event
70
+ def Subscribe(theClock)
71
+ # Calling SecondChange without parameters returns the Event object itself
72
+ theClock.DeciSecondChange += proc { |*args| TimeHasChanged(*args) } # subscribing with a proc
73
+ theClock.SecondChange.subscribe do |theClock, ti| # subscribing with block
74
+ puts "Wow! Second passed!: #{ti.hour}:#{ti.min}:#{ti.sec}"
75
+ end
76
+ end
77
+
78
+ #// The method that implements the delegated functionality
79
+ def TimeHasChanged(theClock, ti)
80
+ puts "Current Time: #{ti.hour}:#{ti.min}:#{ti.sec}"
81
+ end
82
+ end
83
+
84
+ # A second subscriber whose job is to write to a file
85
+ class LogClock
86
+
87
+ def Subscribe(theClock)
88
+ theClock.DeciSecondChange += method :WriteLogEntry # subscribing with a Method name
89
+ end
90
+
91
+ # This method should write to a file, but we just write to the console to see the effect
92
+ def WriteLogEntry(theClock, ti)
93
+ puts "Logging to file: #{ti.hour}:#{ti.min}:#{ti.sec}"
94
+ # Code that logs to file goes here...
95
+ end
96
+ end
97
+ end
data/spec/spec.opts ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format nested
@@ -0,0 +1,23 @@
1
+ require 'bundler'
2
+ Bundler.setup
3
+ Bundler.require :test
4
+
5
+ require 'amqp-events'
6
+ require 'pathname'
7
+
8
+ BASE_PATH = Pathname.new(__FILE__).dirname + '..'
9
+
10
+ Spec::Runner.configure do |config|
11
+ # == Mock Framework
12
+ #
13
+ # RSpec uses it's own mocking framework by default. If you prefer to
14
+ # use mocha, flexmock or RR, uncomment the appropriate line:
15
+ #
16
+ # config.mock_with :mocha
17
+ # config.mock_with :flexmock
18
+ # config.mock_with :rr
19
+ end
20
+
21
+ module EventsTest
22
+
23
+ end # module EventsTest
data/tasks/common.rake ADDED
@@ -0,0 +1,18 @@
1
+ #task :default => 'test:run'
2
+ #task 'gem:release' => 'test:run'
3
+
4
+ task :notes do
5
+ puts 'Output annotations (TBD)'
6
+ end
7
+
8
+ #Bundler not ready for prime time just yet
9
+ #desc 'Bundle dependencies'
10
+ #task :bundle do
11
+ # output = `bundle check 2>&1`
12
+ #
13
+ # unless $?.to_i == 0
14
+ # puts output
15
+ # system "bundle install"
16
+ # puts
17
+ # end
18
+ #end
data/tasks/doc.rake ADDED
@@ -0,0 +1,14 @@
1
+ desc 'Alias to doc:rdoc'
2
+ task :doc => 'doc:rdoc'
3
+
4
+ namespace :doc do
5
+ require 'rake/rdoctask'
6
+ Rake::RDocTask.new do |rdoc|
7
+ # Rake::RDocTask.new(:rdoc => "rdoc", :clobber_rdoc => "clobber", :rerdoc => "rerdoc") do |rdoc|
8
+ rdoc.rdoc_dir = DOC_PATH.basename.to_s
9
+ rdoc.title = "#{NAME} #{VERSION} Documentation"
10
+ rdoc.main = "README.doc"
11
+ rdoc.rdoc_files.include('README*')
12
+ rdoc.rdoc_files.include('lib/**/*.rb')
13
+ end
14
+ end
data/tasks/gem.rake ADDED
@@ -0,0 +1,40 @@
1
+ desc "Alias to gem:release"
2
+ task :release => 'gem:release'
3
+
4
+ desc "Alias to gem:install"
5
+ task :install => 'gem:install'
6
+
7
+ desc "Alias to gem:build"
8
+ task :gem => 'gem:build'
9
+
10
+ namespace :gem do
11
+ gem_file = "#{NAME}-#{VERSION}.gem"
12
+
13
+ desc "(Re-)Build gem"
14
+ task :build do
15
+ puts "Remove existing gem package"
16
+ rm_rf PKG_PATH
17
+ puts "Build new gem package"
18
+ system "gem build #{NAME}.gemspec"
19
+ puts "Move built gem to package dir"
20
+ mkdir_p PKG_PATH
21
+ mv gem_file, PKG_PATH
22
+ end
23
+
24
+ desc "Cleanup already installed gem(s)"
25
+ task :cleanup do
26
+ puts "Cleaning up installed gem(s)"
27
+ system "gem cleanup #{NAME}"
28
+ end
29
+
30
+ desc "Build and install gem"
31
+ task :install => :build do
32
+ system "gem install #{PKG_PATH}/#{gem_file}"
33
+ end
34
+
35
+ desc "Build and push gem to Gemcutter"
36
+ task :release => [:build, 'git:tag'] do
37
+ puts "Pushing gem to Gemcutter"
38
+ system "gem push #{PKG_PATH}/#{gem_file}"
39
+ end
40
+ end
data/tasks/git.rake ADDED
@@ -0,0 +1,34 @@
1
+ desc "Alias to git:commit"
2
+ task :git => 'git:commit'
3
+
4
+ namespace :git do
5
+
6
+ desc "Stage and commit your work [with message]"
7
+ task :commit, [:message] do |t, args|
8
+ puts "Staging new (unversioned) files"
9
+ system "git add --all"
10
+ if args.message
11
+ puts "Committing with message: #{args.message}"
12
+ system %Q[git commit -a -m "#{args.message}" --author arvicco]
13
+ else
14
+ puts "Committing"
15
+ system %Q[git commit -a -m "No message" --author arvicco]
16
+ end
17
+ end
18
+
19
+ desc "Push local changes to Github"
20
+ task :push => :commit do
21
+ puts "Pushing local changes to remote"
22
+ system "git push"
23
+ end
24
+
25
+ desc "Create (release) tag on Github"
26
+ task :tag => :push do
27
+ tag = VERSION
28
+ puts "Creating git tag: #{tag}"
29
+ system %Q{git tag -a -m "Release tag #{tag}" #{tag}}
30
+ puts "Pushing #{tag} to remote"
31
+ system "git push origin #{tag}"
32
+ end
33
+
34
+ end
data/tasks/spec.rake ADDED
@@ -0,0 +1,19 @@
1
+ desc 'Alias to spec:spec'
2
+ task :spec => 'spec:spec'
3
+
4
+ namespace :spec do
5
+ require 'spec/rake/spectask'
6
+
7
+ desc "Run all specs"
8
+ Spec::Rake::SpecTask.new(:spec) do |t|
9
+ t.spec_opts = ['--options', %Q{"#{BASE_PATH}/spec/spec.opts"}]
10
+ t.spec_files = FileList['spec/**/*_spec.rb']
11
+ end
12
+
13
+ desc "Run specs with RCov"
14
+ Spec::Rake::SpecTask.new(:rcov) do |t|
15
+ t.spec_files = FileList['spec/**/*_spec.rb']
16
+ t.rcov = true
17
+ t.rcov_opts = ['--exclude', 'spec']
18
+ end
19
+ end
@@ -0,0 +1,71 @@
1
+ class Version
2
+ attr_accessor :major, :minor, :patch, :build
3
+
4
+ def initialize(version_string)
5
+ raise "Invalid version #{version_string}" unless version_string =~ /^(\d+)\.(\d+)\.(\d+)(?:\.(.*?))?$/
6
+ @major = $1.to_i
7
+ @minor = $2.to_i
8
+ @patch = $3.to_i
9
+ @build = $4
10
+ end
11
+
12
+ def bump_major(x)
13
+ @major += x.to_i
14
+ @minor = 0
15
+ @patch = 0
16
+ @build = nil
17
+ end
18
+
19
+ def bump_minor(x)
20
+ @minor += x.to_i
21
+ @patch = 0
22
+ @build = nil
23
+ end
24
+
25
+ def bump_patch(x)
26
+ @patch += x.to_i
27
+ @build = nil
28
+ end
29
+
30
+ def update(major, minor, patch, build=nil)
31
+ @major = major
32
+ @minor = minor
33
+ @patch = patch
34
+ @build = build
35
+ end
36
+
37
+ def write(desc = nil)
38
+ CLASS_NAME::VERSION_FILE.open('w') {|file| file.puts to_s }
39
+ (BASE_PATH + 'HISTORY').open('a') do |file|
40
+ file.puts "\n== #{to_s} / #{Time.now.strftime '%Y-%m-%d'}\n"
41
+ file.puts "\n* #{desc}\n" if desc
42
+ end
43
+ end
44
+
45
+ def to_s
46
+ [major, minor, patch, build].compact.join('.')
47
+ end
48
+ end
49
+
50
+ desc 'Set version: [x.y.z] - explicitly, [1/10/100] - bump major/minor/patch, [.build] - build'
51
+ task :version, [:command, :desc] do |t, args|
52
+ version = Version.new(VERSION)
53
+ case args.command
54
+ when /^(\d+)\.(\d+)\.(\d+)(?:\.(.*?))?$/ # Set version explicitly
55
+ version.update($1, $2, $3, $4)
56
+ when /^\.(.*?)$/ # Set build
57
+ version.build = $1
58
+ when /^(\d{1})$/ # Bump patch
59
+ version.bump_patch $1
60
+ when /^(\d{1})0$/ # Bump minor
61
+ version.bump_minor $1
62
+ when /^(\d{1})00$/ # Bump major
63
+ version.bump_major $1
64
+ else # Unknown command, just display VERSION
65
+ puts "#{NAME} #{version}"
66
+ next
67
+ end
68
+
69
+ puts "Writing version #{version} to VERSION file"
70
+ version.write args.desc
71
+ end
metadata ADDED
@@ -0,0 +1,145 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: amqp-events
3
+ version: !ruby/object:Gem::Version
4
+ hash: 27
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 2
10
+ version: 0.0.2
11
+ platform: ruby
12
+ authors:
13
+ - arvicco
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2010-10-25 00:00:00 +04:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: rspec
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 13
30
+ segments:
31
+ - 1
32
+ - 2
33
+ - 9
34
+ version: 1.2.9
35
+ type: :development
36
+ version_requirements: *id001
37
+ - !ruby/object:Gem::Dependency
38
+ name: cucumber
39
+ prerelease: false
40
+ requirement: &id002 !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ hash: 3
46
+ segments:
47
+ - 0
48
+ version: "0"
49
+ type: :development
50
+ version_requirements: *id002
51
+ - !ruby/object:Gem::Dependency
52
+ name: bundler
53
+ prerelease: false
54
+ requirement: &id003 !ruby/object:Gem::Requirement
55
+ none: false
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ hash: 13
60
+ segments:
61
+ - 1
62
+ - 2
63
+ - 9
64
+ version: 1.2.9
65
+ type: :runtime
66
+ version_requirements: *id003
67
+ description: Distributed Events/RPC system using AMQP as a transport (pre-alpha)
68
+ email: arvitallian@gmail.com
69
+ executables:
70
+ - amqp-events
71
+ extensions: []
72
+
73
+ extra_rdoc_files:
74
+ - LICENSE
75
+ - HISTORY
76
+ - README.rdoc
77
+ files:
78
+ - bin/amqp-events
79
+ - lib/amqp-events/events.rb
80
+ - lib/amqp-events.rb
81
+ - lib/version.rb
82
+ - spec/amqp-events_spec.rb
83
+ - spec/cs_spec.rb
84
+ - spec/spec.opts
85
+ - spec/spec_helper.rb
86
+ - features/amqp-events.feature
87
+ - features/step_definitions/amqp-events_steps.rb
88
+ - features/support/env.rb
89
+ - features/support/world.rb
90
+ - tasks/common.rake
91
+ - tasks/doc.rake
92
+ - tasks/gem.rake
93
+ - tasks/git.rake
94
+ - tasks/spec.rake
95
+ - tasks/version.rake
96
+ - Rakefile
97
+ - README.rdoc
98
+ - LICENSE
99
+ - VERSION
100
+ - HISTORY
101
+ - .gitignore
102
+ has_rdoc: true
103
+ homepage: http://github.com/arvicco/amqp-events
104
+ licenses: []
105
+
106
+ post_install_message:
107
+ rdoc_options:
108
+ - --charset
109
+ - UTF-8
110
+ - --main
111
+ - README.rdoc
112
+ - --title
113
+ - amqp-events
114
+ require_paths:
115
+ - lib
116
+ required_ruby_version: !ruby/object:Gem::Requirement
117
+ none: false
118
+ requirements:
119
+ - - ">="
120
+ - !ruby/object:Gem::Version
121
+ hash: 3
122
+ segments:
123
+ - 0
124
+ version: "0"
125
+ required_rubygems_version: !ruby/object:Gem::Requirement
126
+ none: false
127
+ requirements:
128
+ - - ">="
129
+ - !ruby/object:Gem::Version
130
+ hash: 3
131
+ segments:
132
+ - 0
133
+ version: "0"
134
+ requirements: []
135
+
136
+ rubyforge_project:
137
+ rubygems_version: 1.3.7
138
+ signing_key:
139
+ specification_version: 3
140
+ summary: Distributed Events/RPC system using AMQP as a transport.
141
+ test_files:
142
+ - spec/amqp-events_spec.rb
143
+ - spec/cs_spec.rb
144
+ - spec/spec.opts
145
+ - spec/spec_helper.rb