amqp-events 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +24 -0
- data/HISTORY +11 -0
- data/LICENSE +20 -0
- data/README.rdoc +115 -0
- data/Rakefile +24 -0
- data/VERSION +1 -0
- data/bin/amqp-events +7 -0
- data/features/amqp-events.feature +9 -0
- data/features/step_definitions/amqp-events_steps.rb +0 -0
- data/features/support/env.rb +10 -0
- data/features/support/world.rb +12 -0
- data/lib/amqp-events.rb +26 -0
- data/lib/amqp-events/events.rb +157 -0
- data/lib/version.rb +10 -0
- data/spec/amqp-events_spec.rb +214 -0
- data/spec/cs_spec.rb +97 -0
- data/spec/spec.opts +2 -0
- data/spec/spec_helper.rb +23 -0
- data/tasks/common.rake +18 -0
- data/tasks/doc.rake +14 -0
- data/tasks/gem.rake +40 -0
- data/tasks/git.rake +34 -0
- data/tasks/spec.rake +19 -0
- data/tasks/version.rake +71 -0
- metadata +145 -0
data/.gitignore
ADDED
data/HISTORY
ADDED
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
File without changes
|
data/lib/amqp-events.rb
ADDED
@@ -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,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
data/spec/spec_helper.rb
ADDED
@@ -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
|
data/tasks/version.rake
ADDED
@@ -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
|