amqp-client 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 5cf775bb44cd9021dec5416472d52dec17a5e0ae847de7d56ea0d3acd1e27b11
4
+ data.tar.gz: 589416e377d8d3bcc448991664666aab664b7dd194fb8990e0294251439d0838
5
+ SHA512:
6
+ metadata.gz: c3c5672321ffadc0966a3dd549697699be445a58db1448ac2e2cb2f06ad4a9b456036b64bfd52a1c35a56dd88301b36dea4cd639ca2d90155ead57d40b9ca16b
7
+ data.tar.gz: b23676715032b2fb4e909a550e6674155e1ef2be9fae08e543db6811b7f8a456c35ac09d57b0a0fa0c356e9eea954614221c33c27898beb30acd36f5b2c8e41d
@@ -0,0 +1,18 @@
1
+ name: Ruby
2
+
3
+ on: [push,pull_request]
4
+
5
+ jobs:
6
+ build:
7
+ runs-on: ubuntu-latest
8
+ steps:
9
+ - uses: actions/checkout@v2
10
+ - name: Set up Ruby
11
+ uses: ruby/setup-ruby@v1
12
+ with:
13
+ ruby-version: 3.0.0
14
+ - name: Run the default task
15
+ run: |
16
+ gem install bundler -v 2.2.15
17
+ bundle install
18
+ bundle exec rake
data/.gitignore ADDED
@@ -0,0 +1,8 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
data/.rubocop.yml ADDED
@@ -0,0 +1,15 @@
1
+ inherit_from: .rubocop_todo.yml
2
+
3
+ AllCops:
4
+ TargetRubyVersion: 2.5
5
+
6
+ Style/StringLiterals:
7
+ Enabled: true
8
+ EnforcedStyle: double_quotes
9
+
10
+ Style/StringLiteralsInInterpolation:
11
+ Enabled: true
12
+ EnforcedStyle: double_quotes
13
+
14
+ Layout/LineLength:
15
+ Max: 120
data/.rubocop_todo.yml ADDED
@@ -0,0 +1,86 @@
1
+ # This configuration was generated by
2
+ # `rubocop --auto-gen-config`
3
+ # on 2021-04-29 10:30:28 UTC using RuboCop version 1.12.1.
4
+ # The point is for the user to remove these configuration records
5
+ # one by one as the offenses are removed from the code base.
6
+ # Note that changes in the inspected code, or installation of new
7
+ # versions of RuboCop, may require this file to be generated again.
8
+
9
+ # Offense count: 1
10
+ # Cop supports --auto-correct.
11
+ # Configuration parameters: AutoCorrect, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
12
+ # URISchemes: http, https
13
+ Layout/LineLength:
14
+ Max: 129
15
+
16
+ # Offense count: 2
17
+ Lint/ShadowedException:
18
+ Exclude:
19
+ - 'lib/amqp/client.rb'
20
+ - 'lib/amqp/client/connection.rb'
21
+
22
+ # Offense count: 2
23
+ # Cop supports --auto-correct.
24
+ # Configuration parameters: AllowUnusedKeywordArguments, IgnoreEmptyMethods, IgnoreNotImplementedMethods.
25
+ Lint/UnusedMethodArgument:
26
+ Exclude:
27
+ - 'lib/amqp/client/channel.rb'
28
+
29
+ # Offense count: 6
30
+ # Configuration parameters: IgnoredMethods, CountRepeatedAttributes.
31
+ Metrics/AbcSize:
32
+ Max: 52
33
+
34
+ # Offense count: 1
35
+ # Configuration parameters: CountComments, CountAsOne, ExcludedMethods, IgnoredMethods.
36
+ # IgnoredMethods: refine
37
+ Metrics/BlockLength:
38
+ Max: 76
39
+
40
+ # Offense count: 3
41
+ # Configuration parameters: CountComments, CountAsOne.
42
+ Metrics/ClassLength:
43
+ Max: 148
44
+
45
+ # Offense count: 5
46
+ # Configuration parameters: IgnoredMethods.
47
+ Metrics/CyclomaticComplexity:
48
+ Max: 18
49
+
50
+ # Offense count: 9
51
+ # Configuration parameters: CountComments, CountAsOne, ExcludedMethods, IgnoredMethods.
52
+ Metrics/MethodLength:
53
+ Max: 83
54
+
55
+ # Offense count: 1
56
+ # Configuration parameters: CountKeywordArgs, MaxOptionalParameters.
57
+ Metrics/ParameterLists:
58
+ Max: 6
59
+
60
+ # Offense count: 3
61
+ # Configuration parameters: IgnoredMethods.
62
+ Metrics/PerceivedComplexity:
63
+ Max: 12
64
+
65
+ # Offense count: 1
66
+ # Cop supports --auto-correct.
67
+ # Configuration parameters: EnforcedStyle.
68
+ # SupportedStyles: always, always_true, never
69
+ Style/FrozenStringLiteralComment:
70
+ Exclude:
71
+ - 'bm_append_string.rb'
72
+
73
+ # Offense count: 1
74
+ # Cop supports --auto-correct.
75
+ # Configuration parameters: EnforcedStyle.
76
+ # SupportedStyles: implicit, explicit
77
+ Style/RescueStandardError:
78
+ Exclude:
79
+ - 'lib/amqp/client.rb'
80
+
81
+ # Offense count: 1
82
+ # Cop supports --auto-correct.
83
+ # Configuration parameters: AutoCorrect, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
84
+ # URISchemes: http, https
85
+ Layout/LineLength:
86
+ Max: 129
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2021-04-13
4
+
5
+ - Initial release
data/Gemfile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in amqp-client.gemspec
6
+ gemspec
7
+
8
+ gem "rake", "~> 13.0"
9
+
10
+ gem "minitest", "~> 5.0"
11
+
12
+ gem "rubocop", "~> 1.7"
data/Gemfile.lock ADDED
@@ -0,0 +1,42 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ amqp-client (0.1.0)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ ast (2.4.2)
10
+ minitest (5.14.2)
11
+ parallel (1.20.1)
12
+ parser (3.0.1.0)
13
+ ast (~> 2.4.1)
14
+ rainbow (3.0.0)
15
+ rake (13.0.3)
16
+ regexp_parser (2.1.1)
17
+ rexml (3.2.4)
18
+ rubocop (1.12.1)
19
+ parallel (~> 1.10)
20
+ parser (>= 3.0.0.0)
21
+ rainbow (>= 2.2.2, < 4.0)
22
+ regexp_parser (>= 1.8, < 3.0)
23
+ rexml
24
+ rubocop-ast (>= 1.2.0, < 2.0)
25
+ ruby-progressbar (~> 1.7)
26
+ unicode-display_width (>= 1.4.0, < 3.0)
27
+ rubocop-ast (1.4.1)
28
+ parser (>= 2.7.1.5)
29
+ ruby-progressbar (1.11.0)
30
+ unicode-display_width (2.0.0)
31
+
32
+ PLATFORMS
33
+ x86_64-linux
34
+
35
+ DEPENDENCIES
36
+ amqp-client!
37
+ minitest (~> 5.0)
38
+ rake (~> 13.0)
39
+ rubocop (~> 1.7)
40
+
41
+ BUNDLED WITH
42
+ 2.2.15
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2021 Carl Hörberg
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,47 @@
1
+ # AMQP::Client
2
+
3
+ An AMQP 0-9-1 client alternative, trying to keep things as simple as possible.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'amqp-client'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle install
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install amqp-client
20
+
21
+ ## Usage
22
+
23
+ ```ruby
24
+ require "amqp-client"
25
+
26
+ c = AMQP::Client.new("amqp://guest:guest@localhost")
27
+ conn = c.connect
28
+ ch = conn.channel
29
+ q = ch.queue_declare
30
+ ch.basic_publish "Hello World!", "", q[:queue_name]
31
+ msg = ch.basic_get q[:queue_name]
32
+ puts msg.body
33
+ ```
34
+
35
+ ## Development
36
+
37
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
38
+
39
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
40
+
41
+ ## Contributing
42
+
43
+ Bug reports and pull requests are welcome on GitHub at https://github.com/cloudamqp/amqp-client.rb
44
+
45
+ ## License
46
+
47
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << "test"
8
+ t.libs << "lib"
9
+ t.test_files = FileList["test/**/*_test.rb"]
10
+ end
11
+
12
+ require "rubocop/rake_task"
13
+
14
+ RuboCop::RakeTask.new
15
+
16
+ task default: %i[test rubocop]
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/amqp/client/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "amqp-client"
7
+ spec.version = AMQP::Client::VERSION
8
+ spec.authors = ["Carl Hörberg"]
9
+ spec.email = ["carl@cloudamqp.com"]
10
+
11
+ spec.summary = "AMQP 0-9-1 client"
12
+ spec.description = "Work in progress"
13
+ spec.homepage = "https://github.com/cloudamqp/amqp-client.rb"
14
+ spec.license = "MIT"
15
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.5.0")
16
+
17
+ spec.metadata["homepage_uri"] = spec.homepage
18
+ spec.metadata["source_code_uri"] = "#{spec.homepage}.git"
19
+ spec.metadata["changelog_uri"] = "https://github.com/cloudamqp/amqp-client.rb/blob/main/CHANGELOG.md"
20
+
21
+ # Specify which files should be added to the gem when it is released.
22
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
23
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
24
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
25
+ end
26
+ spec.bindir = "exe"
27
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
28
+ spec.require_paths = ["lib"]
29
+
30
+ # Uncomment to register a new dependency of your gem
31
+ # spec.add_dependency "example-gem", "~> 1.0"
32
+
33
+ # For more information and examples about making a new gem, checkout our
34
+ # guide at: https://bundler.io/guides/creating_gem.html
35
+ end
data/bin/console ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "amqp/client"
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require "irb"
15
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "socket"
4
+ require "uri"
5
+ require "openssl"
6
+ require_relative "client/version"
7
+ require_relative "client/errors"
8
+ require_relative "client/frames"
9
+ require_relative "client/connection"
10
+ require_relative "client/channel"
11
+
12
+ module AMQP
13
+ # AMQP 0-9-1 Client
14
+ class Client
15
+ def initialize(uri)
16
+ @uri = URI.parse(uri)
17
+ @tls = @uri.scheme == "amqps"
18
+ @port = @uri.port || @tls ? 5671 : 5672
19
+ @host = @uri.host || "localhost"
20
+ @user = @uri.user || "guest"
21
+ @password = @uri.password || "guest"
22
+ @vhost = URI.decode_www_form_component(@uri.path[1..-1] || "/")
23
+ @options = URI.decode_www_form(@uri.query || "").to_h
24
+ end
25
+
26
+ def connect
27
+ socket = Socket.tcp @host, @port, connect_timeout: 20, resolv_timeout: 5
28
+ enable_tcp_keepalive(socket)
29
+ if @tls
30
+ context = OpenSSL::SSL::SSLContext.new
31
+ context.verify_mode = OpenSSL::SSL::VERIFY_PEER unless @options["verify_peer"] == "none"
32
+ socket = OpenSSL::SSL::SSLSocket.new(socket, context)
33
+ socket.sync_close = true # closing the TLS socket also closes the TCP socket
34
+ end
35
+ channel_max, frame_max, heartbeat = establish(socket)
36
+ Connection.new(socket, channel_max, frame_max, heartbeat)
37
+ end
38
+
39
+ private
40
+
41
+ def establish(socket)
42
+ channel_max, frame_max, heartbeat = nil
43
+ socket.write "AMQP\x00\x00\x09\x01"
44
+ buf = String.new(capacity: 4096)
45
+ loop do
46
+ begin
47
+ socket.readpartial(4096, buf)
48
+ rescue EOFError, IOError, OpenSSL::Error => e
49
+ raise Error, "Could not establish AMQP connection: #{e.message}"
50
+ end
51
+
52
+ type, channel_id, frame_size = buf.unpack("C S> L>")
53
+ frame_end = buf.unpack1("@#{frame_size + 7} C")
54
+ raise UnexpectedFrameEndError, frame_end if frame_end != 206
55
+
56
+ case type
57
+ when 1 # method frame
58
+ class_id, method_id = buf.unpack("@7 S> S>")
59
+ case class_id
60
+ when 10 # connection
61
+ raise Error, "Unexpected channel id #{channel_id} for Connection frame" if channel_id != 0
62
+
63
+ case method_id
64
+ when 10 # connection#start
65
+ socket.write FrameBytes.connection_start_ok "\u0000#{@user}\u0000#{@password}"
66
+ when 30 # connection#tune
67
+ channel_max, frame_max, heartbeat = buf.unpack("@11 S> L> S>")
68
+ channel_max = [channel_max, 2048].min
69
+ frame_max = [frame_max, 4096].min
70
+ heartbeat = [heartbeat, 0].min
71
+ socket.write FrameBytes.connection_tune_ok(channel_max, frame_max, heartbeat)
72
+ socket.write FrameBytes.connection_open(@vhost)
73
+ when 41 # connection#open-ok
74
+ return [channel_max, frame_max, heartbeat]
75
+ when 50 # connection#close
76
+ code, text_len = buf.unpack("@11 S> C")
77
+ text, error_class_id, error_method_id = buf.unpack("@14 a#{text_len} S> S>")
78
+ socket.write FrameBytes.connection_close_ok
79
+ raise Error, "Could not establish AMQP connection: #{code} #{text} #{error_class_id} #{error_method_id}"
80
+ else raise Error, "Unexpected class/method: #{class_id} #{method_id}"
81
+ end
82
+ else raise Error, "Unexpected class/method: #{class_id} #{method_id}"
83
+ end
84
+ else raise Error, "Unexpected frame type: #{type}"
85
+ end
86
+ end
87
+ rescue StandardError
88
+ socket.close
89
+ raise
90
+ end
91
+
92
+ def enable_tcp_keepalive(socket)
93
+ socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true)
94
+ socket.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPIDLE, 60)
95
+ socket.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPINTVL, 10)
96
+ socket.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPCNT, 3)
97
+ rescue => e
98
+ warn "amqp-client: Could not enable TCP keepalive on socket. #{e.inspect}"
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "./message"
4
+
5
+ module AMQP
6
+ # AMQP Channel
7
+ class Channel
8
+ def initialize(connection, id)
9
+ @rpc = Queue.new
10
+ @connection = connection
11
+ @id = id
12
+ @closed = false
13
+ end
14
+
15
+ attr_reader :id
16
+
17
+ def open
18
+ write_bytes FrameBytes.channel_open(@id)
19
+ expect(:channel_open_ok)
20
+ self
21
+ end
22
+
23
+ def close(reason = "", code = 200)
24
+ return if @closed
25
+
26
+ write_bytes FrameBytes.channel_close(@id, reason, code)
27
+ expect :channel_close_ok
28
+ @closed = true
29
+ end
30
+
31
+ def queue_declare(name = "", passive: false, durable: true, exclusive: false, auto_delete: false, **args)
32
+ durable = false if name.empty?
33
+ exclusive = true if name.empty?
34
+ auto_delete = true if name.empty?
35
+
36
+ write_bytes FrameBytes.queue_declare(@id, name, passive, durable, exclusive, auto_delete)
37
+ name, message_count, consumer_count = expect(:queue_declare_ok)
38
+ {
39
+ queue_name: name,
40
+ message_count: message_count,
41
+ consumer_count: consumer_count
42
+ }
43
+ end
44
+
45
+ def basic_get(queue_name, no_ack: true)
46
+ return if @closed
47
+
48
+ write_bytes FrameBytes.basic_get(@id, queue_name, no_ack)
49
+ resp = @rpc.shift
50
+ frame, = resp
51
+ case frame
52
+ when :basic_get_ok
53
+ _, exchange_name, routing_key, redelivered = resp
54
+ body_size, properties = expect(:header)
55
+ pos = 0
56
+ body = ""
57
+ while pos < body_size
58
+ body_part = expect(:body)
59
+ body += body_part
60
+ pos += body_part.bytesize
61
+ end
62
+ Message.new(exchange_name, routing_key, properties, body, redelivered)
63
+ when :basic_get_empty
64
+ nil
65
+ else raise AMQP::Client::UnexpectedFrame, %i[basic_get_ok basic_get_empty], frame
66
+ end
67
+ end
68
+
69
+ def basic_publish(exchange, routing_key, body, properties = {})
70
+ raise AMQP::Client::ChannelClosedError, @id if @closed
71
+
72
+ write_bytes FrameBytes.basic_publish(@id, exchange, routing_key)
73
+ write_bytes FrameBytes.header(@id, body.bytesize, properties)
74
+
75
+ # body frames, splitted on frame size
76
+ pos = 0
77
+ while pos < body.bytesize
78
+ len = [4096, body.bytesize - pos].min
79
+ body_part = body.byteslice(pos, len)
80
+ write_bytes FrameBytes.body(@id, body_part)
81
+ pos += len
82
+ end
83
+ end
84
+
85
+ def push(*args)
86
+ @rpc.push(*args)
87
+ end
88
+
89
+ private
90
+
91
+ def write_bytes(bytes)
92
+ @connection.write_bytes bytes
93
+ end
94
+
95
+ def expect(expected_frame_type)
96
+ frame_type, args = @rpc.shift
97
+ frame_type == expected_frame_type || raise(UnexpectedFrame, expected_frame_type, frame_type)
98
+ args
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AMQP
4
+ # AMQP Connection
5
+ class Connection
6
+ def initialize(socket, channel_max, frame_max, heartbeat)
7
+ @socket = socket
8
+ @channel_max = channel_max
9
+ @frame_max = frame_max
10
+ @heartbeat = heartbeat
11
+ @channels = {}
12
+ @closed = false
13
+ @rpc = Queue.new
14
+ Thread.new { read_loop }
15
+ end
16
+
17
+ def channel
18
+ id = 1.upto(@channel_max) { |i| break i unless @channels.key? i }
19
+ ch = Channel.new(self, id)
20
+ @channels[id] = ch
21
+ ch.open
22
+ end
23
+
24
+ def close(reason = "", code = 200)
25
+ write_bytes FrameBytes.connection_close(code, reason)
26
+ expect(:close_ok)
27
+ @closed = true
28
+ end
29
+
30
+ def write_bytes(bytes)
31
+ @socket.write bytes
32
+ end
33
+
34
+ private
35
+
36
+ def read_loop
37
+ socket = @socket
38
+ frame_max = @frame_max
39
+ buffer = String.new(capacity: frame_max)
40
+ loop do
41
+ begin
42
+ socket.readpartial(frame_max, buffer)
43
+ rescue IOError, EOFError
44
+ break
45
+ end
46
+
47
+ buf_pos = 0
48
+ while buf_pos < buffer.bytesize
49
+ type, channel_id, frame_size = buffer.unpack("@#{buf_pos}C S> L>")
50
+ frame_end = buffer.unpack1("@#{buf_pos + 7 + frame_size} C")
51
+ raise AMQP::Client::UnexpectedFrameEnd if frame_end != 206
52
+
53
+ buf = buffer.byteslice(buf_pos, frame_size + 8)
54
+ buf_pos += frame_size + 8
55
+ parse_frame(type, channel_id, frame_size, buf) || return
56
+ end
57
+ end
58
+ ensure
59
+ @closed = true
60
+ begin
61
+ @socket.close
62
+ rescue IOError
63
+ nil
64
+ end
65
+ end
66
+
67
+ def parse_frame(type, channel_id, frame_size, buf)
68
+ case type
69
+ when 1 # method frame
70
+ class_id, method_id = buf.unpack("@7 S> S>")
71
+ case class_id
72
+ when 10 # connection
73
+ raise AMQP::Client::Error, "Unexpected channel id #{channel_id} for Connection frame" if channel_id != 0
74
+
75
+ case method_id
76
+ when 50 # connection#close
77
+ code, text_len = buf.unpack("@11 S> C")
78
+ text, error_class_id, error_method_id = buf.unpack("@14 a#{text_len} S> S>")
79
+ warn "Connection closed #{code} #{text} #{error_class_id} #{error_method_id}"
80
+ write_bytes FrameBytes.connection_close_ok
81
+ return false
82
+ when 51 # connection#close-ok
83
+ @rpc.push [:close_ok]
84
+ return false
85
+ else raise AMQP::Client::UnsupportedMethodFrame, class_id, method_id
86
+ end
87
+ when 20 # channel
88
+ case method_id
89
+ when 11 # channel#open-ok
90
+ @channels[channel_id].push [:channel_open_ok]
91
+ when 40 # channel#close
92
+ channel = @channels.delete(channel_id)
93
+ channel&.closed!
94
+ when 41 # channel#close-ok
95
+ @channels[channel_id].push [:channel_close_ok]
96
+ else raise AMQP::Client::UnsupportedMethodFrame, class_id, method_id
97
+ end
98
+ when 50 # queue
99
+ case method_id
100
+ when 11 # queue#declare-ok
101
+ queue_name_len = buf.unpack1("@11 C")
102
+ queue_name, message_count, consumer_count = buf.unpack("@12 a#{queue_name_len} L> L>")
103
+ @channels[channel_id].push [:queue_declare_ok, queue_name, message_count, consumer_count]
104
+ else raise AMQP::Client::UnsupportedMethodFrame, class_id, method_id
105
+ end
106
+ when 60 # basic
107
+ case method_id
108
+ when 71 # get-ok
109
+ delivery_tag, redelivered, exchange_name_len = buf.unpack("@11 Q> C C")
110
+ exchange_name = buf.byteslice(21, exchange_name_len)
111
+ pos = 21 + exchange_name_len
112
+ routing_key_len = buf.unpack1("@#{pos} C")
113
+ pos += 1
114
+ routing_key = buf.byteslice(pos, routing_key_len)
115
+ pos += routing_key_len
116
+ message_count = buf.unpack1("@#{pos} L>")
117
+ @channels[channel_id].push [:basic_get_ok, delivery_tag, exchange_name, routing_key, message_count, redelivered == 1]
118
+ when 72 # get-empty
119
+ @channels[channel_id].push [:basic_get_empty]
120
+ else raise AMQP::Client::UnsupportedMethodFrame, class_id, method_id
121
+ end
122
+ else raise AMQP::Client::UnsupportedMethodFrame, class_id, method_id
123
+ end
124
+ when 2 # header
125
+ body_size = buf.unpack1("@11 Q>")
126
+ @channels[channel_id].push [:header, body_size, nil]
127
+ when 3 # body
128
+ body = buf.byteslice(7, frame_size)
129
+ @channels[channel_id].push [:body, body]
130
+ else raise AMQP::Client::UnsupportedFrameType, type
131
+ end
132
+ true
133
+ end
134
+
135
+ def expect(expected_frame_type)
136
+ frame_type, args = @rpc.shift
137
+ frame_type == expected_frame_type || raise(UnexpectedFrame, expected_frame_type, frame_type)
138
+ args
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,37 @@
1
+ module AMQP
2
+ class Client
3
+ class Error < StandardError; end
4
+
5
+ # Raised when a frame that wasn't expected arrives
6
+ class UnexpectedFrame < Error
7
+ def initialize(expected, actual)
8
+ super "Expected frame type '#{expected}' but got '#{actual}'"
9
+ end
10
+ end
11
+
12
+ # Raised when a frame doesn't end with 206
13
+ class UnexpectedFrameEnd < Error
14
+ def initialize(actual)
15
+ super "Expected frame end 206 but got '#{actual}'"
16
+ end
17
+ end
18
+
19
+ class UnsupportedFrameType < Error
20
+ def initialize(type)
21
+ super "Unsupported frame type '#{type}'"
22
+ end
23
+ end
24
+
25
+ class UnsupportedMethodFrame < Error
26
+ def initialize(class_id, method_id)
27
+ super "Unsupported class/method: #{class_id} #{method_id}"
28
+ end
29
+ end
30
+
31
+ class ChannelClosedError < Error
32
+ def initialize(id)
33
+ super "Channel #{id} already closed"
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,186 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AMQP
4
+ # Generate binary data for different frames
5
+ # Each frame type implemented as a method
6
+ # Having a class for each frame type is more expensive in terms of CPU and memory
7
+ module FrameBytes
8
+ module_function
9
+
10
+ def connection_start_ok(response)
11
+ [
12
+ 1, # type: method
13
+ 0, # channel id
14
+ 4 + 4 + 6 + 4 + response.bytesize + 1, # frame size
15
+ 10, # class id
16
+ 11, # method id
17
+ 0, # client props
18
+ 5, "PLAIN", # mechanism
19
+ response.bytesize, response,
20
+ 0, "", # locale
21
+ 206 # frame end
22
+ ].pack("C S> L> S> S> L> Ca* L>a* Ca* C")
23
+ end
24
+
25
+ def connection_tune_ok(channel_max, frame_max, heartbeat)
26
+ [
27
+ 1, # type: method
28
+ 0, # channel id
29
+ 12, # frame size
30
+ 10, # class: connection
31
+ 31, # method: tune-ok
32
+ channel_max,
33
+ frame_max,
34
+ heartbeat,
35
+ 206 # frame end
36
+ ].pack("CS>L>S>S>S>L>S>C")
37
+ end
38
+
39
+ def connection_open(vhost)
40
+ [
41
+ 1, # type: method
42
+ 0, # channel id
43
+ 2 + 2 + 1 + vhost.bytesize + 1 + 1, # frame_size
44
+ 10, # class: connection
45
+ 40, # method: open
46
+ vhost.bytesize, vhost,
47
+ 0, # reserved1
48
+ 0, # reserved2
49
+ 206 # frame end
50
+ ].pack("C S> L> S> S> Ca* CCC")
51
+ end
52
+
53
+ def connection_close(code, reason)
54
+ frame_size = 2 + 2 + 2 + 1 + reason.bytesize + 2 + 2
55
+ [
56
+ 1, # type: method
57
+ 0, # channel id
58
+ frame_size, # frame size
59
+ 10, # class: connection
60
+ 50, # method: close
61
+ code,
62
+ reason.bytesize, reason,
63
+ 0, # error class id
64
+ 0, # error method id
65
+ 206 # frame end
66
+ ].pack("C S> L> S> S> S> Ca* S> S> C")
67
+ end
68
+
69
+ def connection_close_ok
70
+ [
71
+ 1, # type: method
72
+ 0, # channel id
73
+ 4, # frame size
74
+ 10, # class: connection
75
+ 51, # method: close-ok
76
+ 206 # frame end
77
+ ].pack("C S> L> S> S> C")
78
+ end
79
+
80
+ def channel_open(id)
81
+ [
82
+ 1, # type: method
83
+ id, # channel id
84
+ 5, # frame size
85
+ 20, # class: channel
86
+ 10, # method: open
87
+ 0, # reserved1
88
+ 206 # frame end
89
+ ].pack("C S> L> S> S> C C")
90
+ end
91
+
92
+ def channel_close(id, reason, code)
93
+ frame_size = 2 + 2 + 2 + 1 + reason.bytesize + 2 + 2
94
+ [
95
+ 1, # type: method
96
+ id, # channel id
97
+ frame_size, # frame size
98
+ 20, # class: channel
99
+ 40, # method: close
100
+ code,
101
+ reason.bytesize, reason,
102
+ 0, # error class id
103
+ 0, # error method id
104
+ 206 # frame end
105
+ ].pack("C S> L> S> S> S> Ca* S> S> C")
106
+ end
107
+
108
+ def queue_declare(id, name, passive, durable, exclusive, auto_delete)
109
+ no_wait = false
110
+ bits = 0
111
+ bits |= (1 << 0) if passive
112
+ bits |= (1 << 1) if durable
113
+ bits |= (1 << 2) if exclusive
114
+ bits |= (1 << 3) if auto_delete
115
+ bits |= (1 << 4) if no_wait
116
+ frame_size = 2 + 2 + 2 + 1 + name.bytesize + 1 + 4
117
+ [
118
+ 1, # type: method
119
+ id, # channel id
120
+ frame_size, # frame size
121
+ 50, # class: queue
122
+ 10, # method: declare
123
+ 0, # reserved1
124
+ name.bytesize, name,
125
+ bits,
126
+ 0, # arguments
127
+ 206 # frame end
128
+ ].pack("C S> L> S> S> S> Ca* C L> C")
129
+ end
130
+
131
+ def basic_get(id, queue_name, no_ack)
132
+ frame_size = 2 + 2 + 2 + 1 + queue_name.bytesize + 2 + 2
133
+ [
134
+ 1, # type: method
135
+ id, # channel id
136
+ frame_size, # frame size
137
+ 60, # class: basic
138
+ 70, # method: get
139
+ 0, # reserved1
140
+ queue_name.bytesize, queue_name,
141
+ no_ack ? 1 : 0,
142
+ 206 # frame end
143
+ ].pack("C S> L> S> S> S> Ca* C C")
144
+ end
145
+
146
+ def basic_publish(id, exchange, routing_key)
147
+ frame_size = 2 + 2 + 2 + 1 + exchange.bytesize + 1 + routing_key.bytesize + 1
148
+ [
149
+ 1, # type: method
150
+ id, # channel id
151
+ frame_size, # frame size
152
+ 60, # class: basic
153
+ 40, # method: publish
154
+ 0, # reserved1
155
+ exchange.bytesize, exchange,
156
+ routing_key.bytesize, routing_key,
157
+ 0, # bits, mandatory/immediate
158
+ 206 # frame end
159
+ ].pack("C S> L> S> S> S> Ca* Ca* C C")
160
+ end
161
+
162
+ def header(id, body_size, properties)
163
+ frame_size = 2 + 2 + 8 + 2
164
+ [
165
+ 2, # type: header
166
+ id, # channel id
167
+ frame_size, # frame size
168
+ 60, # class: basic
169
+ 0, # weight
170
+ body_size,
171
+ 0, # properties
172
+ 206 # frame end
173
+ ].pack("C S> L> S> S> Q> S> C")
174
+ end
175
+
176
+ def body(id, body_part)
177
+ [
178
+ 3, # type: body
179
+ id, # channel id
180
+ body_part.bytesize, # frame size
181
+ body_part,
182
+ 206 # frame end
183
+ ].pack("C S> L> a* C")
184
+ end
185
+ end
186
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AMQP
4
+ Message = Struct.new(:exchange_name, :routing_key, :properties, :body, :redelivered)
5
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AMQP
4
+ class Client
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
metadata ADDED
@@ -0,0 +1,66 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: amqp-client
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Carl Hörberg
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2021-05-28 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Work in progress
14
+ email:
15
+ - carl@cloudamqp.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - ".github/workflows/main.yml"
21
+ - ".gitignore"
22
+ - ".rubocop.yml"
23
+ - ".rubocop_todo.yml"
24
+ - CHANGELOG.md
25
+ - Gemfile
26
+ - Gemfile.lock
27
+ - LICENSE.txt
28
+ - README.md
29
+ - Rakefile
30
+ - amqp-client.gemspec
31
+ - bin/console
32
+ - bin/setup
33
+ - lib/amqp/client.rb
34
+ - lib/amqp/client/channel.rb
35
+ - lib/amqp/client/connection.rb
36
+ - lib/amqp/client/errors.rb
37
+ - lib/amqp/client/frames.rb
38
+ - lib/amqp/client/message.rb
39
+ - lib/amqp/client/version.rb
40
+ homepage: https://github.com/cloudamqp/amqp-client.rb
41
+ licenses:
42
+ - MIT
43
+ metadata:
44
+ homepage_uri: https://github.com/cloudamqp/amqp-client.rb
45
+ source_code_uri: https://github.com/cloudamqp/amqp-client.rb.git
46
+ changelog_uri: https://github.com/cloudamqp/amqp-client.rb/blob/main/CHANGELOG.md
47
+ post_install_message:
48
+ rdoc_options: []
49
+ require_paths:
50
+ - lib
51
+ required_ruby_version: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: 2.5.0
56
+ required_rubygems_version: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ requirements: []
62
+ rubygems_version: 3.2.15
63
+ signing_key:
64
+ specification_version: 4
65
+ summary: AMQP 0-9-1 client
66
+ test_files: []