basquiat 1.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +19 -0
  3. data/.metrics +5 -0
  4. data/.rspec +4 -0
  5. data/.rubocop.yml +12 -0
  6. data/.ruby-gemset +1 -0
  7. data/.ruby-version +1 -0
  8. data/.travis.yml +3 -0
  9. data/Gemfile +4 -0
  10. data/Guardfile +17 -0
  11. data/LICENSE.txt +22 -0
  12. data/README.md +95 -0
  13. data/Rakefile +13 -0
  14. data/basquiat.gemspec +39 -0
  15. data/docker/Dockerfile +11 -0
  16. data/docker/basquiat_start.sh +9 -0
  17. data/docker-compose.yml +11 -0
  18. data/lib/basquiat/adapters/base_adapter.rb +54 -0
  19. data/lib/basquiat/adapters/rabbitmq_adapter.rb +130 -0
  20. data/lib/basquiat/adapters/test_adapter.rb +52 -0
  21. data/lib/basquiat/adapters.rb +2 -0
  22. data/lib/basquiat/interfaces/base.rb +62 -0
  23. data/lib/basquiat/rails/railtie.rb +14 -0
  24. data/lib/basquiat/support/configuration.rb +62 -0
  25. data/lib/basquiat/support/hash_refinements.rb +24 -0
  26. data/lib/basquiat/support/json.rb +13 -0
  27. data/lib/basquiat/support.rb +3 -0
  28. data/lib/basquiat/version.rb +4 -0
  29. data/lib/basquiat.rb +27 -0
  30. data/spec/lib/adapters/base_adapter_spec.rb +11 -0
  31. data/spec/lib/adapters/rabbitmq_adapter_spec.rb +79 -0
  32. data/spec/lib/adapters/test_adapter_spec.rb +61 -0
  33. data/spec/lib/basquiat_spec.rb +27 -0
  34. data/spec/lib/interfaces/base_spec.rb +100 -0
  35. data/spec/lib/support/configuration_spec.rb +73 -0
  36. data/spec/lib/support/hash_refinements_spec.rb +24 -0
  37. data/spec/lib/support/json_spec.rb +5 -0
  38. data/spec/spec_helper.rb +22 -0
  39. data/spec/support/basquiat.yml +9 -0
  40. data/spec/support/shared_examples/basquiat_adapter_shared_examples.rb +23 -0
  41. metadata +305 -0
