untied-publisher 0.0.5 → 0.0.6
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/.travis.yml +2 -0
- data/CHANGELOG.mkd +6 -0
- data/README.md +1 -1
- data/lib/untied-publisher.rb +42 -21
- data/lib/untied-publisher/amqp.rb +16 -0
- data/lib/untied-publisher/amqp/producer.rb +44 -0
- data/lib/untied-publisher/base.rb +12 -0
- data/lib/untied-publisher/base_producer.rb +62 -0
- data/lib/untied-publisher/bunny.rb +15 -0
- data/lib/untied-publisher/bunny/producer.rb +45 -0
- data/lib/untied-publisher/config.rb +8 -17
- data/lib/untied-publisher/default_doorkeeper.rb +8 -0
- data/lib/untied-publisher/event.rb +18 -2
- data/lib/untied-publisher/event_representer.rb +10 -6
- data/lib/untied-publisher/observer.rb +4 -7
- data/lib/untied-publisher/railtie.rb +2 -4
- data/lib/untied-publisher/version.rb +1 -1
- data/spec/amqp/amqp_producer_spec.rb +66 -0
- data/spec/base_producer_spec.rb +31 -0
- data/spec/bunny/bunny_producer_spec.rb +83 -0
- data/spec/event_representer_spec.rb +43 -0
- data/spec/{publisher_observer_spec.rb → observer_spec.rb} +12 -6
- data/spec/support/setup_ar_and_schema.rb +1 -0
- data/untied-publisher.gemspec +1 -0
- metadata +40 -10
- data/lib/untied-publisher/producer.rb +0 -73
- data/spec/producer_spec.rb +0 -61
data/.travis.yml
CHANGED
data/CHANGELOG.mkd
CHANGED
data/README.md
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
This is the publisher part of Untied. Untied is an observer pattern implementation to distributed systems.
|
4
4
|
|
5
|
-
For usage information, please visit the [Untied](http://github.com
|
5
|
+
For usage information, please visit the [Untied](http://github.com/redu/untied) page.
|
6
6
|
|
7
7
|
[](https://travis-ci.org/redu/untied-publisher)
|
8
8
|
|
data/lib/untied-publisher.rb
CHANGED
@@ -2,37 +2,58 @@ require "untied-publisher/version"
|
|
2
2
|
|
3
3
|
require 'rubygems'
|
4
4
|
require 'bundler/setup'
|
5
|
-
|
5
|
+
|
6
|
+
require 'untied-publisher/event_representer'
|
7
|
+
require 'untied-publisher/event'
|
8
|
+
require 'untied-publisher/doorkeeper'
|
9
|
+
require 'untied-publisher/default_doorkeeper'
|
10
|
+
require 'untied-publisher/config'
|
11
|
+
require 'untied-publisher/observer'
|
12
|
+
require 'untied-publisher/base_producer'
|
13
|
+
require 'untied-publisher/amqp'
|
14
|
+
require 'untied-publisher/bunny'
|
15
|
+
require 'untied-publisher/base'
|
6
16
|
|
7
17
|
module Untied
|
8
18
|
module Publisher
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
19
|
+
# Configures untied-publisher.
|
20
|
+
def self.configure(&block)
|
21
|
+
yield(config) if block_given?
|
22
|
+
if config.deliver_messages
|
23
|
+
adapter.start
|
24
|
+
else
|
25
|
+
config.adapter = :Base
|
26
|
+
adapter.start
|
14
27
|
end
|
15
28
|
end
|
16
29
|
|
17
|
-
def self.
|
18
|
-
@
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
30
|
+
def self.config
|
31
|
+
@config ||= Config.new
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.adapter
|
35
|
+
producer_booter = "Untied::Publisher::#{self.config.adapter}"
|
36
|
+
|
37
|
+
@adapter ||= begin
|
38
|
+
begin
|
39
|
+
constantize(producer_booter)
|
40
|
+
rescue NameError
|
41
|
+
config.logger.info "#{producer_booter} is not defined. Falling back " +\
|
42
|
+
"to Untied::Publisher::Bunny"
|
43
|
+
Untied::Publisher::Bunny
|
23
44
|
end
|
24
|
-
|
25
|
-
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Transforms string into constant
|
49
|
+
def self.constantize(class_name)
|
50
|
+
unless /\A(?:::)?([A-Z]\w*(?:::[A-Z]\w*)*)\z/ =~ class_name
|
51
|
+
raise NameError, "#{class_name.inspect} is not a valid constant name!"
|
26
52
|
end
|
53
|
+
|
54
|
+
Object.module_eval("::#{$1}", __FILE__, __LINE__)
|
27
55
|
end
|
28
56
|
end
|
29
57
|
end
|
30
58
|
|
31
|
-
|
32
|
-
require 'untied-publisher/event_representer'
|
33
|
-
require 'untied-publisher/event'
|
34
|
-
require 'untied-publisher/config'
|
35
|
-
require 'untied-publisher/doorkeeper'
|
36
|
-
require 'untied-publisher/observer'
|
37
|
-
require 'untied-publisher/producer'
|
38
59
|
require 'untied-publisher/railtie' if defined?(Rails)
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'amqp/utilities/event_loop_helper'
|
2
|
+
require 'untied-publisher/amqp/producer'
|
3
|
+
|
4
|
+
module Untied
|
5
|
+
module Publisher
|
6
|
+
module AMQP
|
7
|
+
def self.start
|
8
|
+
config.channel ||= ::AMQP::Channel.new(::AMQP.connection)
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.producer
|
12
|
+
Untied::Publisher::AMQP::Producer
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
require 'amqp'
|
3
|
+
|
4
|
+
module Untied
|
5
|
+
module Publisher
|
6
|
+
module AMQP
|
7
|
+
class Producer < BaseProducer
|
8
|
+
# Encapsulates both the Channel and Exchange (AMQP).
|
9
|
+
|
10
|
+
def initialize(opts={})
|
11
|
+
super
|
12
|
+
check_em_reactor
|
13
|
+
if ::AMQP.channel || opts[:channel]
|
14
|
+
say "Using defined AMQP.channel"
|
15
|
+
@channel = ::AMQP.channel || opts[:channel]
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# Publish the given event.
|
20
|
+
# event: object which is going to be serialized and sent through the
|
21
|
+
# wire. It should respond to #to_json.
|
22
|
+
def safe_publish(e)
|
23
|
+
on_exchange do |exchange|
|
24
|
+
exchange.publish(e.to_json, :routing_key => @routing_key)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Creates a new exchange and yields it to the block passed when it's ready
|
29
|
+
def on_exchange(&block)
|
30
|
+
return unless @channel
|
31
|
+
@channel.topic('untied', :auto_delete => true, &block)
|
32
|
+
end
|
33
|
+
|
34
|
+
def check_em_reactor
|
35
|
+
if !defined?(EventMachine) || !EM.reactor_running?
|
36
|
+
raise "In order to use the producer you must be running inside an " + \
|
37
|
+
"eventmachine loop"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
|
3
|
+
module Untied
|
4
|
+
module Publisher
|
5
|
+
# Generic class to provide message publishing.
|
6
|
+
class BaseProducer
|
7
|
+
attr_reader :routing_key, :service_name, :deliver_messages
|
8
|
+
|
9
|
+
def initialize(opts={})
|
10
|
+
extract_options!(opts)
|
11
|
+
end
|
12
|
+
|
13
|
+
# Publish the given event.
|
14
|
+
# event: object which is going to be serialized and sent through the
|
15
|
+
# wire. It should respond to #to_json.
|
16
|
+
def publish(event)
|
17
|
+
if deliver_messages
|
18
|
+
safe_publish(event)
|
19
|
+
say "Publishing event with routing key #{routing_key}: #{event.to_json}"
|
20
|
+
else
|
21
|
+
say "Event produced but not delivered. If you want to deliver it " + \
|
22
|
+
"try to set Untied::Publisher.config.deliver_messages to true. " + \
|
23
|
+
"Event: #{event.to_json}"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def extract_options!(opts)
|
30
|
+
options = {
|
31
|
+
:service_name => Publisher.config.service_name,
|
32
|
+
:deliver_messages => Publisher.config.deliver_messages,
|
33
|
+
:channel => nil,
|
34
|
+
}.merge(opts)
|
35
|
+
options[:routing_key] = "untied.#{options[:service_name]}"
|
36
|
+
|
37
|
+
options.each do |k,v|
|
38
|
+
instance_variable_set(:"@#{k}", v)
|
39
|
+
end
|
40
|
+
|
41
|
+
unless options[:service_name]
|
42
|
+
msg = "you should inform service_name option or configure it " + \
|
43
|
+
"through Untied::Publisher.configure block"
|
44
|
+
raise ArgumentError.new(msg)
|
45
|
+
end
|
46
|
+
|
47
|
+
say "Producer intialized with options " + \
|
48
|
+
"#{options.inspect} and routing key #{routing_key}"
|
49
|
+
|
50
|
+
if !deliver_messages
|
51
|
+
say "Channel was not setted up because message delivering is disabled."
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
protected
|
56
|
+
|
57
|
+
def say(msg)
|
58
|
+
Publisher.config.logger.info "#{self.class.to_s}: #{msg}"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'bunny'
|
2
|
+
|
3
|
+
module Untied
|
4
|
+
module Publisher
|
5
|
+
module Bunny
|
6
|
+
class Producer < BaseProducer
|
7
|
+
def initialize(opts={})
|
8
|
+
super
|
9
|
+
|
10
|
+
begin
|
11
|
+
connection.start
|
12
|
+
rescue ::Bunny::TCPConnectionFailed => e
|
13
|
+
say "Can't connect to RabbitMQ: #{e.message}"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# Publish the given event.
|
18
|
+
# event: object which is going to be serialized and sent through the
|
19
|
+
# wire. It should respond to #to_json.
|
20
|
+
def safe_publish(event)
|
21
|
+
if connection.status == :open
|
22
|
+
exchange.publish(event.to_json, :routing_key => routing_key)
|
23
|
+
else
|
24
|
+
say "Event not sent. Connection status is #{connection.status}: " + \
|
25
|
+
"#{event.to_json}"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
protected
|
30
|
+
|
31
|
+
def connection
|
32
|
+
@connection ||= ::Bunny.new
|
33
|
+
end
|
34
|
+
|
35
|
+
def exchange
|
36
|
+
@exchange ||= channel.topic('untied', :auto_delete => true)
|
37
|
+
end
|
38
|
+
|
39
|
+
def channel
|
40
|
+
@channel ||= connection.create_channel
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -4,28 +4,19 @@ require 'logger'
|
|
4
4
|
|
5
5
|
module Untied
|
6
6
|
module Publisher
|
7
|
-
def self.configure(&block)
|
8
|
-
yield(config) if block_given?
|
9
|
-
if config.deliver_messages
|
10
|
-
Untied::Publisher.start
|
11
|
-
EventMachine.next_tick do
|
12
|
-
config.channel ||= AMQP::Channel.new(AMQP.connection)
|
13
|
-
end
|
14
|
-
end
|
15
|
-
end
|
16
|
-
|
17
|
-
def self.config
|
18
|
-
@config ||= Config.new
|
19
|
-
end
|
20
|
-
|
21
7
|
class Config
|
22
8
|
include Configurable
|
23
9
|
|
10
|
+
# Logger used by untied. Default: STDOUT
|
24
11
|
config :logger, Logger.new(STDOUT)
|
12
|
+
# Deliver the messages to AMQP broker. Default: true
|
25
13
|
config :deliver_messages, true
|
26
|
-
|
27
|
-
config :
|
28
|
-
|
14
|
+
# An unique identifier to the publisher. Default: untied_publisher
|
15
|
+
config :service_name, 'untied_publisher'
|
16
|
+
# Doorkeeper class name. Default: DefaultDoorkeeper
|
17
|
+
config :doorkeeper, Untied::Publisher::DefaultDoorkeeper
|
18
|
+
# RabbitMQ adapter
|
19
|
+
config :adapter, :Bunny
|
29
20
|
end
|
30
21
|
end
|
31
22
|
end
|
@@ -8,14 +8,30 @@ module Untied
|
|
8
8
|
@config = {
|
9
9
|
:name => "after_create",
|
10
10
|
:payload => nil,
|
11
|
-
:origin => nil
|
11
|
+
:origin => nil,
|
12
|
+
:payload_representer => nil
|
12
13
|
}.merge(attrs)
|
13
14
|
|
14
15
|
raise "You should inform the origin service" unless @config[:origin]
|
15
16
|
|
16
17
|
@name = @config.delete(:name)
|
17
|
-
@payload = @config.delete(:payload)
|
18
|
+
@payload = represent(@config.delete(:payload))
|
18
19
|
@origin = @config.delete(:origin)
|
20
|
+
|
21
|
+
self.extend(Publisher::EventRepresenter)
|
22
|
+
end
|
23
|
+
|
24
|
+
protected
|
25
|
+
|
26
|
+
# Extends payload with representer and returns the hash. In cases there is
|
27
|
+
# no representer, the raw payload is returned
|
28
|
+
def represent(payload)
|
29
|
+
if representer = @config[:payload_representer]
|
30
|
+
payload.extend(representer)
|
31
|
+
payload.to_hash
|
32
|
+
else
|
33
|
+
payload
|
34
|
+
end
|
19
35
|
end
|
20
36
|
end
|
21
37
|
end
|
@@ -3,13 +3,17 @@
|
|
3
3
|
require 'representable/json'
|
4
4
|
|
5
5
|
module Untied
|
6
|
-
module
|
7
|
-
|
6
|
+
module Publisher
|
7
|
+
module EventRepresenter
|
8
|
+
# Publisher::Event is extended with this module at runtime. It defines
|
9
|
+
# how the event will be serialized.
|
8
10
|
|
9
|
-
|
11
|
+
include Representable::JSON
|
12
|
+
self.representation_wrap = true
|
10
13
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
+
property :name
|
15
|
+
property :payload
|
16
|
+
property :origin
|
17
|
+
end
|
14
18
|
end
|
15
19
|
end
|
@@ -28,17 +28,14 @@ module Untied
|
|
28
28
|
protected
|
29
29
|
|
30
30
|
def produce_event(callback, model, options={})
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
e = Event.new(:name => callback,
|
35
|
-
:payload => model, :origin => Publisher.config.service_name)
|
36
|
-
e.extend(EventRepresenter)
|
31
|
+
e = Event.new(:name => callback, :payload => model,
|
32
|
+
:origin => Publisher.config.service_name,
|
33
|
+
:payload_representer => options[:represent_with])
|
37
34
|
producer.publish(e)
|
38
35
|
end
|
39
36
|
|
40
37
|
def producer
|
41
|
-
|
38
|
+
Publisher.adapter.producer.new
|
42
39
|
end
|
43
40
|
|
44
41
|
def publisher
|
@@ -8,10 +8,8 @@ module Untied
|
|
8
8
|
config.active_record.observers ||= []
|
9
9
|
ActiveRecord::Base.observers << Untied::Publisher::Observer
|
10
10
|
config.active_record.observers << Untied::Publisher::Observer
|
11
|
-
|
12
|
-
|
13
|
-
Untied::Publisher::Observer.instance
|
14
|
-
end
|
11
|
+
Publisher.config.logger.debug "Untied::Publisher: initializing observer"
|
12
|
+
Untied::Publisher::Observer.instance
|
15
13
|
end
|
16
14
|
|
17
15
|
config.to_prepare do
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
require 'spec_helper'
|
3
|
+
|
4
|
+
module Untied
|
5
|
+
module Publisher
|
6
|
+
module AMQP
|
7
|
+
describe Producer do
|
8
|
+
context "configuration" do
|
9
|
+
it "should use service name configured" do
|
10
|
+
mock_reactor_and_amqp do |c|
|
11
|
+
Producer.new.service_name.should == 'core'
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should initilize producer" do
|
17
|
+
mock_reactor_and_amqp do |c|
|
18
|
+
Producer.new.should be_a Producer
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should raise RuntimeError when trying to run without connection" do
|
23
|
+
expect {
|
24
|
+
Producer.new(:deliver_messages => true)
|
25
|
+
}.to raise_error
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should raise RuntimeError if EM is not running" do
|
29
|
+
mock_reactor_and_amqp do |c|
|
30
|
+
EM.stub("reactor_running?" => false)
|
31
|
+
expect {
|
32
|
+
Producer.new(:channel => c, :deliver_messages => true)
|
33
|
+
}.to raise_error
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
context "#publish" do
|
38
|
+
let(:event) do
|
39
|
+
Event.new(:name => "create", :payload => { :foo => 'bar' }, :origin => :core)
|
40
|
+
end
|
41
|
+
|
42
|
+
it "should call Channel#publish" do
|
43
|
+
mock_reactor_and_amqp do |channel|
|
44
|
+
producer = Producer.new(:channel => channel, :deliver_messages => true)
|
45
|
+
producer.should_receive(:safe_publish).with(event)
|
46
|
+
producer.publish(event)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def mock_reactor_and_amqp
|
52
|
+
# Do nothing when calling start
|
53
|
+
Untied.stub(:start).and_return(nil)
|
54
|
+
# Simulate reactor running
|
55
|
+
EM.stub(:reactor_running?).and_return(true)
|
56
|
+
|
57
|
+
exchange = double('Exchange')
|
58
|
+
channel = double('Channel')
|
59
|
+
channel.stub(:topic).and_return(exchange)
|
60
|
+
|
61
|
+
yield(channel)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Untied
|
4
|
+
module Publisher
|
5
|
+
describe BaseProducer do
|
6
|
+
context ".new" do
|
7
|
+
it "should raise exception when there is no service_name" do
|
8
|
+
Publisher.config.stub(:service_name).and_return(nil)
|
9
|
+
expect {
|
10
|
+
BaseProducer.new
|
11
|
+
}.to raise_error(ArgumentError, /you should inform service_name/)
|
12
|
+
end
|
13
|
+
|
14
|
+
%w(routing_key service_name deliver_messages).each do |attr|
|
15
|
+
it "should have #{attr} attr reader" do
|
16
|
+
BaseProducer.new.should respond_to(attr)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should log when message delivering is setted to false" do
|
21
|
+
Publisher.config.logger.should_receive(:info).
|
22
|
+
with(/Producer intialized with options/)
|
23
|
+
Publisher.config.logger.should_receive(:info).
|
24
|
+
with(/Channel was not/)
|
25
|
+
BaseProducer.new(:deliver_messages => false)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Untied
|
4
|
+
module Publisher
|
5
|
+
module Bunny
|
6
|
+
describe Producer do
|
7
|
+
context ".new" do
|
8
|
+
it "call super" do
|
9
|
+
Producer.should_receive(:new)
|
10
|
+
Producer.new
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
context "failing connection" do
|
15
|
+
let(:bunny) do
|
16
|
+
bunny = double('Bunny')
|
17
|
+
end
|
18
|
+
before do
|
19
|
+
bunny.stub(:start).
|
20
|
+
and_raise(::Bunny::TCPConnectionFailed.new(nil, "host", "port"))
|
21
|
+
|
22
|
+
Producer.any_instance.stub(:connection).and_return(bunny)
|
23
|
+
end
|
24
|
+
let(:logger) { Publisher.config.logger }
|
25
|
+
|
26
|
+
context ".new" do
|
27
|
+
|
28
|
+
it "should log when connection is refused" do
|
29
|
+
Producer.any_instance.stub(:connection).and_return(bunny)
|
30
|
+
expect { Producer.new }.to_not raise_error
|
31
|
+
end
|
32
|
+
|
33
|
+
it "should log properly" do
|
34
|
+
logger.should_receive(:info).with(/Channel was not setted up/)
|
35
|
+
logger.should_receive(:info).with(/Producer intialized/)
|
36
|
+
logger.should_receive(:info).with(/Can't connect to RabbitMQ/)
|
37
|
+
|
38
|
+
Producer.new
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
context "#publish" do
|
43
|
+
it "should log properly" do
|
44
|
+
subject = Producer.new
|
45
|
+
bunny.stub(:status).and_return(:closed)
|
46
|
+
logger.should_receive(:info).with(/Event not sent/)
|
47
|
+
subject.safe_publish({})
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
context "#publish" do
|
53
|
+
let(:connection) { ::Bunny.new }
|
54
|
+
let(:channel) { connection.create_channel }
|
55
|
+
let(:exchange) { channel.topic('untied', :auto_delete => true) }
|
56
|
+
let(:event) do
|
57
|
+
Event.new(:name => "create", :payload => { :foo => 'bar' },
|
58
|
+
:origin => :core)
|
59
|
+
end
|
60
|
+
before do
|
61
|
+
connection.start
|
62
|
+
Publisher.config.deliver_messages = true
|
63
|
+
end
|
64
|
+
after do
|
65
|
+
connection.close
|
66
|
+
Publisher.config.deliver_messages = false
|
67
|
+
end
|
68
|
+
|
69
|
+
it "should route the message correctly" do
|
70
|
+
queue = channel.queue("", :exclusive => true).
|
71
|
+
bind(exchange, :routing_key => "untied.core")
|
72
|
+
subject.publish(event)
|
73
|
+
|
74
|
+
sleep(0.5)
|
75
|
+
|
76
|
+
queue.message_count.should == 1
|
77
|
+
channel.close
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Untied
|
4
|
+
describe Event do
|
5
|
+
before(:all) do
|
6
|
+
module PostRepresenter
|
7
|
+
include Representable::JSON
|
8
|
+
|
9
|
+
property :title
|
10
|
+
end
|
11
|
+
end
|
12
|
+
let(:post) { Post.create(:title => "DIY") }
|
13
|
+
|
14
|
+
context "#to_json" do
|
15
|
+
it "should serialize according with EventRepresenter" do
|
16
|
+
event = Event.new(:name => "foo", :payload => {}, :origin => :bar)
|
17
|
+
JSON.parse(event.to_json).to_a.sort.should ==
|
18
|
+
{ "event" =>
|
19
|
+
{ "name" => "foo", "origin" => "bar", "payload" => { } } }.to_a.sort
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should represent payload" do
|
23
|
+
event = Event.new(:name => "foo", :payload => post, :origin => :bar,
|
24
|
+
:payload_representer => PostRepresenter)
|
25
|
+
|
26
|
+
JSON.parse(event.to_json).to_a.sort.should ==
|
27
|
+
{ "event" =>
|
28
|
+
{ "name" => "foo", "origin" => "bar",
|
29
|
+
"payload" => { "title" => "DIY" } } }.to_a.sort
|
30
|
+
end
|
31
|
+
|
32
|
+
it "should fallback to default to_json when there is no representer" do
|
33
|
+
person = User.create(:name => "Guila")
|
34
|
+
event = Event.new(:name => "foo", :payload => person, :origin => :bar)
|
35
|
+
|
36
|
+
JSON.parse(event.to_json).to_a.sort.should ==
|
37
|
+
{ "event" =>
|
38
|
+
{ "name" => "foo", "origin" => "bar",
|
39
|
+
"payload" => JSON.parse(person.to_json) } }.to_a.sort
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -1,16 +1,21 @@
|
|
1
1
|
# -*- encoding : utf-8 -*-
|
2
2
|
require 'spec_helper'
|
3
|
+
require 'representable/json'
|
3
4
|
|
4
5
|
module Untied
|
5
6
|
module Publisher
|
6
7
|
describe Observer do
|
8
|
+
before(:all) do
|
9
|
+
module UserRepresenter
|
10
|
+
include Representable::JSON
|
11
|
+
property :name
|
12
|
+
end
|
13
|
+
end
|
7
14
|
before do
|
8
15
|
class ::FooDoorkeeper
|
9
16
|
include Untied::Publisher::Doorkeeper
|
10
17
|
end
|
11
18
|
Untied::Publisher.config.doorkeeper = ::FooDoorkeeper
|
12
|
-
module UserRepresenter
|
13
|
-
end
|
14
19
|
end
|
15
20
|
let(:doorkeeper) { ::FooDoorkeeper.new }
|
16
21
|
|
@@ -32,11 +37,12 @@ module Untied
|
|
32
37
|
Observer.instance.send(:after_create, user)
|
33
38
|
end
|
34
39
|
|
35
|
-
context "passing :
|
36
|
-
it "should
|
37
|
-
|
40
|
+
context "passing :represent_with" do
|
41
|
+
it "should create a new Event with :payload_representer option" do
|
42
|
+
Event.should_receive(:new).
|
43
|
+
with(hash_including(:payload_representer))
|
38
44
|
Observer.instance.
|
39
|
-
send(:after_create, user, :
|
45
|
+
send(:after_create, user, :represent_with => UserRepresenter)
|
40
46
|
end
|
41
47
|
end
|
42
48
|
end
|
data/untied-publisher.gemspec
CHANGED
@@ -26,6 +26,7 @@ Gem::Specification.new do |gem|
|
|
26
26
|
gem.add_runtime_dependency "configurable"
|
27
27
|
gem.add_runtime_dependency "json"
|
28
28
|
gem.add_runtime_dependency "representable"
|
29
|
+
gem.add_runtime_dependency "bunny", "0.9.0.pre6"
|
29
30
|
|
30
31
|
if RUBY_VERSION < "1.9"
|
31
32
|
gem.add_development_dependency "ruby-debug"
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: untied-publisher
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 19
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
8
|
- 0
|
9
|
-
-
|
10
|
-
version: 0.0.
|
9
|
+
- 6
|
10
|
+
version: 0.0.6
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Guilherme Cavalcanti
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date:
|
18
|
+
date: 2013-02-22 00:00:00 Z
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|
21
21
|
version_requirements: &id001 !ruby/object:Gem::Requirement
|
@@ -131,6 +131,24 @@ dependencies:
|
|
131
131
|
requirement: *id008
|
132
132
|
- !ruby/object:Gem::Dependency
|
133
133
|
version_requirements: &id009 !ruby/object:Gem::Requirement
|
134
|
+
none: false
|
135
|
+
requirements:
|
136
|
+
- - "="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
hash: 2794158613
|
139
|
+
segments:
|
140
|
+
- 0
|
141
|
+
- 9
|
142
|
+
- 0
|
143
|
+
- pre
|
144
|
+
- 6
|
145
|
+
version: 0.9.0.pre6
|
146
|
+
prerelease: false
|
147
|
+
type: :runtime
|
148
|
+
name: bunny
|
149
|
+
requirement: *id009
|
150
|
+
- !ruby/object:Gem::Dependency
|
151
|
+
version_requirements: &id010 !ruby/object:Gem::Requirement
|
134
152
|
none: false
|
135
153
|
requirements:
|
136
154
|
- - ">="
|
@@ -142,7 +160,7 @@ dependencies:
|
|
142
160
|
prerelease: false
|
143
161
|
type: :development
|
144
162
|
name: ruby-debug
|
145
|
-
requirement: *
|
163
|
+
requirement: *id010
|
146
164
|
description: Provides the Publisher part of the Untied gem.
|
147
165
|
email:
|
148
166
|
- guiocavalcanti@gmail.com
|
@@ -162,18 +180,27 @@ files:
|
|
162
180
|
- README.md
|
163
181
|
- Rakefile
|
164
182
|
- lib/untied-publisher.rb
|
183
|
+
- lib/untied-publisher/amqp.rb
|
184
|
+
- lib/untied-publisher/amqp/producer.rb
|
185
|
+
- lib/untied-publisher/base.rb
|
186
|
+
- lib/untied-publisher/base_producer.rb
|
187
|
+
- lib/untied-publisher/bunny.rb
|
188
|
+
- lib/untied-publisher/bunny/producer.rb
|
165
189
|
- lib/untied-publisher/config.rb
|
190
|
+
- lib/untied-publisher/default_doorkeeper.rb
|
166
191
|
- lib/untied-publisher/doorkeeper.rb
|
167
192
|
- lib/untied-publisher/event.rb
|
168
193
|
- lib/untied-publisher/event_representer.rb
|
169
194
|
- lib/untied-publisher/observer.rb
|
170
|
-
- lib/untied-publisher/producer.rb
|
171
195
|
- lib/untied-publisher/railtie.rb
|
172
196
|
- lib/untied-publisher/version.rb
|
197
|
+
- spec/amqp/amqp_producer_spec.rb
|
198
|
+
- spec/base_producer_spec.rb
|
199
|
+
- spec/bunny/bunny_producer_spec.rb
|
173
200
|
- spec/doorkeeper_spec.rb
|
201
|
+
- spec/event_representer_spec.rb
|
174
202
|
- spec/event_spec.rb
|
175
|
-
- spec/
|
176
|
-
- spec/publisher_observer_spec.rb
|
203
|
+
- spec/observer_spec.rb
|
177
204
|
- spec/spec_helper.rb
|
178
205
|
- spec/support/setup_ar_and_schema.rb
|
179
206
|
- untied-publisher.gemspec
|
@@ -211,9 +238,12 @@ signing_key:
|
|
211
238
|
specification_version: 3
|
212
239
|
summary: Untied is a Observer Pattern implementation for distributed systems. Think as a cross-application ActiveRecord::Observer. This gem handles the publishing of events
|
213
240
|
test_files:
|
241
|
+
- spec/amqp/amqp_producer_spec.rb
|
242
|
+
- spec/base_producer_spec.rb
|
243
|
+
- spec/bunny/bunny_producer_spec.rb
|
214
244
|
- spec/doorkeeper_spec.rb
|
245
|
+
- spec/event_representer_spec.rb
|
215
246
|
- spec/event_spec.rb
|
216
|
-
- spec/
|
217
|
-
- spec/publisher_observer_spec.rb
|
247
|
+
- spec/observer_spec.rb
|
218
248
|
- spec/spec_helper.rb
|
219
249
|
- spec/support/setup_ar_and_schema.rb
|
@@ -1,73 +0,0 @@
|
|
1
|
-
# -*- encoding : utf-8 -*-
|
2
|
-
require 'amqp'
|
3
|
-
|
4
|
-
module Untied
|
5
|
-
module Publisher
|
6
|
-
class Producer
|
7
|
-
# Encapsulates both the Channel and Exchange (AMQP).
|
8
|
-
|
9
|
-
def initialize(opts={})
|
10
|
-
@opts = {
|
11
|
-
:service_name => Publisher.config.service_name,
|
12
|
-
:deliver_messages => Publisher.config.deliver_messages,
|
13
|
-
:channel => nil,
|
14
|
-
}.merge(opts)
|
15
|
-
|
16
|
-
Publisher.config.logger.info "Untied::Publisher: Producer intialized with options #{@opts.inspect}"
|
17
|
-
|
18
|
-
@routing_key = "untied.#{@opts[:service_name]}"
|
19
|
-
|
20
|
-
if !@opts[:deliver_messages]
|
21
|
-
Publisher.config.logger.info \
|
22
|
-
"AMQP.channel was not setted up because message delivering is disabled."
|
23
|
-
return
|
24
|
-
end
|
25
|
-
|
26
|
-
check_em_reactor
|
27
|
-
|
28
|
-
if AMQP.channel || @opts[:channel]
|
29
|
-
Publisher.config.logger.info "Using defined AMQP.channel"
|
30
|
-
@channel = AMQP.channel || @opts[:channel]
|
31
|
-
end
|
32
|
-
end
|
33
|
-
|
34
|
-
# Publish the given event.
|
35
|
-
# event: object which is going to be serialized and sent through the
|
36
|
-
# wire. It should respond to #to_json.
|
37
|
-
def publish(event)
|
38
|
-
safe_publish(event)
|
39
|
-
end
|
40
|
-
|
41
|
-
protected
|
42
|
-
|
43
|
-
# Publishes if message delivering is enabled. Otherwise just warns.
|
44
|
-
def safe_publish(e)
|
45
|
-
if @opts[:deliver_messages]
|
46
|
-
on_exchange do |exchange|
|
47
|
-
exchange.publish(e.to_json, :routing_key => @routing_key) do
|
48
|
-
Publisher.config.logger.info \
|
49
|
-
"Publishing event #{e.inspect} with routing key #{@routing_key}"
|
50
|
-
end
|
51
|
-
end
|
52
|
-
else
|
53
|
-
Publisher.config.logger.info \
|
54
|
-
"The event #{ e.inspect} was not delivered. Try to set " + \
|
55
|
-
"Untied::Publisher.config.deliver_messages to true"
|
56
|
-
end
|
57
|
-
end
|
58
|
-
|
59
|
-
# Creates a new exchange and yields it to the block passed when it's ready
|
60
|
-
def on_exchange(&block)
|
61
|
-
return unless @channel
|
62
|
-
@channel.topic('untied', :auto_delete => true, &block)
|
63
|
-
end
|
64
|
-
|
65
|
-
def check_em_reactor
|
66
|
-
if !defined?(EventMachine) || !EM.reactor_running?
|
67
|
-
raise "In order to use the producer you must be running inside an " + \
|
68
|
-
"eventmachine loop"
|
69
|
-
end
|
70
|
-
end
|
71
|
-
end
|
72
|
-
end
|
73
|
-
end
|
data/spec/producer_spec.rb
DELETED
@@ -1,61 +0,0 @@
|
|
1
|
-
# -*- encoding : utf-8 -*-
|
2
|
-
require 'spec_helper'
|
3
|
-
|
4
|
-
module Untied
|
5
|
-
module Publisher
|
6
|
-
describe Producer do
|
7
|
-
context "configuration" do
|
8
|
-
it "should use service name configured" do
|
9
|
-
opts = Producer.new.instance_variable_get(:@opts)
|
10
|
-
opts[:service_name].should == 'core'
|
11
|
-
end
|
12
|
-
end
|
13
|
-
|
14
|
-
it "should initilize producer" do
|
15
|
-
Producer.new.should be_a Producer
|
16
|
-
end
|
17
|
-
|
18
|
-
it "should raise RuntimeError when trying to run without connection" do
|
19
|
-
expect {
|
20
|
-
Producer.new(:deliver_messages => true)
|
21
|
-
}.to raise_error
|
22
|
-
end
|
23
|
-
|
24
|
-
it "should raise RuntimeError if EM is not running" do
|
25
|
-
mock_reactor_and_amqp do |c|
|
26
|
-
EM.stub("reactor_running?" => false)
|
27
|
-
expect {
|
28
|
-
Producer.new(:channel => c, :deliver_messages => true)
|
29
|
-
}.to raise_error
|
30
|
-
end
|
31
|
-
end
|
32
|
-
|
33
|
-
context "#publish" do
|
34
|
-
let(:event) do
|
35
|
-
Event.new(:name => "create", :payload => { :foo => 'bar' }, :origin => :core)
|
36
|
-
end
|
37
|
-
|
38
|
-
it "should call Channel#publish" do
|
39
|
-
mock_reactor_and_amqp do |channel|
|
40
|
-
producer = Producer.new(:channel => channel, :deliver_messages => true)
|
41
|
-
producer.should_receive(:safe_publish).with(event)
|
42
|
-
producer.publish(event)
|
43
|
-
end
|
44
|
-
end
|
45
|
-
end
|
46
|
-
|
47
|
-
def mock_reactor_and_amqp
|
48
|
-
# Do nothing when calling start
|
49
|
-
Untied.stub(:start).and_return(nil)
|
50
|
-
# Simulate reactor running
|
51
|
-
EM.stub(:reactor_running?).and_return(true)
|
52
|
-
|
53
|
-
exchange = double('Exchange')
|
54
|
-
channel = double('Channel')
|
55
|
-
channel.stub(:topic).and_return(exchange)
|
56
|
-
|
57
|
-
yield(channel)
|
58
|
-
end
|
59
|
-
end
|
60
|
-
end
|
61
|
-
end
|