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