ivanvanderbyl-amqp 0.6.13.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.
Files changed (51) hide show
  1. data/.gitignore +4 -0
  2. data/HISTORY +27 -0
  3. data/README.md +169 -0
  4. data/Rakefile +24 -0
  5. data/TODO +32 -0
  6. data/VERSION +1 -0
  7. data/doc/EXAMPLE_01_PINGPONG +2 -0
  8. data/doc/EXAMPLE_02_CLOCK +2 -0
  9. data/doc/EXAMPLE_03_STOCKS +2 -0
  10. data/doc/EXAMPLE_04_MULTICLOCK +2 -0
  11. data/doc/EXAMPLE_05_ACK +2 -0
  12. data/doc/EXAMPLE_05_POP +2 -0
  13. data/doc/EXAMPLE_06_HASHTABLE +2 -0
  14. data/lib/amqp.rb +110 -0
  15. data/lib/amqp/buffer.rb +270 -0
  16. data/lib/amqp/client.rb +225 -0
  17. data/lib/amqp/frame.rb +66 -0
  18. data/lib/amqp/protocol.rb +161 -0
  19. data/lib/amqp/server.rb +99 -0
  20. data/lib/amqp/spec.rb +832 -0
  21. data/lib/amqp/version.rb +6 -0
  22. data/lib/ext/blankslate.rb +7 -0
  23. data/lib/ext/em.rb +8 -0
  24. data/lib/ext/emfork.rb +69 -0
  25. data/lib/mq.rb +875 -0
  26. data/lib/mq/exchange.rb +351 -0
  27. data/lib/mq/header.rb +33 -0
  28. data/lib/mq/logger.rb +89 -0
  29. data/lib/mq/queue.rb +455 -0
  30. data/lib/mq/rpc.rb +100 -0
  31. data/old/README +30 -0
  32. data/old/Rakefile +12 -0
  33. data/old/amqp-0.8.json +606 -0
  34. data/old/amqp_spec.rb +796 -0
  35. data/old/amqpc.rb +695 -0
  36. data/old/codegen.rb +148 -0
  37. data/protocol/amqp-0.8.json +617 -0
  38. data/protocol/amqp-0.8.xml +3908 -0
  39. data/protocol/codegen.rb +173 -0
  40. data/protocol/doc.txt +281 -0
  41. data/research/api.rb +88 -0
  42. data/research/primes-forked.rb +63 -0
  43. data/research/primes-processes.rb +135 -0
  44. data/research/primes-threaded.rb +49 -0
  45. data/tasks/common.rake +18 -0
  46. data/tasks/doc.rake +14 -0
  47. data/tasks/gem.rake +40 -0
  48. data/tasks/git.rake +34 -0
  49. data/tasks/spec.rake +15 -0
  50. data/tasks/version.rake +71 -0
  51. metadata +158 -0
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ *.rbc
3
+ ~*
4
+ #*
data/HISTORY ADDED
@@ -0,0 +1,27 @@
1
+ == 0.6.7 / 2010-11-03
2
+
3
+ * Forked tmm1/amqp at Github
4
+
5
+ == 0.6.8 / 2010-11-03
6
+
7
+ * Additional improvements from latest forks added
8
+
9
+ == 0.6.9 / 2010-11-04
10
+
11
+ * Exchange#publish raises error if no connection to broker
12
+
13
+ == 0.6.10 / 2010-11-04
14
+
15
+ * Bacon tests converted to Rspec
16
+
17
+ == 0.6.11 / 2010-11-04
18
+
19
+ * AMQP class behavior specified
20
+
21
+ == 0.6.12 / 2010-11-04
22
+
23
+ * MQ class methods specified
24
+
25
+ == 0.6.13 / 2010-11-05
26
+
27
+ * Secondary MQ instance methods specified
@@ -0,0 +1,169 @@
1
+ What AMQP gem is
2
+ ================
3
+
4
+ Simple asynchronous AMQP driver for Ruby/EventMachine
5
+ This library works with Ruby 1.8, Ruby 1.9, JRuby and Rubinius,
6
+ and is licensed under [the Ruby License](http://www.ruby-lang.org/en/LICENSE.txt).
7
+
8
+ This library was tested primarily with RabbitMQ, although it should be compatible with any
9
+ server implementing the [AMQP 0-8 spec](http://www.amqp.org/confluence/download/attachments/720900/amqp0-8.pdf).
10
+
11
+
12
+ This fork of AMQP
13
+ =================
14
+
15
+ Intention is to describe AMQP lib behavior using RSpec, with reasonable level of completeness.
16
+ Given a solid spec suite, it will be easier to expand the library going forward.
17
+
18
+ Some improvements that are currently merged in, but not yet fully specified:
19
+
20
+ * Support for setting extended headers in MQ::Exchange#publish. Particularly useful for :reply_to for RPC.
21
+ * Multibyte character support (Ruby 1.9) in MQ::Exchange#publish.
22
+ * MQ::Exchange#publish raises error if no connection to broker.
23
+ * MQ::Queue only wraps headers with new MQ::Headers if they are not nil. This allows pops to tell more easily when they've requested a message from an empty queue. See (https://github.com/tmm1/amqp/issues#issue/22)
24
+ * Support for receiving Headers with zero-size data packets. Such contents with no body frames are totally legit if indicated header size is zero.
25
+
26
+ Some improvements that are planned:
27
+ * Support for AMQP::Protocol::Basic::Return method. See (https://github.com/tmm1/amqp/issues#issue/1).
28
+
29
+ Getting started
30
+ ===============
31
+
32
+ First things first, start with:
33
+
34
+ $ gem install arvicco-amqp
35
+
36
+ To use examples with RabbitMQ, first [install the broker](http://www.rabbitmq.com/install.html).
37
+
38
+ Then have a look at the various bundled examples:
39
+
40
+ ruby examples/mq/pingpong.rb # 1-1 communication using amq.direct
41
+ ruby examples/mq/clock.rb # 1-N communication using amq.fanout
42
+ ruby examples/mq/stocks.rb # 1-subscriber communication using amq.topic
43
+
44
+ ruby examples/mq/multiclock.rb # header based routing (new rabbitmq feature)
45
+ ruby examples/mq/ack.rb # using ack
46
+ ruby examples/mq/pop.rb # pop off messages one at a time
47
+
48
+ ruby examples/mq/hashtable.rb # simple async rpc layer
49
+ ruby examples/mq/primes.rb 4 # parallelized prime number generation
50
+ ruby examples/mq/logger.rb # simple logging api
51
+
52
+ For high level API documentation see MQ class.
53
+ For more details into the lower level AMQP client API, run the simple client example:
54
+
55
+ ruby examples/amqp/simple.rb # low-level AMQP api
56
+ ruby examples/mq/internal.rb # low-level Queue/Exchange api
57
+
58
+ Or refer to protocol/doc.txt, which enumerates packets sent between a server and client
59
+ during a typical session, in both binary and decoded formats.
60
+
61
+ How to use AMQP gem with Ruby on Rails, Merb, Sinatra and other web frameworks
62
+ ==============================================================================
63
+
64
+ To use AMQP gem from web applications, you would need to have EventMachine reactor running.
65
+ If you use [Thin](http://code.macournoyer.com/thin/), you are set: Thin uses EventMachine under
66
+ the hook.
67
+
68
+ With other web servers, you need to start EventMachine reactor in it's own thread like this:
69
+
70
+ Thread.new { EM.run }
71
+
72
+ because otherwise EventMachine will block current thread. Then connect to AMQP broker:
73
+
74
+ AMQP.connect(:host => "localhost", :user => "guest", :pass => "guest", :vhost => "/")
75
+
76
+ In a Ruby on Rails app, probably the best place for this code is initializer
77
+ (like config/initializers/amqp.rb). For Merb apps, it is config/init.rb. For
78
+ Sinatra and pure Rack applications, place it next to other configuration
79
+ code.
80
+
81
+ Same separate thread technique can be used to make EventMachine play nicely with other
82
+ libraries that would block current thread (like [File::Tail](http://rubygems.org/gems/file-tail)).
83
+
84
+ AMQP gem resources
85
+ ==================
86
+
87
+ * [AMQP gem mailing list](http://groups.google.com/group/ruby-amqp)
88
+ * [AMQP gem at GitHub](http://github.com/tmm1/amqp)
89
+ * [AMQP gem at Rubygems](http://rubygems.org/gems/amqp)
90
+ * [This fork at GitHub](http://github.com/arvicco/amqp)
91
+ * [This fork as a gem at Rubygems](http://rubygems.org/gems/arvicco-amqp)
92
+
93
+ Running specifications suite
94
+ ============================
95
+
96
+ To run the test suite make sure you have [Rspec2](http://gemcutter.org/gems/rspec) gem installed and run:
97
+
98
+ rake spec
99
+
100
+ The lib/amqp/spec.rb file is generated automatically based on the [AMQP specification](http://www.amqp.org/confluence/display/AMQP/AMQP+Specification). To generate it:
101
+
102
+ rake codegen
103
+
104
+ Writing your own AMQP/evented specs
105
+ ===================================
106
+
107
+ Writing evented specs properly is a bit difficult due to gory details of event loop management. Libraries such as
108
+ [AMQP-Spec](http://github.com/arvicco/amqp-spec) and [Moqueue](https://github.com/danielsdeleo/moqueue) may be helpful.
109
+
110
+
111
+ Credits and more information
112
+ ============================
113
+
114
+ (c) 2008—2010 [Aman Gupta](http://github.com/tmm1) (tmm1)
115
+
116
+ This project was inspired by [py-amqplib](http://barryp.org/software/py-amqplib/), [rabbitmq](http://rabbitmq.com), [qpid](http://qpid.apache.org/) and [rubbyt](http://github.com/rubbyt/rubbyt).
117
+ Special thanks to Dmitriy Samovskiy, Ben Hood and Tony Garnock-Jones.
118
+
119
+ AMQP brokers
120
+ ------------
121
+
122
+ * [RabbitMQ](http://rabbitmq.com) (Rabbit Technologies, Erlang/OTP, MPL)
123
+ * [ZeroMQ](http://www.zeromq.org) (iMatix/FastMQ/Intel, C++, GPL3)
124
+ * [OpenAMQ](http://openamq.org) (iMatix, C, GPL2)
125
+ * [ActiveMQ](http://activemq.apache.org) (Apache Foundation, Java, Apache2)
126
+
127
+ AMQP resources
128
+ --------------
129
+
130
+ * Steve Vinoski [explains AMQP](http://steve.vinoski.net/pdf/IEEE-Advanced_Message_Queuing_Protocol.pdf) in his column, Towards Integration
131
+
132
+ * John O'Hara on [the history of AMQP](http://www.acmqueue.org/modules.php?name=Content&pa=showpage&pid=485)
133
+
134
+ * Dmitriy's [presentation on RabbitMQ/AMQP](http://somic-org.homelinux.org/blog/2008/07/31/slides-for-my-amqprabbitmq-talk/)
135
+
136
+ * ZeroMQ's [analysis of the messaging technology market](http://www.zeromq.org/whitepapers:market-analysis)
137
+
138
+ * Pieter Hintjens's [background to AMQP](http://www.openamq.org/doc:amqp-background)
139
+
140
+ * Barry Pederson's [py-amqplib](http://barryp.org/software/py-amqplib/)
141
+
142
+ * Ben Hood on [writing an AMQP client](http://hopper.squarespace.com/blog/2008/6/21/build-your-own-amqp-client.html)
143
+
144
+ * Dmitriy Samovskiy introduces [Ruby + QPid + RabbitMQ](http://somic-org.homelinux.org/blog/2008/06/24/ruby-amqp-rabbitmq-example/)
145
+
146
+ * Ben Hood's [as3-amqp](http://github.com/0x6e6562/as3-amqp) ([two](http://hopper.squarespace.com/blog/2008/7/4/server-side-as3.html), [three](http://hopper.squarespace.com/blog/2008/3/24/as3-amqp-client-first-cut.html))
147
+
148
+ * RabbitMQ's [AMQP protocol code generator](http://hg.rabbitmq.com/rabbitmq-codegen/)
149
+
150
+ * Erlang Exchange [presentation on the implementation of RabbitMQ](http://skillsmatter.com/podcast/erlang/presenting-rabbitmq-an-erlang-based-implementatio-nof-amqp) (and on the [LShift blog](http://www.lshift.net/blog/2008/07/01/slides-from-our-erlang-exchange-talk))
151
+
152
+ * Jonathan Conway's series on RabbitMQ and using it with Ruby and Merb: [One](http://jaikoo.com/2007/9/4/didn-t-you-get-the-memo), [Two](http://jaikoo.com/2008/2/29/friday-round-up-2008-02-29), [Three](http://jaikoo.com/2008/3/14/oh-hai-rabbitmq), [Four](http://jaikoo.com/2008/3/20/daemonize-rabbitmq)
153
+
154
+ * Open Enterprise's series on messaging middleware and AMQP: [Part 1](http://www1.interopsystems.com/analysis/can-amqp-break-ibms-mom-monopoly-part-1.html), [Part 2](http://www1.interopsystems.com/analysis/can-amqp-break-ibms-mom-monopoly-part-2.html), [Part 3](http://www1.interopsystems.com/analysis/can-amqp-break-ibms-mom-monopoly-part-3.html)
155
+
156
+ Messaging and distributed systems resources
157
+ -------------------------------------------
158
+
159
+ * [A Critique of the Remote Procedure Call Paradigm](http://www.cs.vu.nl/~ast/publications/euteco-1988.pdf)
160
+
161
+ * [A Note on Distributed Computing](http://research.sun.com/techrep/1994/smli_tr-94-29.pdf)
162
+
163
+ * [Convenience Over Correctness](http://steve.vinoski.net/pdf/IEEE-Convenience_Over_Correctness.pdf)
164
+
165
+ * [Metaprotocol Taxonomy and Communications Patterns](http://hessian.caucho.com/doc/metaprotocol-taxonomy.xtp)
166
+
167
+ * Joe Armstrong on [Erlang messaging vs RPC](http://armstrongonsoftware.blogspot.com/2008/05/road-we-didnt-go-down.html)
168
+
169
+ * [SEDA: scalable internet services using message queues](http://www.eecs.harvard.edu/~mdw/papers/seda-sosp01.pdf)
@@ -0,0 +1,24 @@
1
+ require 'rake'
2
+ require 'pathname'
3
+
4
+ BASE_PATH = Pathname.new(__FILE__).dirname
5
+ LIB_PATH = BASE_PATH + 'lib'
6
+ PKG_PATH = BASE_PATH + 'pkg'
7
+ DOC_PATH = BASE_PATH + 'rdoc'
8
+
9
+ $LOAD_PATH.unshift LIB_PATH.to_s unless $LOAD_PATH.include? LIB_PATH.to_s
10
+
11
+ require 'amqp/version'
12
+
13
+ NAME = 'ivanvanderbyl-amqp'
14
+ CLASS_NAME = AMQP
15
+ VERSION = CLASS_NAME::VERSION
16
+
17
+ # Load rakefile tasks
18
+ Dir['tasks/*.rake'].sort.each { |file| load file }
19
+
20
+ desc "Generate AMQP specification classes"
21
+ task :codegen do
22
+ sh 'ruby protocol/codegen.rb > lib/amqp/spec.rb'
23
+ sh 'ruby lib/amqp/spec.rb'
24
+ end
data/TODO ADDED
@@ -0,0 +1,32 @@
1
+ - breaks with header values that are nil (deleting nil headers in Exchange#publish for now)
2
+
3
+ - generate amqp/spec.rb from original xml spec
4
+ - add peek and pop to queues
5
+ - use rabbitmq generated consumer tag from basic.consume-ok reply
6
+
7
+ - allow temporary queues with amq.queue(nil) syntax (use uuids)
8
+ - use as temp queue in rpc
9
+ - use uuids for message ids in rpc
10
+
11
+ - add ack/completed responses for messages
12
+ - deleting queues/bindings/exchanges
13
+ + queue.unbind
14
+ - queue.remove or queue.close or queue.delete
15
+ - exchange.remove
16
+ - rpc.remove
17
+
18
+ - handle errors and exceptions
19
+ binding to a non-existent (or not yet created in clock.rb) exchange
20
+ #<AMQP::Protocol::Channel::Close:0x11d35d4
21
+ @class_id=50,
22
+ @debug=1,
23
+ @method_id=20,
24
+ @reply_code=404,
25
+ @reply_text="NOT_FOUND - no exchange 'clock' in vhost '/'">>]
26
+
27
+ - handle connection.redirect during connect (for rabbitmq in distributed mode) [or just set insist to true]
28
+
29
+ - add amq.queue('name').size{ |num| "#{num} messages in the queue" } (send declare passive, look at declare-ok response)
30
+ - clean up MQ.default on disconnect
31
+
32
+ - add proper handling of Basic::Return (:immediate and :mandatory flags)
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.6.13.1
@@ -0,0 +1,2 @@
1
+ == Ping Pong Example
2
+ :include: mq/pingpong.rb
@@ -0,0 +1,2 @@
1
+ == Clock Example
2
+ :include: mq/clock.rb
@@ -0,0 +1,2 @@
1
+ == Stocks Example
2
+ :include: mq/stocks.rb
@@ -0,0 +1,2 @@
1
+ == Muti-format Clock Example
2
+ :include: mq/multiclock.rb
@@ -0,0 +1,2 @@
1
+ == Ack Example
2
+ :include: mq/ack.rb
@@ -0,0 +1,2 @@
1
+ == Pop Example
2
+ :include: mq/pop.rb
@@ -0,0 +1,2 @@
1
+ == HashTable RPC Example
2
+ :include: mq/hashtable.rb
@@ -0,0 +1,110 @@
1
+ require File.expand_path('../ext/em', __FILE__)
2
+ require File.expand_path('../ext/blankslate', __FILE__)
3
+
4
+ %w[ version buffer spec protocol frame client ].each do |file|
5
+ require File.expand_path("../amqp/#{file}", __FILE__)
6
+ end
7
+
8
+ module AMQP
9
+ class << self
10
+ @logging = false
11
+ attr_accessor :logging
12
+ attr_reader :conn, :closing
13
+ alias :closing? :closing
14
+ alias :connection :conn
15
+ end
16
+
17
+ def self.connect *args
18
+ Client.connect *args
19
+ end
20
+
21
+ def self.settings
22
+ @settings ||= {
23
+ # server address
24
+ :host => '127.0.0.1',
25
+ :port => PORT,
26
+
27
+ # login details
28
+ :user => 'guest',
29
+ :pass => 'guest',
30
+ :vhost => '/',
31
+
32
+ # connection timeout
33
+ :timeout => nil,
34
+
35
+ # logging
36
+ :logging => false,
37
+
38
+ # ssl
39
+ :ssl => false
40
+ }
41
+ end
42
+
43
+ # Must be called to startup the connection to the AMQP server.
44
+ #
45
+ # The method takes several arguments and an optional block.
46
+ #
47
+ # This takes any option that is also accepted by EventMachine::connect.
48
+ # Additionally, there are several AMQP-specific options.
49
+ #
50
+ # * :user => String (default 'guest')
51
+ # The username as defined by the AMQP server.
52
+ # * :pass => String (default 'guest')
53
+ # The password for the associated :user as defined by the AMQP server.
54
+ # * :vhost => String (default '/')
55
+ # The virtual host as defined by the AMQP server.
56
+ # * :timeout => Numeric (default nil)
57
+ # Measured in seconds.
58
+ # * :logging => true | false (default false)
59
+ # Toggle the extremely verbose logging of all protocol communications
60
+ # between the client and the server. Extremely useful for debugging.
61
+ #
62
+ # AMQP.start do
63
+ # # default is to connect to localhost:5672
64
+ #
65
+ # # define queues, exchanges and bindings here.
66
+ # # also define all subscriptions and/or publishers
67
+ # # here.
68
+ #
69
+ # # this block never exits unless EM.stop_event_loop
70
+ # # is called.
71
+ # end
72
+ #
73
+ # Most code will use the MQ api. Any calls to MQ.direct / MQ.fanout /
74
+ # MQ.topic / MQ.queue will implicitly call #start. In those cases,
75
+ # it is sufficient to put your code inside of an EventMachine.run
76
+ # block. See the code examples in MQ for details.
77
+ #
78
+ def self.start *args, &blk
79
+ EM.run{
80
+ @conn ||= connect *args
81
+ @conn.callback(&blk) if blk
82
+ @conn
83
+ }
84
+ end
85
+
86
+ class << self
87
+ alias :run :start
88
+ end
89
+
90
+ def self.stop
91
+ if @conn and not @closing
92
+ @closing = true
93
+ @conn.close{
94
+ yield if block_given?
95
+ @conn = nil
96
+ @closing = false
97
+ }
98
+ end
99
+ end
100
+
101
+ def self.fork workers
102
+ EM.fork(workers) do
103
+ # clean up globals in the fork
104
+ Thread.current[:mq] = nil
105
+ AMQP.instance_variable_set('@conn', nil)
106
+
107
+ yield
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,270 @@
1
+ if [].map.respond_to? :with_index
2
+ class Array #:nodoc:
3
+ def enum_with_index
4
+ each.with_index
5
+ end
6
+ end
7
+ else
8
+ require 'enumerator'
9
+ end
10
+
11
+ module AMQP
12
+ class Buffer #:nodoc: all
13
+ class Overflow < StandardError; end
14
+ class InvalidType < StandardError; end
15
+
16
+ def initialize data = ''
17
+ @data = data
18
+ @pos = 0
19
+ end
20
+
21
+ attr_reader :pos
22
+
23
+ def data
24
+ @data.clone
25
+ end
26
+ alias :contents :data
27
+ alias :to_s :data
28
+
29
+ def << data
30
+ @data << data.to_s
31
+ self
32
+ end
33
+
34
+ def length
35
+ @data.bytesize
36
+ end
37
+
38
+ def empty?
39
+ pos == length
40
+ end
41
+
42
+ def rewind
43
+ @pos = 0
44
+ end
45
+
46
+ def read_properties *types
47
+ types.shift if types.first == :properties
48
+
49
+ i = 0
50
+ values = []
51
+
52
+ while props = read(:short)
53
+ (0..14).each do |n|
54
+ # no more property types
55
+ break unless types[i]
56
+
57
+ # if flag is set
58
+ if props & (1<<(15-n)) != 0
59
+ if types[i] == :bit
60
+ # bit values exist in flags only
61
+ values << true
62
+ else
63
+ # save type name for later reading
64
+ values << types[i]
65
+ end
66
+ else
67
+ # property not set or is false bit
68
+ values << (types[i] == :bit ? false : nil)
69
+ end
70
+
71
+ i+=1
72
+ end
73
+
74
+ # bit(0) == 0 means no more property flags
75
+ break unless props & 1 == 1
76
+ end
77
+
78
+ values.map do |value|
79
+ value.is_a?(Symbol) ? read(value) : value
80
+ end
81
+ end
82
+
83
+ def read *types
84
+ if types.first == :properties
85
+ return read_properties(*types)
86
+ end
87
+
88
+ values = types.map do |type|
89
+ case type
90
+ when :octet
91
+ _read(1, 'C')
92
+ when :short
93
+ _read(2, 'n')
94
+ when :long
95
+ _read(4, 'N')
96
+ when :longlong
97
+ upper, lower = _read(8, 'NN')
98
+ upper << 32 | lower
99
+ when :shortstr
100
+ _read read(:octet)
101
+ when :longstr
102
+ _read read(:long)
103
+ when :timestamp
104
+ Time.at read(:longlong)
105
+ when :table
106
+ t = Hash.new
107
+
108
+ table = Buffer.new(read(:longstr))
109
+ until table.empty?
110
+ key, type = table.read(:shortstr, :octet)
111
+ key = key.intern
112
+ t[key] ||= case type
113
+ when 83 # 'S'
114
+ table.read(:longstr)
115
+ when 73 # 'I'
116
+ table.read(:long)
117
+ when 68 # 'D'
118
+ exp = table.read(:octet)
119
+ num = table.read(:long)
120
+ num / 10.0**exp
121
+ when 84 # 'T'
122
+ table.read(:timestamp)
123
+ when 70 # 'F'
124
+ table.read(:table)
125
+ end
126
+ end
127
+
128
+ t
129
+ when :bit
130
+ if (@bits ||= []).empty?
131
+ val = read(:octet)
132
+ @bits = (0..7).map{|i| (val & 1<<i) != 0 }
133
+ end
134
+
135
+ @bits.shift
136
+ else
137
+ raise InvalidType, "Cannot read data of type #{type}"
138
+ end
139
+ end
140
+
141
+ types.size == 1 ? values.first : values
142
+ end
143
+
144
+ def write type, data
145
+ case type
146
+ when :octet
147
+ _write(data, 'C')
148
+ when :short
149
+ _write(data, 'n')
150
+ when :long
151
+ _write(data, 'N')
152
+ when :longlong
153
+ lower = data & 0xffffffff
154
+ upper = (data & ~0xffffffff) >> 32
155
+ _write([upper, lower], 'NN')
156
+ when :shortstr
157
+ data = (data || '').to_s
158
+ _write([data.bytesize, data], 'Ca*')
159
+ when :longstr
160
+ if data.is_a? Hash
161
+ write(:table, data)
162
+ else
163
+ data = (data || '').to_s
164
+ _write([data.bytesize, data], 'Na*')
165
+ end
166
+ when :timestamp
167
+ write(:longlong, data.to_i)
168
+ when :table
169
+ data ||= {}
170
+ write :longstr, (data.inject(Buffer.new) do |table, (key, value)|
171
+ table.write(:shortstr, key.to_s)
172
+
173
+ case value
174
+ when String
175
+ table.write(:octet, 83) # 'S'
176
+ table.write(:longstr, value.to_s)
177
+ when Fixnum
178
+ table.write(:octet, 73) # 'I'
179
+ table.write(:long, value)
180
+ when Float
181
+ table.write(:octet, 68) # 'D'
182
+ # XXX there's gotta be a better way to do this..
183
+ exp = value.to_s.split('.').last.bytesize
184
+ num = value * 10**exp
185
+ table.write(:octet, exp)
186
+ table.write(:long, num)
187
+ when Time
188
+ table.write(:octet, 84) # 'T'
189
+ table.write(:timestamp, value)
190
+ when Hash
191
+ table.write(:octet, 70) # 'F'
192
+ table.write(:table, value)
193
+ end
194
+
195
+ table
196
+ end)
197
+ when :bit
198
+ [*data].to_enum(:each_slice, 8).each{|bits|
199
+ write(:octet, bits.enum_with_index.inject(0){ |byte, (bit, i)|
200
+ byte |= 1<<i if bit
201
+ byte
202
+ })
203
+ }
204
+ when :properties
205
+ values = []
206
+ data.enum_with_index.inject(0) do |short, ((type, value), i)|
207
+ n = i % 15
208
+ last = i+1 == data.size
209
+
210
+ if (n == 0 and i != 0) or last
211
+ if data.size > i+1
212
+ short |= 1<<0
213
+ elsif last and value
214
+ values << [type,value]
215
+ short |= 1<<(15-n)
216
+ end
217
+
218
+ write(:short, short)
219
+ short = 0
220
+ end
221
+
222
+ if value and !last
223
+ values << [type,value]
224
+ short |= 1<<(15-n)
225
+ end
226
+
227
+ short
228
+ end
229
+
230
+ values.each do |type, value|
231
+ write(type, value) unless type == :bit
232
+ end
233
+ else
234
+ raise InvalidType, "Cannot write data of type #{type}"
235
+ end
236
+
237
+ self
238
+ end
239
+
240
+ def extract
241
+ begin
242
+ cur_data, cur_pos = @data.clone, @pos
243
+ yield self
244
+ rescue Overflow
245
+ @data, @pos = cur_data, cur_pos
246
+ nil
247
+ end
248
+ end
249
+
250
+ def _read size, pack = nil
251
+ if @pos + size > length
252
+ raise Overflow
253
+ else
254
+ data = @data[@pos,size]
255
+ @data[@pos,size] = ''
256
+ if pack
257
+ data = data.unpack(pack)
258
+ data = data.pop if data.size == 1
259
+ end
260
+ data
261
+ end
262
+ end
263
+
264
+ def _write data, pack = nil
265
+ data = [*data].pack(pack) if pack
266
+ @data[@pos,0] = data
267
+ @pos += data.bytesize
268
+ end
269
+ end
270
+ end