@@ -0,0 +1,24 @@
1
+ module Basquiat
2
+ module HashRefinements
3
+ refine Hash do
4
+ def deep_merge(other_hash)
5
+ other_hash.each_pair do |key, value|
6
+ current = self[key]
7
+ if current.is_a?(Hash) && value.is_a?(Hash)
8
+ current.deep_merge(value)
9
+ else
10
+ self[key] = value
11
+ end
12
+ end
13
+ end
14
+
15
+ def symbolize_keys
16
+ each_with_object({}) do |(key, value), new_hash|
17
+ new_key = key.to_sym rescue key
18
+ new_value = (value.is_a? Hash) ? value.symbolize_keys : value
19
+ new_hash[new_key] = new_value
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,13 @@
1
+ module Basquiat
2
+ module Json
3
+ def self.encode(object)
4
+ MultiJson.dump(object)
5
+ end
6
+
7
+ def self.decode(object)
8
+ MultiJson.load(object, symbolize_keys: true)
9
+ rescue MultiJson::ParseError
10
+ {}
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,3 @@
1
+ require 'basquiat/support/hash_refinements'
2
+ require 'basquiat/support/configuration'
3
+ require 'basquiat/support/json'
@@ -0,0 +1,4 @@
1
+ # Version file
2
+ module Basquiat
3
+ VERSION = '1.1.1'
4
+ end
data/lib/basquiat.rb ADDED
@@ -0,0 +1,27 @@
1
+ require 'multi_json'
2
+ require 'naught'
3
+ require 'yaml'
4
+
5
+ require 'basquiat/support'
6
+ require 'basquiat/adapters'
7
+ require 'basquiat/version'
8
+ require 'basquiat/interfaces/base'
9
+
10
+ # Overall namespace And config class
11
+ module Basquiat
12
+ class << self
13
+ def reset
14
+ @configuration = Configuration.new
15
+ end
16
+
17
+ def configuration
18
+ @configuration ||= Configuration.new
19
+ end
20
+
21
+ def configure
22
+ yield configuration
23
+ end
24
+ end
25
+ end
26
+
27
+ require_relative 'basquiat/rails/railtie.rb' if defined?(Rails)
@@ -0,0 +1,11 @@
1
+ require 'spec_helper'
2
+
3
+ # Sample class used for testing
4
+ class SampleAdapter
5
+ include Basquiat::Adapters::Base
6
+ end
7
+
8
+ describe Basquiat::Adapters::Base do
9
+ subject { SampleAdapter.new }
10
+ it_behaves_like 'a Basquiat::Adapter'
11
+ end
@@ -0,0 +1,79 @@
1
+ require 'spec_helper'
2
+ require 'basquiat/adapters/rabbitmq_adapter'
3
+
4
+ describe Basquiat::Adapters::RabbitMq do
5
+ subject { Basquiat::Adapters::RabbitMq.new }
6
+
7
+ it_behaves_like 'a Basquiat::Adapter'
8
+
9
+ let(:base_options) do
10
+ { servers: [{ host: ENV.fetch('BASQUIAT_RABBITMQ_1_PORT_5672_TCP_ADDR') { 'localhost' },
11
+ port: ENV.fetch('BASQUIAT_RABBITMQ_1_PORT_5672_TCP_PORT') { 5672 } }] }
12
+ end
13
+
14
+ before(:each) do
15
+ subject.adapter_options(base_options)
16
+ end
17
+
18
+ after(:each) do
19
+ remove_queues_and_exchanges
20
+ end
21
+
22
+ context 'failover' do
23
+ let(:failover_settings) do
24
+ base_options[:servers].unshift({ host: 'localhost', port: 1234 })
25
+ base_options.merge(failover: { default_timeout: 0.2, max_retries: 2 })
26
+ end
27
+
28
+ it 'tries a reconnection after a few seconds' do
29
+ subject.adapter_options(servers: [host: 'localhost', port: 1234],
30
+ failover: { default_timeout: 0.2, max_retries: 1 })
31
+ expect { subject.connect }.to raise_exception(Bunny::TCPConnectionFailed)
32
+ end
33
+
34
+ it 'uses another server after all retries on a single one' do
35
+ subject.adapter_options(failover_settings)
36
+ expect { subject.connect }.to_not raise_error
37
+ expect(subject.connection_uri).to match(/5672/)
38
+ end
39
+ end
40
+
41
+ it '#connected?' do
42
+ expect(subject.connected?).to be_nil
43
+ subject.connect
44
+ expect(subject.connected?).to_not be_nil
45
+ end
46
+
47
+ context 'publisher' do
48
+ it '#publish [enqueue a message]' do
49
+ expect do
50
+ subject.publish('messages.welcome', data: 'A Nice Welcome Message')
51
+ end.to_not raise_error
52
+ end
53
+ end
54
+
55
+ context 'listener' do
56
+ it '#subscribe_to some event' do
57
+ message_received = ''
58
+ subject.subscribe_to('some.event', lambda do |msg|
59
+ msg[:data].upcase!
60
+ message_received = msg
61
+ end)
62
+ subject.listen(block: false)
63
+
64
+ subject.publish('some.event', data: 'coisa')
65
+ sleep 0.1 # Wait for the listening thread.
66
+
67
+ expect(message_received).to eq(data: 'COISA')
68
+ end
69
+ end
70
+
71
+ def remove_queues_and_exchanges
72
+ subject.send(:queue).delete
73
+ subject.send(:exchange).delete
74
+ rescue Bunny::TCPConnectionFailed
75
+ true
76
+ ensure
77
+ subject.send(:disconnect)
78
+ end
79
+ end
@@ -0,0 +1,61 @@
1
+ require 'spec_helper'
2
+
3
+ describe Basquiat::Adapters::Test do
4
+ subject { Basquiat::Adapters::Test.new }
5
+ it_behaves_like 'a Basquiat::Adapter'
6
+
7
+ context 'publisher' do
8
+ it '#publish [enqueue a message]' do
9
+ expect do
10
+ subject.publish('messages.welcome', value: 'A Nice Welcome Message')
11
+ end.to change { subject.events('messages.welcome').size }.by(1)
12
+ expect(subject.events('messages.welcome')[0]).to match(/A Nice Welcome Message/)
13
+ end
14
+ end
15
+
16
+ context 'listener' do
17
+ before(:each) do
18
+ subject.publish('some.event', data: 'some message')
19
+ end
20
+
21
+ it '#subscribe_to some event' do
22
+ subject.subscribe_to('some.event', ->(msg) { msg.values.map(&:upcase) })
23
+ expect(subject.listen).to eq(['SOME MESSAGE'])
24
+ end
25
+
26
+ it '#subscribe_to multiple events' do
27
+ subject.instance_eval <<-METHCALL
28
+ subscribe_to('some.event', ->(msg) { publish 'some.other', data: msg.values.first.upcase; msg })
29
+ METHCALL
30
+ subject.subscribe_to('some.other', ->(msg) { msg.values.first.downcase })
31
+ expect(subject.listen).to eq(data: 'some message')
32
+ expect(subject.events('some.other')[0]).to match(/SOME MESSAGE/)
33
+ expect(subject.listen).to eq('some message')
34
+ end
35
+ end
36
+
37
+ describe '#clean' do
38
+ context 'when no event has been published' do
39
+
40
+ it 'should have no event registered' do
41
+ Basquiat::Adapters::Test.clean
42
+
43
+ expect(Basquiat::Adapters::Test.events).to be_empty
44
+ end
45
+ end
46
+
47
+ context 'when some events have been published' do
48
+ before do
49
+ subject.publish('some.message', value: 'A Nice Welcome Message')
50
+ subject.publish('some.message', value: 'A Nasty Welcome Message')
51
+ subject.publish('other.message', value: 'A Random Welcome Message')
52
+ end
53
+
54
+ it 'should have no event registered' do
55
+ Basquiat::Adapters::Test.clean
56
+
57
+ expect(Basquiat::Adapters::Test.events).to be_empty
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,27 @@
1
+ require 'spec_helper'
2
+
3
+ describe Basquiat do
4
+ after(:all) do
5
+ Basquiat.configure do |config|
6
+ config.config_file = File.expand_path('../../support/basquiat.yml', __FILE__)
7
+ end
8
+ end
9
+
10
+ it 'should have a version number' do
11
+ expect(Basquiat::VERSION).not_to be_nil
12
+ end
13
+
14
+ it '#configure yields to a block' do
15
+ expect { |block| Basquiat.configure(&block) }.to yield_control
16
+ end
17
+
18
+ it '#configuration' do
19
+ expect(Basquiat.configuration).to be_a Basquiat::Configuration
20
+ end
21
+
22
+ it '#reset' do
23
+ config = Basquiat.configuration
24
+ Basquiat.reset
25
+ expect(Basquiat.configuration).not_to equal(config)
26
+ end
27
+ end
@@ -0,0 +1,100 @@
1
+ require 'spec_helper'
2
+
3
+ # Sample class used for testing
4
+ class SampleClass
5
+ extend Basquiat::Base
6
+ self.event_adapter = Basquiat::Adapters::Test
7
+ end
8
+
9
+ describe Basquiat::Base do
10
+ subject { SampleClass }
11
+
12
+ it '.event_adapter' do
13
+ expect(subject).to respond_to(:event_adapter=)
14
+ end
15
+
16
+ it '.event_source(option_hash)' do
17
+ expect(subject).to respond_to(:adapter_options)
18
+ end
19
+
20
+ it 'set the adapter options' do
21
+ subject.adapter_options host: 'localhost', port: 5672, durable: true
22
+ expect(subject.adapter.options[:port]).to eq(5672)
23
+ expect(subject.adapter.options[:host]).to eq('localhost')
24
+ expect(subject.adapter.options[:durable]).to be_truthy
25
+ end
26
+
27
+ it 'delegates disconnect and connected? to the adapter' do
28
+ expect(subject.adapter).to receive(:connected?)
29
+ subject.connected?
30
+
31
+ expect(subject.adapter).to receive(:disconnect)
32
+ subject.disconnect
33
+ end
34
+
35
+ context 'using the defaults' do
36
+ class DefaultClass
37
+ extend Basquiat::Base
38
+ end
39
+
40
+ subject(:defaults) { DefaultClass }
41
+
42
+ it 'has an event_adapter' do
43
+ expect(defaults.adapter).to be_a(Basquiat::Adapters::Test)
44
+ end
45
+
46
+ it 'publishes to the configured queue and exchanges' do
47
+ expect do
48
+ subject.publish('test.message', message: 'useful test message')
49
+ end.to change { subject.adapter.events('test.message').size }.by(1)
50
+ end
51
+ end
52
+
53
+ context 'as a Producer' do
54
+ it '#publish' do
55
+ expect do
56
+ subject.publish('test.message', message: 'useful test message')
57
+ end.to change { subject.adapter.events('test.message').size }.by(1)
58
+ end
59
+ end
60
+
61
+ context 'as a Consumer' do
62
+ before(:each) do
63
+ subject.publish('some.event', 'test message')
64
+ end
65
+
66
+ after(:each) do
67
+ subject.adapter.events('some.event').clear
68
+ end
69
+
70
+ it 'reads a message from the queue' do
71
+ subject.subscribe_to 'some.event', ->(msg) { msg }
72
+ expect do
73
+ subject.listen(block: false)
74
+ end.to change { subject.adapter.events('some.event').size }.by(-1)
75
+ end
76
+
77
+ it 'runs the proc for each message' do
78
+ subject.subscribe_to('some.event', ->(msg) { "#{msg} LAMBDA LAMBDA LAMBDA" })
79
+ expect(subject.listen(block: false)).to match(/LAMBDA LAMBDA LAMBDA$/)
80
+ end
81
+
82
+ it 'can receive a symbol that will point to a method' do
83
+ def subject.test_method(msg)
84
+ msg.scan(/e/)
85
+ end
86
+
87
+ subject.subscribe_to('some.event', :test_method)
88
+ expect(subject.listen(block: false)).to eq(%w(e e e))
89
+ end
90
+ end
91
+
92
+ it 'trigger an event after processing a message' do
93
+ subject.publish('some.event', 'some message')
94
+ subject.instance_eval <<-METHCALL
95
+ subscribe_to 'some.event', ->(msg) { publish('other.event', "Redirected \#{msg}") }
96
+ METHCALL
97
+ expect { subject.listen(block: false) }.to_not raise_error
98
+ expect(subject.adapter.events('other.event').size).to eq(1)
99
+ end
100
+ end
@@ -0,0 +1,73 @@
1
+ require 'spec_helper'
2
+
3
+ describe Basquiat::Configuration do
4
+ subject(:config) { Basquiat::Configuration.new }
5
+
6
+ context 'accessors' do
7
+ it '#environment' do
8
+ expect(config.environment).to eq(:test)
9
+ end
10
+
11
+ it '#environment=' do
12
+ config.environment = 'test'
13
+ expect(config.environment).to eq(:test)
14
+ end
15
+
16
+ it '#queue_name' do
17
+ expect(config.queue_name).to eq('basquiat.queue')
18
+ end
19
+
20
+ it '#queue_name=' do
21
+ config.queue_name = 'basquiat.test'
22
+ expect(config.queue_name).to eq('basquiat.test')
23
+
24
+ config.queue_name = nil
25
+ expect(config.queue_name).to eq('basquiat.queue')
26
+ end
27
+
28
+ it '#exchange_name' do
29
+ expect(config.exchange_name).to eq('basquiat.exchange')
30
+ end
31
+
32
+ it '#exchange_name=' do
33
+ config.exchange_name = 'test'
34
+ expect(config.exchange_name).to eq('test')
35
+
36
+ config.exchange_name = nil
37
+ expect(config.exchange_name).to eq('basquiat.exchange')
38
+ end
39
+
40
+ it '#logger' do
41
+ expect(config.logger).not_to be_nil
42
+ end
43
+
44
+ it '#logger=' do
45
+ config.logger = Logger.new('/dev/null')
46
+ expect(config.logger).to be_a Logger
47
+ end
48
+ end
49
+
50
+ it '#config_file=' do
51
+ config.config_file = File.join(File.dirname(__FILE__), '../../support/basquiat.yml')
52
+
53
+ expect(config.queue_name).to eq('my.nice_queue')
54
+ expect(config.exchange_name).to eq('my.test_exchange')
55
+ expect(config.default_adapter).to eq('Basquiat::Adapters::Test')
56
+ expect(config.adapter_options).to have_key(:servers)
57
+ end
58
+
59
+ it 'settings provided on the config file have lower precedence' do
60
+ config.exchange_name = 'super.nice_exchange'
61
+ config.config_file = File.join(File.dirname(__FILE__), '../../support/basquiat.yml')
62
+
63
+ expect(config.exchange_name).to eq('super.nice_exchange')
64
+ end
65
+
66
+ it '#reload_classes' do
67
+ class MyTest
68
+ extend Basquiat::Base
69
+ end
70
+
71
+ config.reload_classes
72
+ end
73
+ end
@@ -0,0 +1,24 @@
1
+ require 'spec_helper'
2
+
3
+ describe Basquiat::HashRefinements do
4
+ using Basquiat::HashRefinements
5
+
6
+ subject(:hash) do
7
+ { 'key' => 1, 'another_key' => 2, 3 => 3, 'array' => [1, 3, 5], 'hash' => { 'inner_key' => 6 } }
8
+ end
9
+
10
+ it '#deep_merge' do
11
+ hash.deep_merge({ 'hash' => { 'inner_key' => 7, 'other_inner_key' => 10 } })
12
+ expect(hash['hash']).to have_key('other_inner_key')
13
+ expect(hash['hash']['inner_key']).to eq(7)
14
+ end
15
+
16
+ it '#symbolize_keys' do
17
+ symbol_hash = hash.symbolize_keys
18
+ expect(symbol_hash).to have_key(:array)
19
+ expect(symbol_hash).to have_key(3)
20
+ expect(symbol_hash).to have_key(:hash)
21
+ expect(symbol_hash[:hash]).to have_key(:inner_key)
22
+ end
23
+
24
+ end
@@ -0,0 +1,5 @@
1
+ describe Basquiat::Json do
2
+ it "returns an empty hash if it can't parse the payload" do
3
+ expect(Basquiat::Json.decode('Idaho Potato')).to eq({})
4
+ end
5
+ end
@@ -0,0 +1,22 @@
1
+ $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
2
+
3
+ require 'simplecov'
4
+
5
+ SimpleCov.start do
6
+ add_filter { |source| source.lines_of_code <= 3 }
7
+ add_filter { |source| source.filename =~ /spec/ }
8
+
9
+ add_group('Adapters') { |source| source.filename =~ /basquiat\/adapters/ }
10
+ add_group('Interfaces') { |source| source.filename =~ /basquiat\/interfaces/ }
11
+ add_group('Support') { |source| source.filename =~ /basquiat\/support/ }
12
+ add_group('Main Gem File') { |source| source.filename =~ %r{\/lib\/basquiat\.rb$} }
13
+ end
14
+
15
+ ENV['BASQUIAT_ENV'] = 'test'
16
+ require 'basquiat'
17
+
18
+ Basquiat.configure do |config|
19
+ config.config_file = File.expand_path('../support/basquiat.yml', __FILE__)
20
+ end
21
+
22
+ require 'support/shared_examples/basquiat_adapter_shared_examples'
@@ -0,0 +1,9 @@
1
+ test:
2
+ exchange_name: 'my.test_exchange'
3
+ queue_name: 'my.nice_queue'
4
+ default_adapter: Basquiat::Adapters::Test
5
+ adapter_options:
6
+ servers:
7
+ -
8
+ host: <%= ENV.fetch('BASQUIAT_RABBITMQ_1_PORT_5672_TCP_ADDR') { 'localhost' } %>
9
+ port: <%= ENV.fetch('BASQUIAT_RABBITMQ_1_PORT_5672_TCP_PORT') { 5672 } %>
@@ -0,0 +1,23 @@
1
+ shared_examples_for 'a Basquiat::Adapter' do
2
+ it '#adapter_options(opts)' do
3
+ expect(subject).to respond_to(:adapter_options)
4
+ end
5
+
6
+ it '#publish' do
7
+ expect(subject).to respond_to(:publish)
8
+ end
9
+
10
+ it '#default_options [template for option initialization]' do
11
+ expect(subject).to respond_to(:default_options)
12
+ end
13
+
14
+ it '#subscribe_to' do
15
+ expect(subject).to respond_to(:subscribe_to)
16
+ end
17
+
18
+ it 'merges the options with the default ones' do
19
+ opts = subject.instance_variable_get(:@options)
20
+ subject.adapter_options(nice_option: '127.0.0.2')
21
+ expect(opts[:nice_option]).to eq('127.0.0.2')
22
+ end
23
+ end