ivanvanderbyl-amqp 0.6.13.1

Sign up to get free protection for your applications and to get access to all the features.
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