synapses 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: e3261f0f0862b7da1353c90061c96343d0ff71ed
4
+ data.tar.gz: 957ef33f068ceedaa4bfaa72baa9aa2635188745
5
+ SHA512:
6
+ metadata.gz: 57e1246b031236ff6e9a273724bb1accebe35fc8e55f582c9185b530c252a3f6b1844a2b8b3e0f39e5ddfe5cc0fc73998c6d9ff5ef146d796a7dda72b1081ddb
7
+ data.tar.gz: b127deed76a22ad81f51991f621251eee0ff2a1b77977bc8d1403547df2dcd535dd1726ac3533455d475f57e2ab987c54e418b36b1a413695807f06db51eb978
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
data/Gemfile ADDED
@@ -0,0 +1,14 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in synapses.gemspec
4
+ gemspec
5
+
6
+ group :development do
7
+ gem 'yardstick'
8
+ gem 'backports'
9
+ end
10
+
11
+ group :development, :test do
12
+ gem 'bson'
13
+ gem 'bson_ext'
14
+ end
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Alexander Semyonov
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,29 @@
1
+ # Synapses
2
+
3
+ TODO: Write a gem description
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'synapses'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install synapses
18
+
19
+ ## Usage
20
+
21
+ TODO: Write usage instructions here
22
+
23
+ ## Contributing
24
+
25
+ 1. Fork it
26
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
27
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
28
+ 4. Push to the branch (`git push origin my-new-feature`)
29
+ 5. Create new Pull Request
@@ -0,0 +1,51 @@
1
+ require 'bundler/gem_tasks'
2
+
3
+ begin
4
+ require 'rspec/core/rake_task'
5
+ RSpec::Core::RakeTask.new(:spec)
6
+ rescue LoadError
7
+ task :spec do
8
+ abort 'RSpec is not available. In order to run specs, you must: gem install rspec'
9
+ end
10
+ end
11
+
12
+ namespace :doc do
13
+ begin
14
+ require 'yard'
15
+ YARD::Rake::YardocTask.new(:default)
16
+ rescue LoadError
17
+ task :default do
18
+ abort 'YARD is not available. In order to run yardoc, you must: gem install yard'
19
+ end
20
+ end
21
+
22
+ begin
23
+ require 'yardstick/rake/measurement'
24
+
25
+ Yardstick::Rake::Measurement.new(:measure) do |measurement|
26
+ measurement.output = 'doc/measurement.txt'
27
+ end
28
+ task doc: :measure
29
+ rescue LoadError
30
+ task :measure do
31
+ abort 'YARDStick is not available. In order to measure documentation coverage, you should run `gem install yardstick`'
32
+ end
33
+ end
34
+
35
+ begin
36
+ require 'yardstick/rake/verify'
37
+ Yardstick::Rake::Verify.new(:verify) do |verify|
38
+ verify.threshold = 100
39
+ end
40
+ task doc: :verify
41
+ rescue LoadError
42
+ task :verify do
43
+ abort 'YARDStick is not available. In order to verify documentation coverage, you should run `gem install backports yardstick`'
44
+ end
45
+ end
46
+ end
47
+
48
+ task doc: 'doc:default'
49
+
50
+
51
+ task default: :spec
@@ -0,0 +1,2 @@
1
+ development:
2
+ uri: amqp://localhost
@@ -0,0 +1,5 @@
1
+ ---
2
+ commit_keyword: "#giteaucrat"
3
+ copyright_owner: Alexander Semyonov
4
+ copyright_format: "© %{owner}, %{years}"
5
+ include_encoding: true
@@ -0,0 +1,2 @@
1
+ development:
2
+ uri: amqp://localhost
@@ -0,0 +1,19 @@
1
+ gov:
2
+ name: radar
3
+ queues:
4
+ logger:
5
+ bind: amq.fanout
6
+ messages:
7
+ ufo:
8
+ class_name: UFO
9
+ schema:
10
+ shape: String
11
+ color: string
12
+ rocket:
13
+ schema:
14
+ speed: Integer
15
+ target: [Integer, Integer]
16
+ plane:
17
+ schema:
18
+ brand: String
19
+ color: String
@@ -0,0 +1,11 @@
1
+ mil:
2
+ name: rocket_center
3
+ queues:
4
+ rocket_shield:
5
+ bind: amq.fanout
6
+ rocket_launcher:
7
+ bind: amq.fanout
8
+ messages:
9
+ protect:
10
+
11
+ shed:
@@ -0,0 +1,110 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $:.unshift File.expand_path('../../lib', __FILE__)
4
+ require 'synapses'
5
+
6
+ Synapses.setup
7
+
8
+ module Messages
9
+ include Synapses::Messages
10
+
11
+ class UFO < Message
12
+ self.routing_key = 'gov.radar.ufo'
13
+ self.message_type = 'gov.radar.ufo'
14
+ attribute :shape
15
+ attribute :color
16
+
17
+ def description
18
+ "UFO in shape of #{shape}, colored in #{color}"
19
+ end
20
+ end
21
+
22
+ class Plane < Message
23
+ self.routing_key = 'gov.radar.plane'
24
+ self.message_type = 'gov.radar.plane'
25
+ attribute :brand
26
+
27
+ def description
28
+ "There is a #{brand} Plane!"
29
+ end
30
+ end
31
+
32
+ class Rocket < Message
33
+ self.routing_key = 'gov.radar.rocket'
34
+ self.message_type = 'gov.radar.rocket'
35
+ attribute :speed
36
+ attribute :target
37
+
38
+ def description
39
+ "There is a rocket flying to the #{target} (#{speed})!"
40
+ end
41
+ end
42
+
43
+ class Protect < Message
44
+ self.routing_key = 'mil.rocket_center.protect'
45
+ self.message_type = 'mil.rocket_center.protect'
46
+ end
47
+ end
48
+
49
+ class Radar < Synapses::Producer
50
+ exchange 'amq.fanout'
51
+ end
52
+
53
+ class RocketCenter < Synapses::Producer
54
+ exchange 'amq.direct'
55
+ end
56
+
57
+ class RocketLauncher < Synapses::Consumer
58
+ exchange 'amq.fanout'
59
+ queue 'mil.rocket_center.rocket_launcher'
60
+
61
+ on Messages::UFO do |ufo|
62
+ puts "RocketLauncher: Messages::UFO -> #{ufo.description}"
63
+ end
64
+
65
+ on Messages::Rocket do |rocket|
66
+ puts "RocketLauncher: Messages::Rocket -> #{rocket.description}"
67
+ end
68
+ end
69
+
70
+ class RocketShield < Synapses::Consumer
71
+ exchange 'amq.fanout'
72
+ queue 'mil.rocket_center.rocket_shield'
73
+
74
+ on Messages::Protect do |protect|
75
+ puts "RocketShield: Messages::Protect -> #{protect}"
76
+ end
77
+ end
78
+
79
+ class RadarLogger < Synapses::Consumer
80
+ exchange 'amq.fanout'
81
+ queue 'gov.radar.logger'
82
+
83
+ on do |metadata, payload|
84
+ puts "RadarLogger -> #{metadata.routing_key}, #{payload}"
85
+ end
86
+ end
87
+
88
+ rockets_center_channel = Synapses.another_channel
89
+ rocket_launcher = RocketLauncher.new(rockets_center_channel)
90
+ rocket_shield = RocketShield.new(rockets_center_channel)
91
+
92
+ logger_channel = Synapses.another_channel
93
+ radar_logger = RadarLogger.new(logger_channel)
94
+
95
+ radar_channel = Synapses.another_channel
96
+ radar = Radar.new(radar_channel)
97
+
98
+ puts 'Publishing Radar messages'
99
+ 1000.times do |i|
100
+ radar << Messages::Plane.new(brand: 'boeing') if (i % 3) == 0
101
+ radar << Messages::Rocket.new(target: 'Moon', speed: 42 * i) if (i % 4) == 0
102
+ radar << Messages::UFO.new(shape: 'circle', color: 'green') if (i % 5) == 0
103
+ end
104
+
105
+ EM.run do
106
+ tick_tack = true
107
+ EM.add_periodic_timer(2) { puts (tick_tack = !tick_tack) ? 'tick' : 'tack' }
108
+ end
109
+
110
+ sleep(10)
@@ -0,0 +1,37 @@
1
+ require 'yaml'
2
+ require 'active_support/core_ext/hash/keys'
3
+
4
+ module AMQP
5
+ module Integration
6
+ class Rails
7
+ # @return [String] application environment
8
+ def self.environment
9
+ if defined?(::Rails)
10
+ ::Rails.env
11
+ else
12
+ ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
13
+ end
14
+ end
15
+
16
+ # @return [String] application root directory
17
+ def self.root
18
+ defined?(::Rails) && ::Rails.root || Dir.pwd
19
+ end
20
+
21
+ def self.start(options_or_uri = {}, &block)
22
+ yaml = YAML.load_file(File.join(root, 'config', 'amqp.yml'))
23
+ settings = yaml.fetch(environment, Hash.new).symbolize_keys
24
+
25
+ arg = if options_or_uri.is_a?(Hash)
26
+ settings.merge(options_or_uri)[:uri]
27
+ else
28
+ settings[:uri] || options_or_uri
29
+ end
30
+
31
+ EventMachine.next_tick do
32
+ AMQP.start(arg, &block)
33
+ end
34
+ end
35
+ end # Rails
36
+ end # Integration
37
+ end # AMQP
@@ -0,0 +1,57 @@
1
+ # coding: utf-8
2
+
3
+ ################################################
4
+ # © Alexander Semyonov, 2013—2013 #
5
+ # Author: Alexander Semyonov <al@semyonov.us> #
6
+ ################################################
7
+
8
+ require 'synapses/version'
9
+ require 'active_support'
10
+ require 'active_support/core_ext/class/attribute'
11
+ require 'amqp'
12
+
13
+ # @author Alexander Semyonov <al@semyonov.us>
14
+ module Synapses
15
+ def self.manager
16
+ @manager ||= Manager.new
17
+ end
18
+
19
+ def self.default_contract
20
+ @default_contract ||= Contract.load_defaults
21
+ end
22
+
23
+ def self.default_channel
24
+ @default_channel ||= default_connection && AMQP.channel
25
+ end
26
+
27
+ def self.default_connection
28
+ @default_connection || manager.start && @default_connection
29
+ end
30
+
31
+ def self.default_connection=(connection)
32
+ @default_connection = connection
33
+ end
34
+
35
+ def self.another_channel(connection = Synapses.default_connection)
36
+ manager.channel(connection)
37
+ end
38
+
39
+ def self.setup
40
+ default_contract
41
+ manager.start
42
+ default_connection
43
+ default_channel
44
+ sleep(0.25)
45
+ #default_contract.setup!
46
+ true
47
+ rescue => e
48
+ STDERR.puts e.message, e.backtrace
49
+ false
50
+ end
51
+ end
52
+
53
+ require 'synapses/contract'
54
+ require 'synapses/producer'
55
+ require 'synapses/consumer'
56
+ require 'synapses/messages'
57
+ require 'synapses/manager'
@@ -0,0 +1,104 @@
1
+ # coding: utf-8
2
+
3
+ ################################################
4
+ # © Alexander Semyonov, 2013—2013 #
5
+ # Author: Alexander Semyonov <al@semyonov.us> #
6
+ ################################################
7
+
8
+ require 'synapses'
9
+ require 'synapses/contract/definitions'
10
+
11
+ module Synapses
12
+ # @author Alexander Semyonov <al@semyonov.us>
13
+ class Consumer
14
+ include Contract::Definitions
15
+
16
+ # @return [String]
17
+ class_attribute :queue_name
18
+
19
+ # @return [Synapses::Contract]
20
+ class_attribute :contract
21
+
22
+ # @return [Array]
23
+ class_attribute :subscriptions
24
+ self.subscriptions = Hash.new { |hash, message_type| hash[message_type] = [] }
25
+
26
+ def self.inherited(child)
27
+ super
28
+ child.subscriptions = subscriptions.dup
29
+ end
30
+
31
+ # @param [String] name
32
+ # @param [Synapses::Contract] contract
33
+ def self.queue(name, contract=Synapses.default_contract)
34
+ self.queue_name = name
35
+ self.contract = contract
36
+ end
37
+
38
+ def self.on(message_type = nil, &block)
39
+ if message_type
40
+ subscriptions[message_type.message_type] << [message_type, block]
41
+ else
42
+ subscriptions[nil] << [nil, block]
43
+ end
44
+ end
45
+
46
+ # @param [AMQP::Channel] channel
47
+ def initialize(channel = Synapses.default_channel)
48
+ @channel = channel
49
+
50
+ queue.subscribe(&method(:message_handler))
51
+ end
52
+
53
+ # @param [AMQP::Header] metadata
54
+ def message_handler(metadata, payload)
55
+ if (typed_subscriptions = self.subscriptions[metadata.type]).any?
56
+ typed_subscriptions.each do |message_class, block|
57
+ message = message_class.parse(metadata, payload)
58
+ block.call(message)
59
+ end
60
+ end
61
+
62
+ if (typeless_subscriptions = self.subscriptions[nil]).any?
63
+ typeless_subscriptions.each do |_, block|
64
+ if block.arity == 2
65
+ block.call(metadata, payload)
66
+ else
67
+ message = Messages.parse(metadata, payload)
68
+ block.call(message)
69
+ end
70
+ end
71
+ end
72
+
73
+ unless (typed_subscriptions + typeless_subscriptions).any?
74
+ puts "#{self} -> #{metadata.type}, #{payload}"
75
+ #puts "#{self} received a message:"
76
+ #puts " metadata.routing_key : #{metadata.routing_key}"
77
+ #puts " metadata.content_type: #{metadata.content_type}"
78
+ #puts " metadata.priority : #{metadata.priority}"
79
+ #puts " metadata.headers : #{metadata.headers.inspect}"
80
+ #puts " metadata.timestamp : #{metadata.timestamp.inspect}"
81
+ #puts " metadata.type : #{metadata.type}"
82
+ #puts " metadata.delivery_tag: #{metadata.delivery_tag}"
83
+ #puts " metadata.redelivered : #{metadata.redelivered}"
84
+ ##puts " metadata.app_id : #{metadata.app_id}"
85
+ #puts " metadata.exchange : #{metadata.exchange}"
86
+ #puts
87
+ #puts " Received a message: #{payload}"
88
+ end
89
+ rescue => e
90
+ puts e
91
+ end
92
+
93
+ # @return [AMQP::Channel]
94
+ attr_accessor :channel
95
+
96
+ def queue
97
+ @queue ||= begin
98
+ queue = contract.queue(queue_name, channel)
99
+ queue.bind(exchange)
100
+ queue
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,114 @@
1
+ # coding: utf-8
2
+
3
+ ################################################
4
+ # © Alexander Semyonov, 2013—2013 #
5
+ # Author: Alexander Semyonov <al@semyonov.us> #
6
+ ################################################
7
+
8
+ require 'synapses'
9
+ require 'yaml'
10
+
11
+ module Synapses
12
+ # @author Alexander Semyonov <al@semyonov.us>
13
+ class Contract
14
+ # @return [String]
15
+ def self.default_contract_path
16
+ File.expand_path('../contract/synapses.yml', __FILE__)
17
+ end
18
+
19
+ # @param [String] file_name
20
+ # @return [Synapse::Contract]
21
+ def self.load_file(file_name)
22
+ hash = YAML.load_file(file_name)
23
+ new(hash)
24
+ end
25
+
26
+ # @param [String] root root directory
27
+ def self.load_defaults(root = './')
28
+ contract = new(YAML.load_file(default_contract_path), root: root)
29
+
30
+ ([File.join(root, 'config/synapses.yml')] +
31
+ Dir[File.join(root, 'config/synapses/*.yml')]).each do |file_name|
32
+
33
+ if File.exists?(file_name)
34
+ hash = YAML.load_file(file_name)
35
+ contract.add_contract(hash)
36
+ end
37
+ end
38
+ contract
39
+ end
40
+
41
+ # @param [Hash] hash
42
+ def initialize(hash, options = {})
43
+ @exchanges = Hash.new do |hash, name|
44
+ raise "Unknown Exchange #{name.inspect}. Known are: #{hash.keys.inspect}"
45
+ end
46
+ @queues = Hash.new do |hash, name|
47
+ raise "Unknown Queue #{name.inspect}. Known are: #{hash.keys.inspect}"
48
+ end
49
+ @options = options
50
+ add_contract(hash)
51
+ end
52
+
53
+ # @param [Hash] contract_hash
54
+ def add_contract(contract_hash)
55
+ contract_hash.each do |ns, hash|
56
+ ns = hash['ns'] if hash.key?('ns')
57
+
58
+ name = hash.delete('name')
59
+ prefix = [ns, name].compact.join('.')
60
+
61
+ if hash['exchanges']
62
+ hash.delete('exchanges').each do |name, attributes|
63
+ name = [prefix, name].join('.').to_s
64
+ exchanges[name] = Exchange.new(name, attributes || {})
65
+ end
66
+ end
67
+
68
+ if hash['queues']
69
+ hash.delete('queues').each do |name, attributes|
70
+ name = [prefix, name].join('.').to_s
71
+ queues[name.to_s] = Queue.new(name, attributes || {})
72
+ end
73
+ end
74
+ end
75
+ end
76
+
77
+ # @param [AMQP::Channel] channel
78
+ def setup!(channel = Synapses.default_channel)
79
+ exchanges.values.each { |exchange| exchange.exchange(channel) }
80
+ queues.values.each { |queue| queue.queue(channel) }
81
+ end
82
+
83
+ # @param [Synapses::Contract::Exchange] name
84
+ def exchange_definition(name)
85
+ exchanges[name.to_s]
86
+ end
87
+
88
+ # @param [Synapses::Contract::Queue] name
89
+ def queue_definition(name)
90
+ queues[name.to_s]
91
+ end
92
+
93
+ # @param [String] name
94
+ # @return [AMQP::Exchange]
95
+ def exchange(name, channel=Synapses.default_channel)
96
+ exchange_definition(name).exchange(channel)
97
+ end
98
+
99
+ # @param [String] name
100
+ # @param [AMQP::Channel] channel
101
+ # @return [AMQP::Queue]
102
+ def queue(name, channel=Synapses.default_channel)
103
+ queue_definition(name).queue(channel)
104
+ end
105
+
106
+ # @return [Hash]
107
+ attr_reader :exchanges
108
+ # @return [Hash]
109
+ attr_reader :queues
110
+ end
111
+ end
112
+
113
+ require 'synapses/contract/exchange'
114
+ require 'synapses/contract/queue'
@@ -0,0 +1,9 @@
1
+ require 'synapses/contract'
2
+
3
+ module Synapses
4
+ class Contract
5
+ # @author Alexander Semyonov <al@semyonov.us>
6
+ module Connectible
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,34 @@
1
+ require 'synapses/contract'
2
+ require 'active_support/concern'
3
+
4
+ module Synapses
5
+ class Contract
6
+ # @author Alexander Semyonov <al@semyonov.us>
7
+ module Definitions
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ # @return [String]
12
+ class_attribute :exchange_name
13
+ self.exchange_name = ''
14
+
15
+ # @return [Synapses::Contract]
16
+ class_attribute :contract
17
+ end
18
+
19
+ module ClassMethods
20
+ # @param [String] name
21
+ # @param [Synapses::Contract] contract
22
+ def exchange(name, contract=Synapses.default_contract)
23
+ self.exchange_name = name
24
+ self.contract = contract
25
+ end
26
+ end
27
+
28
+ # @return [AMQP::Exchange]
29
+ def exchange
30
+ @exchange ||= contract.exchange(exchange_name)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,48 @@
1
+ require 'synapses/contract'
2
+ require 'synapses/contract/connectible'
3
+
4
+ module Synapses
5
+ class Contract
6
+ # @author Alexander Semyonov <al@semyonov.us>
7
+ class Exchange
8
+ # @return [String]
9
+ attr_accessor :name
10
+ # @return ['direct', 'topic', 'fanout', 'headers']
11
+ attr_accessor :type
12
+ # @return [Hash]
13
+ attr_accessor :options
14
+
15
+ # @param [String] name
16
+ # @param [Hash] options see {AMQP::Exchange}
17
+ def initialize(name, options = {})
18
+ @name = name
19
+ @type = options.delete('type') { raise "Type for exchange #{name} is not set" }
20
+ @options = options || {}
21
+ end
22
+
23
+ # @return [AMQP::Channel]
24
+ attr_accessor :channel
25
+ def channel
26
+ @channel ||= Synapses.default_channel
27
+ end
28
+
29
+ # @param [AMQP::Channel] channel
30
+ # @return [AMQP::Exchange]
31
+ def connect(channel)
32
+ @exchange = AMQP::Exchange.new(channel, type, name, options)
33
+ end
34
+
35
+ # @return [Boolean]
36
+ def connected?
37
+ !!@queue
38
+ end
39
+
40
+ # @param [AMQP::Channel] channel
41
+ # @return [AMQP::Exchange]
42
+ def exchange(channel=self.channel)
43
+ connect(channel) unless connected?
44
+ @exchange
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,47 @@
1
+ require 'synapses/contract'
2
+ require 'synapses/contract/connectible'
3
+ require 'amqp/queue'
4
+
5
+ module Synapses
6
+ class Contract
7
+ # @author Alexander Semyonov <al@semyonov.us>
8
+ class Queue
9
+ # @return [String]
10
+ attr_accessor :name
11
+
12
+ # @return [Hash] see {AMQP::Queue#initialize}
13
+ attr_accessor :options
14
+
15
+ # @param [String] name
16
+ # @param [Hash] options see {AMQP::Queue#initialize}
17
+ def initialize(name, options = {})
18
+ @name = name
19
+ @bind = options.delete('bind') { raise "Exchange :bind is not specified for queue #{name}" }
20
+ @options = options || {}
21
+ end
22
+
23
+ # @return [AMQP::Channel]
24
+ attr_accessor :channel
25
+ def channel
26
+ @channel ||= Synapses.default_channel
27
+ end
28
+
29
+ # @return [AMQP::Queue]
30
+ def connect(channel=self.channel)
31
+ @queue = AMQP::Queue.new(channel, name, options)
32
+ end
33
+
34
+ # @return [Boolean]
35
+ def connected?
36
+ !!@queue
37
+ end
38
+
39
+ # @param [AMQP::Channel] channel
40
+ # @return [AMQP::Queue]
41
+ def queue(channel=self.channel)
42
+ connect(channel) unless connected?
43
+ @queue
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,14 @@
1
+ amq:
2
+ exchanges:
3
+ fanout:
4
+ type: fanout
5
+ direct:
6
+ type: direct
7
+ '':
8
+ type: direct
9
+ topic:
10
+ type: topic
11
+ match:
12
+ type: headers
13
+ headers:
14
+ type: headers
@@ -0,0 +1,9 @@
1
+ require 'synapses'
2
+ require 'rails'
3
+
4
+ module Synapses
5
+ # @author Alexander Semyonov <al@semyonov.us>
6
+ class Engine < ::Rails::Engine
7
+
8
+ end
9
+ end
@@ -0,0 +1,37 @@
1
+ require 'synapses'
2
+ require 'amqp/utilities/event_loop_helper'
3
+ require 'amqp/integration/rails'
4
+
5
+ module Synapses
6
+ # @author Alexander Semyonov <al@semyonov.us>
7
+ class Manager
8
+ def start
9
+ AMQP::Utilities::EventLoopHelper.run
10
+ AMQP::Integration::Rails.start do |connection|
11
+ Synapses.default_connection ||= connection
12
+
13
+ connection.on_error do |ch, connection_close|
14
+ raise connection_close.reply_text
15
+ end
16
+
17
+ connection.on_tcp_connection_loss do |conn, settings|
18
+ conn.reconnect(false, 2)
19
+ end
20
+
21
+ connection.on_tcp_connection_failure do |conn, settings|
22
+ conn.reconnect(false, 2)
23
+ end
24
+
25
+ AMQP.channel = channel(connection)
26
+ end
27
+ end
28
+
29
+ def channel(connection = Synapses.default_connection)
30
+ channel = AMQP::Channel.new(connection, AMQP::Channel.next_channel_id, auto_recovery: true)
31
+ channel.on_error do |ch, channel_close|
32
+ raise channel_close.reply_text
33
+ end
34
+ channel
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,30 @@
1
+ require 'synapses'
2
+ require 'json'
3
+
4
+ module Synapses
5
+ # @author Alexander Semyonov <al@semyonov.us>
6
+ module Messages
7
+ extend ActiveSupport::Concern
8
+
9
+ def self.registry
10
+ @registry ||= {}
11
+ end
12
+
13
+ # @param [AMQP::Header] metadata
14
+ # @param [String] payload
15
+ def self.parse(metadata, payload)
16
+ if (message_type = registry[metadata.type])
17
+ message_type.parse(metadata, payload)
18
+ else
19
+ Message.new
20
+ end
21
+ end
22
+
23
+ included do
24
+ const_set(:Message, Synapses::Messages::Message)
25
+ end
26
+ end
27
+ end
28
+
29
+ require 'synapses/messages/message'
30
+ require 'synapses/messages/coders'
@@ -0,0 +1,34 @@
1
+ require 'synapses/messages/coders'
2
+ require 'multi_json'
3
+
4
+ module Synapses
5
+ module Messages
6
+ # @author Alexander Semyonov <al@semyonov.us>
7
+ module Coders
8
+ module_function
9
+
10
+ class << self
11
+ attr_accessor :coders
12
+ end
13
+ self.coders = {}
14
+
15
+ if defined?(::MultiJson)
16
+ coders['application/json'] = MultiJson
17
+ end
18
+
19
+ # @param [String] payload
20
+ # @param [String] content_type
21
+ # @return [Hash] +payload+ decoded from +content_type+
22
+ def decode(payload, content_type)
23
+ coders[content_type.to_s].decode(payload)
24
+ end
25
+
26
+ # @param [Hash] payload
27
+ # @param [String] content_type
28
+ # @return [String] +payload+ encoded as +content_type+
29
+ def encode(payload, content_type)
30
+ coders[content_type.to_s].encode(payload)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,99 @@
1
+ require 'synapses/messages'
2
+ require 'synapses/messages/coders'
3
+
4
+ module Synapses
5
+ module Messages
6
+ # @author Alexander Semyonov <al@semyonov.us>
7
+ class Message
8
+ # @return [String] (nil)
9
+ class_attribute :routing_key
10
+
11
+ class << self
12
+ attr_reader :message_type
13
+ end
14
+
15
+ def self.message_type=(type)
16
+ Messages.registry[type] = self
17
+ @message_type = type
18
+ end
19
+
20
+ # @return [Boolean] (false)
21
+ class_attribute :mandatory
22
+ self.mandatory = false
23
+
24
+ # @return [Boolean] (false)
25
+ class_attribute :immediate
26
+ self.immediate = false
27
+
28
+ # @return [Boolean] (false)
29
+ class_attribute :persistent
30
+ self.persistent = false
31
+
32
+ # @return [String]
33
+ class_attribute :content_type
34
+ #self.content_type = 'application/octet-stream'
35
+ self.content_type = 'application/json'
36
+
37
+ class_attribute :attributes
38
+ self.attributes = {}
39
+
40
+ def self.inherited(child)
41
+ super
42
+ child.attributes = attributes.dup
43
+ end
44
+
45
+ def self.attribute(attr, options = {})
46
+ self.attributes[attr.to_s] = options # TODO add types
47
+ attr_accessor attr
48
+ end
49
+
50
+ def self.parse(metadata, payload)
51
+ new(Synapses::Messages::Coders.decode(payload, metadata.content_type).merge(metadata: metadata))
52
+ end
53
+
54
+ def initialize(attributes = {}, metadata = {})
55
+ @attributes = {}
56
+ attributes.each do |name, value|
57
+ if respond_to?((writer = "#{name}="))
58
+ send(writer, value)
59
+ else
60
+ @attributes[name] = value
61
+ end
62
+ end
63
+ metadata.assert_valid_keys(:routing_key, :type)
64
+ end
65
+
66
+ attr_accessor :metadata
67
+
68
+ def attributes
69
+ self.class.attributes.inject({}) do |attributes, (name, type)|
70
+ attributes[name] = public_send(name)
71
+ attributes
72
+ end.merge(@attributes)
73
+ end
74
+
75
+ def to_payload
76
+ Synapses::Messages::Coders.encode(attributes, content_type)
77
+ end
78
+
79
+ def message_type
80
+ @message_type || self.class.message_type
81
+ end
82
+ attr_writer :message_type
83
+
84
+ alias type message_type
85
+ alias type= message_type=
86
+
87
+ def options
88
+ {
89
+ routing_key: routing_key,
90
+ type: message_type,
91
+ mandatory: mandatory,
92
+ immediate: immediate,
93
+ persistent: persistent,
94
+ content_type: content_type
95
+ }
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+
3
+ ################################################
4
+ # © Alexander Semyonov, 2013—2013 #
5
+ # Author: Alexander Semyonov <al@semyonov.us> #
6
+ ################################################
7
+
8
+ require 'synapses'
9
+ require 'synapses/contract/definitions'
10
+
11
+ module Synapses
12
+ # @author Alexander Semyonov <al@semyonov.us>
13
+ class Producer
14
+ include Contract::Definitions
15
+
16
+ def initialize(channel = nil)
17
+ @channel = channel
18
+ end
19
+
20
+ def <<(message)
21
+ EventMachine.next_tick do
22
+ exchange.publish(message.to_payload, message.options) do
23
+ puts "published [#{message.to_payload}, #{message.options}]"
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,10 @@
1
+ # coding: utf-8
2
+
3
+ ################################################
4
+ # © Alexander Semyonov, 2013—2013 #
5
+ # Author: Alexander Semyonov <al@semyonov.us> #
6
+ ################################################
7
+
8
+ module Synapses
9
+ VERSION = '0.0.1'
10
+ end
@@ -0,0 +1,10 @@
1
+ exchanges:
2
+ direct:
3
+ type: direct
4
+ fanout:
5
+ type: fanout
6
+ weather:
7
+ type: topic
8
+ headers:
9
+ type: header
10
+ queues:
@@ -0,0 +1,17 @@
1
+ # This file was generated by the `rspec --init` command. Conventionally, all
2
+ # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
3
+ # Require this file using `require "spec_helper"` to ensure that it is only
4
+ # loaded once.
5
+ #
6
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
7
+ RSpec.configure do |config|
8
+ config.treat_symbols_as_metadata_keys_with_true_values = true
9
+ config.run_all_when_everything_filtered = true
10
+ config.filter_run :focus
11
+
12
+ # Run specs in random order to surface order dependencies. If you find an
13
+ # order dependency and want to debug it, you can fix the order by providing
14
+ # the seed, which is printed after each run.
15
+ # --seed 1234
16
+ config.order = 'random'
17
+ end
@@ -0,0 +1,30 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'synapses/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'synapses'
8
+ spec.version = Synapses::VERSION
9
+ spec.authors = ['Alexander Semyonov']
10
+ spec.email = %w(al@semyonov.us)
11
+ spec.description = %q{MQ-based application communication and event processing}
12
+ spec.summary = %q{Synapses connecting your applications}
13
+ spec.homepage = 'https://github.com/alsemyonov/synapses'
14
+ spec.license = 'MIT'
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = %w(lib)
20
+
21
+ spec.add_runtime_dependency 'eventmachine'
22
+ spec.add_runtime_dependency 'amqp'
23
+ spec.add_runtime_dependency 'activesupport'
24
+
25
+ spec.add_development_dependency 'bundler', '~> 1.3'
26
+ spec.add_development_dependency 'rake'
27
+ spec.add_development_dependency 'rspec'
28
+ spec.add_development_dependency 'yard'
29
+ spec.add_development_dependency 'evented-spec'
30
+ end
metadata ADDED
@@ -0,0 +1,190 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: synapses
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Alexander Semyonov
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2013-09-19 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: eventmachine
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '>='
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '>='
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: amqp
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: activesupport
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: bundler
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: '1.3'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ~>
67
+ - !ruby/object:Gem::Version
68
+ version: '1.3'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rake
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - '>='
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - '>='
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rspec
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - '>='
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - '>='
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: yard
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - '>='
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - '>='
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: evented-spec
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - '>='
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - '>='
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ description: MQ-based application communication and event processing
126
+ email:
127
+ - al@semyonov.us
128
+ executables: []
129
+ extensions: []
130
+ extra_rdoc_files: []
131
+ files:
132
+ - .gitignore
133
+ - .rspec
134
+ - Gemfile
135
+ - LICENSE.txt
136
+ - README.md
137
+ - Rakefile
138
+ - config/amqp.yml
139
+ - config/giteaucrat.yml
140
+ - examples/config/amqp.yml
141
+ - examples/config/synapses/radar.yml
142
+ - examples/config/synapses/rocket_center.yml
143
+ - examples/game.rb
144
+ - lib/amqp/integration/rails.rb
145
+ - lib/synapses.rb
146
+ - lib/synapses/consumer.rb
147
+ - lib/synapses/contract.rb
148
+ - lib/synapses/contract/connectible.rb
149
+ - lib/synapses/contract/definitions.rb
150
+ - lib/synapses/contract/exchange.rb
151
+ - lib/synapses/contract/queue.rb
152
+ - lib/synapses/contract/synapses.yml
153
+ - lib/synapses/engine.rb
154
+ - lib/synapses/manager.rb
155
+ - lib/synapses/messages.rb
156
+ - lib/synapses/messages/coders.rb
157
+ - lib/synapses/messages/message.rb
158
+ - lib/synapses/producer.rb
159
+ - lib/synapses/version.rb
160
+ - spec/examples/contract.yml
161
+ - spec/spec_helper.rb
162
+ - synapses.gemspec
163
+ homepage: https://github.com/alsemyonov/synapses
164
+ licenses:
165
+ - MIT
166
+ metadata: {}
167
+ post_install_message:
168
+ rdoc_options: []
169
+ require_paths:
170
+ - lib
171
+ required_ruby_version: !ruby/object:Gem::Requirement
172
+ requirements:
173
+ - - '>='
174
+ - !ruby/object:Gem::Version
175
+ version: '0'
176
+ required_rubygems_version: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - '>='
179
+ - !ruby/object:Gem::Version
180
+ version: '0'
181
+ requirements: []
182
+ rubyforge_project:
183
+ rubygems_version: 2.0.7
184
+ signing_key:
185
+ specification_version: 4
186
+ summary: Synapses connecting your applications
187
+ test_files:
188
+ - spec/examples/contract.yml
189
+ - spec/spec_helper.rb
190
+ has_rdoc: