amq-protocol 0.0.1.pre → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +3 -1
- data/.travis.yml +7 -0
- data/CONTRIBUTORS +2 -0
- data/Gemfile +16 -0
- data/Gemfile.lock +31 -0
- data/LICENSE +1 -1
- data/PROFILING.md +81 -0
- data/README.textile +34 -17
- data/TODO.todo +24 -0
- data/__init__.py +0 -0
- data/amq-protocol.gemspec +16 -11
- data/amqp_0.9.1_changes.json +1 -1
- data/bin/jenkins.sh +23 -0
- data/codegen.py +50 -16
- data/codegen_helpers.py +81 -26
- data/examples/00_manual_test.rb +117 -38
- data/irb.rb +76 -4
- data/lib/amq/hacks.rb +33 -0
- data/lib/amq/protocol.rb +3 -1472
- data/lib/amq/protocol/client.rb +2230 -0
- data/lib/amq/protocol/frame.rb +111 -38
- data/lib/amq/protocol/table.rb +89 -42
- data/lib/amq/protocol/version.rb +5 -0
- data/post-processing.rb +1 -0
- data/protocol.rb.pytemplate +217 -63
- data/spec/amq/hacks_spec.rb +39 -0
- data/spec/amq/protocol/basic_spec.rb +320 -0
- data/spec/amq/protocol/channel_spec.rb +117 -0
- data/spec/amq/protocol/confirm_spec.rb +44 -0
- data/spec/amq/protocol/connection_spec.rb +149 -0
- data/spec/amq/protocol/exchange_spec.rb +95 -0
- data/spec/amq/protocol/frame_spec.rb +93 -78
- data/spec/amq/protocol/method_spec.rb +40 -0
- data/spec/amq/protocol/queue_spec.rb +129 -0
- data/spec/amq/protocol/table_spec.rb +59 -36
- data/spec/amq/protocol/tx_spec.rb +58 -0
- data/spec/amq/protocol_spec.rb +572 -643
- data/spec/spec_helper.rb +25 -2
- data/tasks.rb +13 -8
- metadata +44 -25
- data/amq-protocol.pre.gemspec +0 -6
- data/examples/01_basics.rb +0 -14
- data/examples/02_eventmachine.rb +0 -4
data/.gitignore
CHANGED
data/.travis.yml
ADDED
data/CONTRIBUTORS
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
source "http://gemcutter.org"
|
4
|
+
|
5
|
+
group(:development) do
|
6
|
+
gem "nake", :platform => :ruby_19
|
7
|
+
gem "contributors", :platform => :ruby_19
|
8
|
+
|
9
|
+
# excludes Windows, Rubinius and JRuby
|
10
|
+
gem "perftools.rb", :platform => :mri
|
11
|
+
end
|
12
|
+
|
13
|
+
group(:test) do
|
14
|
+
gem "rspec", ">=2.0.0"
|
15
|
+
gem 'simplecov', :platform => :ruby_19
|
16
|
+
end
|
data/Gemfile.lock
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
GEM
|
2
|
+
remote: http://gemcutter.org/
|
3
|
+
specs:
|
4
|
+
contributors (0.1.2)
|
5
|
+
diff-lcs (1.1.2)
|
6
|
+
nake (0.0.8)
|
7
|
+
term-ansicolor
|
8
|
+
perftools.rb (0.5.6)
|
9
|
+
rspec (2.5.0)
|
10
|
+
rspec-core (~> 2.5.0)
|
11
|
+
rspec-expectations (~> 2.5.0)
|
12
|
+
rspec-mocks (~> 2.5.0)
|
13
|
+
rspec-core (2.5.1)
|
14
|
+
rspec-expectations (2.5.0)
|
15
|
+
diff-lcs (~> 1.1.2)
|
16
|
+
rspec-mocks (2.5.0)
|
17
|
+
simplecov (0.4.2)
|
18
|
+
simplecov-html (~> 0.4.4)
|
19
|
+
simplecov-html (0.4.4)
|
20
|
+
term-ansicolor (1.0.5)
|
21
|
+
|
22
|
+
PLATFORMS
|
23
|
+
java
|
24
|
+
ruby
|
25
|
+
|
26
|
+
DEPENDENCIES
|
27
|
+
contributors
|
28
|
+
nake
|
29
|
+
perftools.rb
|
30
|
+
rspec (>= 2.0.0)
|
31
|
+
simplecov
|
data/LICENSE
CHANGED
data/PROFILING.md
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
# Profiling
|
2
|
+
|
3
|
+
To profile the encoding and decoding code use Aman Gupta's perftools.rb (https://github.com/tmm1/perftools.rb). It's really easy to use once you've set it up, and it generates graphs that make it pretty obvious where the hotspots are.
|
4
|
+
|
5
|
+
## Setup
|
6
|
+
|
7
|
+
Start by installing perftools.rb:
|
8
|
+
|
9
|
+
gem install perftools.rb
|
10
|
+
|
11
|
+
I have it installed in my global gemset, which is handy since you can profile projects that uses any other gemset without having to add perftools.rb to its Gemfile:
|
12
|
+
|
13
|
+
rvm gemset use global
|
14
|
+
gem install perftools.rb
|
15
|
+
|
16
|
+
If you look at the README for perftools.rb the way to use it seems really icky, having to set environment variables and all that. I have the following script saved as `~/bin/profile` to encapsulate all that:
|
17
|
+
|
18
|
+
#!/bin/bash
|
19
|
+
|
20
|
+
profile_id=$(date +'%Y%m%d%H%M%S')
|
21
|
+
CPUPROFILE=/tmp/$profile_id RUBYOPT="-r`gem which perftools | tail -1`" $@
|
22
|
+
pprof.rb --pdf /tmp/$profile_id > /tmp/$profile_id.pdf
|
23
|
+
open /tmp/$profile_id.pdf
|
24
|
+
|
25
|
+
With this script you can prefix any command with "profile" and perftools.rb will be enabed, and a nice graph will be opened in Preview as soon as the program exits. This only works on OS X, obviously, but I'm sure it can be made to work on Linux too.
|
26
|
+
|
27
|
+
## How to profile the code
|
28
|
+
|
29
|
+
Most of the time you want to profile a specific method or piece of code. The easiest thing to do is write a short script that runs that code over and over again. This will make sure that the profile graph isn't swamped by noise from other parts of the code, and running lots of iterations means that the numbers will be more stable. Perhaps most importantly it will also show the impact of the GC.
|
30
|
+
|
31
|
+
100000.times do
|
32
|
+
strs.each do |str|
|
33
|
+
AMQ::Client::Framing::String::Frame.decode(str)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
In the code above the `strs` variable holds a list of strings I've stolen from the amqp-benchmarks repository. I call the `decode` method of the `AMQ::Client::Framing::String::Frame` class over and over again and after 100K iterations (this takes around ten seconds to run) I get a graph that shows me which parts of the code take most time.
|
38
|
+
|
39
|
+
This is how you run the script above (assuming you fill in the `strs` list and add the necessary require's -- and that you've followed the instructions at the top of this document -- and that you've named it "benchmark_script.rb"):
|
40
|
+
|
41
|
+
profile ruby benchmark_script.rb
|
42
|
+
|
43
|
+
It will run for a few seconds and then open a graph. Next I'll explain how to interpret that graph.
|
44
|
+
|
45
|
+
## Interpreting the profile graph
|
46
|
+
|
47
|
+
The graph generated by perftools.rb shows all the methods that have run, which methods they call, how much time was spent in each method, and a lot of other very interesting things.
|
48
|
+
|
49
|
+
At the top left of the picture it says mow many samples were taken. perftools.rb samples 100 times per second, so 1257 samples means that it ran for 12.57 seconds. The number of samples per second can be tweaked, but I've never had any reason to. The next three numbers just tell you that there are methods that were called not very often, and that these are not shown in the graph, to make it easier to read.
|
50
|
+
|
51
|
+
There are usually two, and sometimes three, nodes which don't have any arrows pointing into them. The first is the entry point into your application, the second the GC, and sometimes there is a third which usually is RubyGems or Bundler. There is no reason there can't be more top level nodes, but those are the ones I've seen.
|
52
|
+
|
53
|
+
Each node represents a method. The label is the class and method name, in classic Ruby style: ClassName#instance_method_name. Unfortunately it doesn't handle class methods very well, so all end up ass Class#method_name. This is bad for us, since most methods in amq-protocol are class methods. It also means that all class methods of the same name, regardless of which class then belong are represented as one node. That's really bad for us, since a lot of methods that we want to profile are called "decode" and "encode".
|
54
|
+
|
55
|
+
The numbers below the label are the self time and cumulative time measurements, first as samples and then, in parentheses as percentages of the whole. Self time means the time spent in the method, excluding other methods, and cumulative time means the total time spent in the method, and all methods below it in the call stack. In other words, if method a calls b and c, the cumulative time for a is its self time, plus the cumulative time of b and c (which is the self time of each of those, plus the cumulative time of the methods they call, and so on).
|
56
|
+
|
57
|
+
The number by the arrow from one node to another is the number of samples that saw the source node call the destination node. Looking at the numbers by the arrows into a node you can see roughly which method calls the method represented by that node the most.
|
58
|
+
|
59
|
+
That's more or less it. I find the percentages most useful to work with, as the number of samples can vary a bit from time to time, its basically the same problem as using `time` to benchmark. Percentages are often much more stable.
|
60
|
+
|
61
|
+
In the next section I'll explain how to act on the information that the graph gives.
|
62
|
+
|
63
|
+
## Optimizing
|
64
|
+
|
65
|
+
The first thing to look at is the GC node. It will always be high (this is Ruby after all), but if it's above 20% this is what you should start with. The reason the GC takes so much time is because the code creates too many objects. It's not always obvious what is creating so many objects, but read the next section for how to remove the worst offenders. When you've picked the low hanging fruit and still have a high GC percentage, go over your code and try to see if there are places where you can memoize, cache, etc. If your objects are immutable you should never have to create two identical objects (on the other hand, it's easy to leak memory if you cache too much).
|
66
|
+
|
67
|
+
The next thing to look at is the node with the highest self time percentage. This is your first hotspot. Apply all the tricks in the next section and see if it makes any difference. Sometimes hotspots are hotspots because of design rather than slow code. If a method is called many, many times you may not be able to optimize it further (but try, each tiny thing will make a huge difference).
|
68
|
+
|
69
|
+
The third thing to look for are methods will high cumulative time. If the methods they call don't look too bad and can be optimized first, you may be able to rewrite the method to call them fewer times by caching values, for example.
|
70
|
+
|
71
|
+
While optimizing run the profiler for each change to make sure you're going in the right direction. It's not uncommon for something that you think should be faster to be slower because of something you couldn't foresee or some unexpected side effect. When the method you're working on is no longer the worst offender, move on to the next, and iterate.
|
72
|
+
|
73
|
+
## Tips
|
74
|
+
|
75
|
+
These are some general tips I've gathered while optimizing Ruby code:
|
76
|
+
|
77
|
+
* Avoid string literals. In contrast to Java, strings in Ruby are mutable, so each literal string is a separate object (otherwise 'a'.upcase! would have very strange side effects). Move string literals to constants.
|
78
|
+
* Avoid range literals. Just as with strings, ranges are objects and they are created every time they are encountered. Ranges are most often used to extract parts of a string, like `str[1..5]`. This can always be rewritten as a slice instead: `str[1, 4]` (the second argument is a length, not an index, so make sure you get the right value). Code like `data[offset..(offset + 4)]` is especially wasteful since it would be much clearer with the slice syntax: `data[offset, 5]`.
|
79
|
+
* Avoid regular expressions. They are convenient, but for code that will be called thousands of times you need to rewrite them using String#index and String#slice/[]. Make sure that your rewrite doesn't create a lot of temporary string objects.
|
80
|
+
* When reading files don't read line by line, and absolutely never byte by byte. It's much faster to read a big chunk (as in megabytes) and use String#split to split into lines. The same goes for writing. Buffer the lines and write them in one big chunk. Avoid IO#tell and #seek unless you're trying to read a few bytes out of a gigabyte file.
|
81
|
+
* Never ever, ever, use Date or DateTime, or require 'date' (to get Time.parse, for example). Date is so slow that it's not even funny. If you use Date.parse you can be sure that whatever else you do that method will account for at least half of the running time. If you really, really need date parsing and date perform date calculations, use the home_run gem, but for almost everything you can use the basic methods on Time (but those aren't cheap either).
|
data/README.textile
CHANGED
@@ -1,32 +1,49 @@
|
|
1
|
-
h1. About
|
1
|
+
h1. About amq-protocol.
|
2
2
|
|
3
|
-
|
3
|
+
amq-protocol is an AMQP 0.9.1 serialization library for Ruby. It is not an
|
4
|
+
AMQP client: amq-protocol only handles serialization and deserialization.
|
5
|
+
If you want to write your own AMQP client, this gem can help you with that.
|
4
6
|
|
5
|
-
|
7
|
+
h2. How does amq-protocol relate to amqp gem, amq-client and libraries like bunny?
|
6
8
|
|
7
|
-
|
8
|
-
gem install amqp-parser
|
9
|
-
</pre>
|
9
|
+
See "this page about AMQP gems family":https://github.com/ruby-amqp/amq-client/blob/master/README.textile
|
10
10
|
|
11
|
-
Check the @examples@ directory for some examples.
|
12
11
|
|
13
|
-
|
12
|
+
h2. Installation
|
13
|
+
|
14
|
+
If you use Bundler and want to use the very latest version, add this to your Gemfile:
|
14
15
|
|
15
16
|
<pre>
|
16
|
-
git
|
17
|
+
gem 'amq-protocol', :git => 'https://github.com/ruby-amqp/amq-protocol.git'
|
17
18
|
</pre>
|
18
19
|
|
19
|
-
|
20
|
+
|
21
|
+
h2. Development
|
22
|
+
|
23
|
+
h3. Nightly Builds
|
24
|
+
|
25
|
+
You can always find nightly builds at "gems.101ideas.cz":http://gems.101ideas.cz.
|
26
|
+
|
27
|
+
You can install them thusly:
|
20
28
|
|
21
29
|
<pre>
|
22
|
-
|
23
|
-
|
30
|
+
wget http://gems.101ideas.cz/amq-protocol-nightly.gem
|
31
|
+
gem install amq-protocol-nightly.gem
|
24
32
|
</pre>
|
25
33
|
|
26
|
-
|
34
|
+
h3. Contributing
|
35
|
+
|
36
|
+
If you want to change some code, don't edit @lib/amq/protocol/client.rb@ —
|
37
|
+
that file is generated from @protocol.rb.pytemplate@
|
38
|
+
which is a template file used by @codegen.py@. You can re-generate by running
|
39
|
+
<pre>nake ./tasks.rb generate</pre>
|
40
|
+
Please note that nake gem only runs on Ruby 1.9.2.
|
27
41
|
|
28
|
-
|
42
|
+
h2. Links
|
29
43
|
|
30
|
-
* "
|
31
|
-
* "Examples":https://github.com/
|
32
|
-
* "
|
44
|
+
* "API Documentation":http://rdoc.info/github/ruby-amqp/amq-protocol/master/frames
|
45
|
+
* "Examples":https://github.com/ruby-amqp/amq-protocol/tree/master/examples/
|
46
|
+
* "Jabber room for contributors":xmpp://amqp-dev@conf.netlab.cz
|
47
|
+
* "Ruby AMQP mailing list":http://groups.google.com/group/ruby-amqp
|
48
|
+
* "Issue tracker":http://github.com/ruby-amqp/amq-protocol/issues
|
49
|
+
* "Continous integration server":http://travis-ci.org/#!/ruby-amqp/amq-protocol
|
data/TODO.todo
CHANGED
@@ -1,3 +1,27 @@
|
|
1
1
|
- YARD documentation
|
2
2
|
- benchmarks
|
3
3
|
- optimisations (see puka)
|
4
|
+
- Don't let users set the deprecated arguments.
|
5
|
+
|
6
|
+
OpenOk
|
7
|
+
if $DEBUG
|
8
|
+
if data[offset..(offset + 1)] != "\x00"
|
9
|
+
raise BadResponseError.new("known_hosts", "\x00", data)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
|
14
|
+
DO NOT GENERATE THE FOLLOWING:
|
15
|
+
attr_reader :known_hosts
|
16
|
+
def initialize(known_hosts)
|
17
|
+
@known_hosts = known_hosts
|
18
|
+
end
|
19
|
+
|
20
|
+
arguments = Basic::Publish.method(:encode).parameters.map { |array| array.last }
|
21
|
+
arguments.should include(:frame_size)
|
22
|
+
|
23
|
+
arguments = Connection::Open.method(:encode).map { |array| array.last }
|
24
|
+
arguments.should_not include(:capabilities)
|
25
|
+
arguments.should_not include(:insist)
|
26
|
+
|
27
|
+
generate the spec file(s)?
|
data/__init__.py
ADDED
File without changes
|
data/amq-protocol.gemspec
CHANGED
@@ -3,30 +3,35 @@
|
|
3
3
|
|
4
4
|
require "base64"
|
5
5
|
|
6
|
+
require File.expand_path("../lib/amq/protocol/version", __FILE__)
|
7
|
+
|
6
8
|
Gem::Specification.new do |s|
|
7
9
|
s.name = "amq-protocol"
|
8
|
-
s.version =
|
9
|
-
s.authors = ["Jakub Stastny"]
|
10
|
-
s.homepage = "http://github.com/
|
10
|
+
s.version = AMQ::Protocol::VERSION
|
11
|
+
s.authors = ["Jakub Stastny", "Michael S. Klishin", "Theo Hultberg", "Mark Abramov"]
|
12
|
+
s.homepage = "http://github.com/ruby-amqp/amq-protocol"
|
11
13
|
s.summary = "AMQP 0.9.1 encoder & decoder."
|
12
|
-
s.description =
|
14
|
+
s.description = <<-DESC
|
15
|
+
amq-protocol is an AMQP 0.9.1 serialization library for Ruby. It is not an
|
16
|
+
AMQP client: amq-protocol only handles serialization and deserialization.
|
17
|
+
If you want to write your own AMQP client, this gem can help you with that.
|
18
|
+
DESC
|
13
19
|
s.cert_chain = nil
|
14
|
-
s.email =
|
20
|
+
s.email = ["bWljaGFlbEBub3ZlbWJlcmFpbi5jb20=\n", "c3Rhc3RueUAxMDFpZGVhcy5jeg==\n"].map { |i| Base64.decode64(i) }
|
15
21
|
s.has_rdoc = true
|
16
22
|
|
17
23
|
# files
|
18
|
-
s.files = `git ls-files`.split("\n")
|
24
|
+
s.files = `git ls-files`.split("\n").reject { |file| file =~ /^vendor\// }
|
19
25
|
s.require_paths = ["lib"]
|
20
26
|
|
21
|
-
|
22
|
-
|
27
|
+
s.extra_rdoc_files = ["README.textile"] + Dir.glob("doc/*")
|
28
|
+
|
23
29
|
|
24
30
|
begin
|
25
31
|
require "changelog"
|
26
|
-
rescue LoadError
|
27
|
-
warn "You have to have changelog gem installed for post install message"
|
28
|
-
else
|
29
32
|
s.post_install_message = CHANGELOG.new.version_changes
|
33
|
+
rescue LoadError
|
34
|
+
# warn "You have to have changelog gem installed for post install message"
|
30
35
|
end
|
31
36
|
|
32
37
|
# RubyForge
|
data/amqp_0.9.1_changes.json
CHANGED
@@ -1 +1 @@
|
|
1
|
-
{"tx": {"select-ok": ["client"], "rollback": ["server"], "commit": ["server"], "rollback-ok": ["client"], "select": ["server"], "commit-ok": ["client"]}, "exchange": {"delete-ok": ["client"], "declare-ok": ["client"], "declare": ["server"], "delete": ["server"], "bind": ["server"], "bind-ok": ["client"], "unbind": ["server"], "unbind-ok": ["client"]}, "queue": {"unbind": ["server"], "unbind-ok": ["client"], "purge-ok": ["client"], "bind": ["server"], "purge": ["server"], "declare-ok": ["client"], "delete-ok": ["client"], "delete": ["server"], "declare": ["server"], "bind-ok": ["client"]}, "connection": {"secure": ["client"], "secure-ok": ["server"], "open-ok": ["client"], "close-ok": ["client", "server"], "start": ["client"], "tune": ["client"], "start-ok": ["server"], "close": ["client", "server"], "open": ["server"], "tune-ok": ["server"]}, "basic": {"qos": ["server"], "consume": ["server"], "reject": ["server"], "get": ["server"], "ack": ["server"], "get-ok": ["client"], "consume-ok": ["client"], "deliver": ["client"], "recover-ok": ["client"], "publish": ["server"], "cancel": ["server"], "recover-async": ["server"], "get-empty": ["client"], "qos-ok": ["client"], "return": ["client"], "recover": ["server"], "cancel-ok": ["client"]}, "channel": {"flow-ok": ["server", "client"], "flow": ["server", "client"], "open-ok": ["client"], "close-ok": ["client", "server"], "close": ["client", "server"], "open": ["server"]}}
|
1
|
+
{"tx": {"select-ok": ["client"], "rollback": ["server"], "commit": ["server"], "rollback-ok": ["client"], "select": ["server"], "commit-ok": ["client"]}, "exchange": {"delete-ok": ["client"], "declare-ok": ["client"], "declare": ["server"], "delete": ["server"], "bind": ["server"], "bind-ok": ["client"], "unbind": ["server"], "unbind-ok": ["client"]}, "queue": {"unbind": ["server"], "unbind-ok": ["client"], "purge-ok": ["client"], "bind": ["server"], "purge": ["server"], "declare-ok": ["client"], "delete-ok": ["client"], "delete": ["server"], "declare": ["server"], "bind-ok": ["client"]}, "connection": {"secure": ["client"], "secure-ok": ["server"], "open-ok": ["client"], "close-ok": ["client", "server"], "start": ["client"], "tune": ["client"], "start-ok": ["server"], "close": ["client", "server"], "open": ["server"], "tune-ok": ["server"]}, "basic": {"qos": ["server"], "consume": ["server"], "reject": ["server"], "get": ["server"], "ack": ["client", "server"], "get-ok": ["client"], "consume-ok": ["client"], "deliver": ["client"], "recover-ok": ["client"], "publish": ["server"], "cancel": ["server"], "recover-async": ["server"], "get-empty": ["client"], "qos-ok": ["client"], "return": ["client"], "recover": ["server"], "cancel-ok": ["client"]}, "channel": {"flow-ok": ["server", "client"], "flow": ["server", "client"], "open-ok": ["client"], "close-ok": ["client", "server"], "close": ["client", "server"], "open": ["server"]}}
|
data/bin/jenkins.sh
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
|
3
|
+
echo -e "\n\n==== Setup ===="
|
4
|
+
source /etc/profile
|
5
|
+
git fetch && git reset origin/master --hard
|
6
|
+
|
7
|
+
echo -e "\n\n==== Ruby 1.9.2 Head ===="
|
8
|
+
rvm use 1.9.2-head@ruby-amqp
|
9
|
+
gem install bundler --no-ri --no-rdoc
|
10
|
+
bundle install --path vendor/bundle/1.9.2 --without development; echo
|
11
|
+
# bundle update --without development; echo
|
12
|
+
bundle exec rspec spec
|
13
|
+
return_status=$?
|
14
|
+
|
15
|
+
echo -e "\n\n==== Ruby 1.8.7 ===="
|
16
|
+
rvm use 1.8.7@ruby-amqp
|
17
|
+
gem install bundler --no-ri --no-rdoc
|
18
|
+
bundle install --path vendor/bundle/1.8.7 --without development; echo
|
19
|
+
bundle update; echo
|
20
|
+
bundle exec rspec spec
|
21
|
+
return_status=$(expr $return_status + $?)
|
22
|
+
|
23
|
+
test $return_status -eq 0
|
data/codegen.py
CHANGED
@@ -1,6 +1,9 @@
|
|
1
1
|
#!/usr/bin/env python
|
2
2
|
# -*- coding: utf-8 -*-
|
3
3
|
|
4
|
+
# Documentation for Mako templates:
|
5
|
+
# http://www.makotemplates.org/docs/syntax.html
|
6
|
+
|
4
7
|
import os, sys, re
|
5
8
|
|
6
9
|
sys.path.append(os.path.join("vendor", "rabbitmq-codegen"))
|
@@ -14,10 +17,9 @@ except ImportError:
|
|
14
17
|
|
15
18
|
# main class
|
16
19
|
class AmqpSpecObject(AmqpSpec):
|
17
|
-
IGNORED_CLASSES = ["access"
|
20
|
+
IGNORED_CLASSES = ["access"]
|
18
21
|
IGNORED_FIELDS = {
|
19
22
|
'ticket': 0,
|
20
|
-
'nowait': 0,
|
21
23
|
'capabilities': '',
|
22
24
|
'insist' : 0,
|
23
25
|
'out_of_band': '',
|
@@ -30,10 +32,10 @@ class AmqpSpecObject(AmqpSpec):
|
|
30
32
|
def extend_field(field):
|
31
33
|
field.ruby_name = re.sub("[- ]", "_", field.name)
|
32
34
|
field.type = self.resolveDomain(field.domain)
|
33
|
-
field.
|
35
|
+
field.ignored = bool(field.name in self.__class__.IGNORED_FIELDS) # I. e. deprecated
|
34
36
|
|
35
37
|
for klass in self.classes:
|
36
|
-
klass.
|
38
|
+
klass.ignored = bool(klass.name in self.__class__.IGNORED_CLASSES)
|
37
39
|
|
38
40
|
for field in klass.fields:
|
39
41
|
extend_field(field)
|
@@ -42,7 +44,7 @@ class AmqpSpecObject(AmqpSpec):
|
|
42
44
|
for field in method.arguments:
|
43
45
|
extend_field(field)
|
44
46
|
|
45
|
-
self.classes = filter(lambda klass: not klass.
|
47
|
+
self.classes = filter(lambda klass: not klass.ignored, self.classes)
|
46
48
|
|
47
49
|
# I know, I'm a bad, bad boy, but come on guys,
|
48
50
|
# monkey-patching is just handy for this case.
|
@@ -73,16 +75,44 @@ def accepted_by(self, *receivers):
|
|
73
75
|
|
74
76
|
AmqpMethod.accepted_by = accepted_by
|
75
77
|
|
78
|
+
def convert_value_to_ruby(value):
|
79
|
+
values = {None: "nil", False: "false", True: "true", "": "EMPTY_STRING"}
|
80
|
+
|
81
|
+
try:
|
82
|
+
return values[value]
|
83
|
+
except:
|
84
|
+
return value.__repr__()
|
85
|
+
|
76
86
|
def convert_to_ruby(field):
|
77
87
|
name = re.sub("-", "_", field.name) # TODO: use ruby_name
|
78
|
-
if
|
79
|
-
return "%s =
|
80
|
-
elif field.defaultvalue == False:
|
81
|
-
return "%s = false" % (name,)
|
82
|
-
elif field.defaultvalue == True:
|
83
|
-
return "%s = true" % (name,)
|
88
|
+
if name == "ticket":
|
89
|
+
return "%s = %s" % (name, field.defaultvalue) # we want to keep it as an int, not as a boolean
|
84
90
|
else:
|
85
|
-
return "%s = %
|
91
|
+
return "%s = %s" % (name, convert_value_to_ruby(field.defaultvalue))
|
92
|
+
|
93
|
+
def not_ignored_args(self):
|
94
|
+
if self.hasContent:
|
95
|
+
return ["payload", "user_headers"] + map(lambda argument: argument.ruby_name, filter(lambda argument: not argument.ignored, self.arguments)) + ["frame_size"]
|
96
|
+
else:
|
97
|
+
return map(lambda argument: argument.ruby_name, filter(lambda argument: not argument.ignored, self.arguments))
|
98
|
+
|
99
|
+
AmqpMethod.not_ignored_args = not_ignored_args
|
100
|
+
|
101
|
+
def ignored_args(self):
|
102
|
+
return filter(lambda argument: argument.ignored, self.arguments)
|
103
|
+
|
104
|
+
AmqpMethod.ignored_args = ignored_args
|
105
|
+
|
106
|
+
# helpers
|
107
|
+
def to_ruby_name(name):
|
108
|
+
return re.sub("[- ]", "_", name)
|
109
|
+
|
110
|
+
def to_ruby_class_name(name):
|
111
|
+
parts = re.split("[- ]", name)
|
112
|
+
ruby_class_name = ""
|
113
|
+
for part in parts:
|
114
|
+
ruby_class_name = ruby_class_name + part[0].upper() + part[1:].lower()
|
115
|
+
return ruby_class_name
|
86
116
|
|
87
117
|
def params(self):
|
88
118
|
buffer = []
|
@@ -113,9 +143,13 @@ def render(path, **context):
|
|
113
143
|
template = Template(file.read())
|
114
144
|
return template.render(**context)
|
115
145
|
|
116
|
-
def
|
117
|
-
|
118
|
-
|
146
|
+
def generateMain(type):
|
147
|
+
def main(json_spec_path):
|
148
|
+
spec = AmqpSpecObject(json_spec_path)
|
149
|
+
spec.type = type
|
150
|
+
print render("protocol.rb.pytemplate", spec = spec)
|
151
|
+
|
152
|
+
return main
|
119
153
|
|
120
154
|
if __name__ == "__main__":
|
121
|
-
do_main_dict({"
|
155
|
+
do_main_dict({"client": generateMain("client"), "server": generateMain("server")})
|
data/codegen_helpers.py
CHANGED
@@ -4,72 +4,124 @@ def genSingleEncode(spec, cValue, unresolved_domain):
|
|
4
4
|
buffer = []
|
5
5
|
type = spec.resolveDomain(unresolved_domain)
|
6
6
|
if type == 'shortstr':
|
7
|
-
buffer.append("
|
8
|
-
buffer.append("
|
7
|
+
buffer.append("buffer << %s.bytesize.chr" % (cValue,))
|
8
|
+
buffer.append("buffer << %s" % (cValue,))
|
9
9
|
elif type == 'longstr':
|
10
|
-
buffer.append("
|
11
|
-
buffer.append("
|
10
|
+
buffer.append("buffer << [%s.bytesize].pack(PACK_UINT32)" % (cValue,))
|
11
|
+
buffer.append("buffer << %s" % (cValue,))
|
12
12
|
elif type == 'octet':
|
13
|
-
buffer.append("
|
13
|
+
buffer.append("buffer << [%s].pack(PACK_CHAR)" % (cValue,))
|
14
14
|
elif type == 'short':
|
15
|
-
buffer.append("
|
15
|
+
buffer.append("buffer << [%s].pack(PACK_UINT16)" % (cValue,))
|
16
16
|
elif type == 'long':
|
17
|
-
buffer.append("
|
17
|
+
buffer.append("buffer << [%s].pack(PACK_UINT32)" % (cValue,))
|
18
18
|
elif type == 'longlong':
|
19
|
-
buffer.append("
|
19
|
+
buffer.append("buffer << AMQ::Hacks.pack_64_big_endian(%s)" % (cValue,))
|
20
20
|
elif type == 'timestamp':
|
21
|
-
buffer.append("
|
21
|
+
buffer.append("buffer << AMQ::Hacks.pack_64_big_endian(%s)" % (cValue,))
|
22
22
|
elif type == 'bit':
|
23
23
|
raise "Can't encode bit in genSingleEncode"
|
24
24
|
elif type == 'table':
|
25
|
-
buffer.append("
|
25
|
+
buffer.append("buffer << AMQ::Protocol::Table.encode(%s)" % (cValue,))
|
26
26
|
else:
|
27
27
|
raise "Illegal domain in genSingleEncode", type
|
28
28
|
|
29
29
|
return buffer
|
30
30
|
|
31
|
-
def genSingleDecode(spec,
|
31
|
+
def genSingleDecode(spec, field):
|
32
|
+
cLvalue = field.ruby_name
|
33
|
+
unresolved_domain = field.domain
|
34
|
+
|
35
|
+
if cLvalue == "known_hosts":
|
36
|
+
import sys
|
37
|
+
print >> sys.stderr, field, field.ignored
|
38
|
+
|
32
39
|
type = spec.resolveDomain(unresolved_domain)
|
33
40
|
buffer = []
|
34
41
|
if type == 'shortstr':
|
35
|
-
buffer.append("length = data[offset
|
42
|
+
buffer.append("length = data[offset, 1].unpack(PACK_CHAR).first")
|
36
43
|
buffer.append("offset += 1")
|
37
|
-
buffer.append("%s = data[offset
|
44
|
+
buffer.append("%s = data[offset, length]" % (cLvalue,))
|
38
45
|
buffer.append("offset += length")
|
39
46
|
elif type == 'longstr':
|
40
|
-
buffer.append("length = data[offset
|
47
|
+
buffer.append("length = data[offset, 4].unpack(PACK_UINT32).first")
|
41
48
|
buffer.append("offset += 4")
|
42
|
-
buffer.append("%s = data[offset
|
49
|
+
buffer.append("%s = data[offset, length]" % (cLvalue,))
|
43
50
|
buffer.append("offset += length")
|
44
51
|
elif type == 'octet':
|
45
|
-
buffer.append("%s = data[offset
|
52
|
+
buffer.append("%s = data[offset, 1].unpack(PACK_CHAR).first" % (cLvalue,))
|
46
53
|
buffer.append("offset += 1")
|
47
54
|
elif type == 'short':
|
48
|
-
buffer.append("%s = data[offset
|
55
|
+
buffer.append("%s = data[offset, 2].unpack(PACK_UINT16).first" % (cLvalue,))
|
49
56
|
buffer.append("offset += 2")
|
50
57
|
elif type == 'long':
|
51
|
-
buffer.append("%s = data[offset
|
58
|
+
buffer.append("%s = data[offset, 4].unpack(PACK_UINT32).first" % (cLvalue,))
|
52
59
|
buffer.append("offset += 4")
|
53
60
|
elif type == 'longlong':
|
54
|
-
buffer.append("%s = data[offset
|
61
|
+
buffer.append("%s = AMQ::Hacks.unpack_64_big_endian(data[offset, 8]).first" % (cLvalue,))
|
55
62
|
buffer.append("offset += 8")
|
56
63
|
elif type == 'timestamp':
|
57
|
-
buffer.append("%s = data[offset
|
64
|
+
buffer.append("%s = data[offset, 8].unpack(PACK_UINT32_X2).first" % (cLvalue,))
|
58
65
|
buffer.append("offset += 8")
|
59
66
|
elif type == 'bit':
|
60
67
|
raise "Can't decode bit in genSingleDecode"
|
61
68
|
elif type == 'table':
|
62
|
-
buffer.append("table_length = Table.length(data[offset
|
63
|
-
buffer.append("%s = Table.decode(data[offset
|
69
|
+
buffer.append("table_length = Table.length(data[offset, 4])")
|
70
|
+
buffer.append("%s = Table.decode(data[offset, table_length + 4])" % (cLvalue,))
|
71
|
+
buffer.append("offset += table_length + 4")
|
72
|
+
else:
|
73
|
+
raise StandardError("Illegal domain '" + type + "' in genSingleDecode")
|
74
|
+
|
75
|
+
return buffer
|
76
|
+
|
77
|
+
|
78
|
+
|
79
|
+
def genSingleSimpleDecode(spec, field):
|
80
|
+
cLvalue = field.ruby_name
|
81
|
+
unresolved_domain = field.domain
|
82
|
+
|
83
|
+
if cLvalue == "known_hosts":
|
84
|
+
import sys
|
85
|
+
print >> sys.stderr, field, field.ignored
|
86
|
+
|
87
|
+
type = spec.resolveDomain(unresolved_domain)
|
88
|
+
buffer = []
|
89
|
+
if type == 'shortstr':
|
90
|
+
# buffer.append("length = data.unpack(PACK_CHAR)[0]")
|
91
|
+
# buffer.append("result = data[offset..-1]")
|
92
|
+
# buffer.append("raise 'Bad size: #{length} expected, got #{result.bytesize}' if result.bytesize != length")
|
93
|
+
# buffer.append("result")
|
94
|
+
buffer.append("data")
|
95
|
+
elif type == 'longstr':
|
96
|
+
# buffer.append("length = data.unpack(PACK_CHAR)[0]")
|
97
|
+
# buffer.append("result = data[offset..-1]")
|
98
|
+
# buffer.append("raise 'Bad size: #{length} expected, got #{result.bytesize}' if result.bytesize != length")
|
99
|
+
# buffer.append("result")
|
100
|
+
buffer.append("data")
|
101
|
+
elif type == 'octet':
|
102
|
+
buffer.append("data.unpack(PACK_CHAR).first")
|
103
|
+
elif type == 'short':
|
104
|
+
buffer.append("data.unpack(PACK_UINT16).first")
|
105
|
+
elif type == 'long':
|
106
|
+
buffer.append("data.unpack(PACK_UINT32).first")
|
107
|
+
elif type == 'longlong':
|
108
|
+
buffer.append("AMQ::Hacks.unpack_64_big_endian(data).first")
|
109
|
+
elif type == 'timestamp':
|
110
|
+
buffer.append("Time.at(data.unpack(PACK_UINT32_X2).last)")
|
111
|
+
elif type == 'bit':
|
112
|
+
raise "Can't decode bit in genSingleDecode"
|
113
|
+
elif type == 'table':
|
114
|
+
buffer.append("Table.decode(data)")
|
64
115
|
else:
|
65
|
-
raise "Illegal domain in
|
116
|
+
raise StandardError("Illegal domain '" + type + "' in genSingleSimpleDecode")
|
66
117
|
|
67
118
|
return buffer
|
68
119
|
|
120
|
+
|
69
121
|
def genEncodeMethodDefinition(spec, m):
|
70
122
|
def finishBits():
|
71
123
|
if bit_index is not None:
|
72
|
-
|
124
|
+
buffer.append("buffer << [bit_buffer].pack(PACK_CHAR)")
|
73
125
|
|
74
126
|
bit_index = None
|
75
127
|
buffer = []
|
@@ -103,11 +155,14 @@ def genDecodeMethodDefinition(spec, m):
|
|
103
155
|
if bitindex >= 8:
|
104
156
|
bitindex = 0
|
105
157
|
if bitindex == 0:
|
106
|
-
buffer.append("bit_buffer = data[offset
|
158
|
+
buffer.append("bit_buffer = data[offset, 1].unpack(PACK_CHAR).first")
|
107
159
|
buffer.append("offset += 1")
|
108
160
|
buffer.append("%s = (bit_buffer & (1 << %d)) != 0" % (f.ruby_name, bitindex))
|
161
|
+
#### TODO: ADD bitindex TO THE buffer
|
162
|
+
else:
|
163
|
+
buffer.append("%s = (bit_buffer & (1 << %d)) != 0" % (f.ruby_name, bitindex))
|
109
164
|
bitindex = bitindex + 1
|
110
165
|
else:
|
111
166
|
bitindex = None
|
112
|
-
buffer += genSingleDecode(spec, f
|
167
|
+
buffer += genSingleDecode(spec, f)
|
113
168
|
return buffer
|