amqp-events 0.0.2

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