synapses 0.0.1

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.
@@ -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: