beefcake 0.1.0
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.
- data/LICENSE +22 -0
- data/README.md +126 -0
- data/lib/beefcake.rb +203 -0
- data/lib/beefcake/buffer.rb +2 -0
- data/lib/beefcake/buffer/base.rb +92 -0
- data/lib/beefcake/buffer/decode.rb +101 -0
- data/lib/beefcake/buffer/encode.rb +115 -0
- data/test/buffer_decode_test.rb +114 -0
- data/test/buffer_encode_test.rb +222 -0
- data/test/buffer_test.rb +44 -0
- data/test/message_test.rb +280 -0
- metadata +122 -0
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2011 Blake Mizerany
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person
|
4
|
+
obtaining a copy of this software and associated documentation
|
5
|
+
files (the "Software"), to deal in the Software without
|
6
|
+
restriction, including without limitation the rights to use,
|
7
|
+
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
8
|
+
copies of the Software, and to permit persons to whom the
|
9
|
+
Software is furnished to do so, subject to the following
|
10
|
+
conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be
|
13
|
+
included in all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
17
|
+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
18
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
19
|
+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
20
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
21
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
22
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,126 @@
|
|
1
|
+
# Beefcake (A sane Google Protocol Buffers library for Ruby)
|
2
|
+
## It's all about being Buf; ProtoBuf.
|
3
|
+
|
4
|
+
# Install
|
5
|
+
|
6
|
+
$ gem install beefcake
|
7
|
+
|
8
|
+
# Example
|
9
|
+
|
10
|
+
require 'beefcake'
|
11
|
+
|
12
|
+
class Variety
|
13
|
+
include Beefcake::Message
|
14
|
+
|
15
|
+
# Required
|
16
|
+
required :x, :int32, 1
|
17
|
+
required :y, :int32, 2
|
18
|
+
|
19
|
+
# Optional
|
20
|
+
optional :tag, :string, 3
|
21
|
+
|
22
|
+
# Repeated
|
23
|
+
repeated :ary, :fixed64, 4
|
24
|
+
repeated :pary, :fixed64, 5, :packed => true
|
25
|
+
|
26
|
+
# Enums - Simply use a Module (NOTE: defaults are optional)
|
27
|
+
module Foonum
|
28
|
+
A = 1
|
29
|
+
B = 2
|
30
|
+
end
|
31
|
+
|
32
|
+
# As per the spec, defaults are only set at the end
|
33
|
+
# of decoding a message, not on object creation.
|
34
|
+
optional :foo, Foonum, 6, :default => Foonum::B
|
35
|
+
end
|
36
|
+
|
37
|
+
x = Variety.new :x => 1, :y => 2
|
38
|
+
# or
|
39
|
+
x = Variety.new
|
40
|
+
x.x = 1
|
41
|
+
x.y = 2
|
42
|
+
|
43
|
+
## Encoding
|
44
|
+
|
45
|
+
Any object responding to `<<` can accept encoding
|
46
|
+
|
47
|
+
s = ""
|
48
|
+
x.encode(s)
|
49
|
+
p [:s, s]
|
50
|
+
# or (because encode returns the string/stream)
|
51
|
+
p [:s, x.encode]
|
52
|
+
# or
|
53
|
+
open("x.dat") do |f|
|
54
|
+
x.encode(f)
|
55
|
+
end
|
56
|
+
|
57
|
+
# decode
|
58
|
+
encoded = x.encode
|
59
|
+
decoded = Variety.decode(encoded)
|
60
|
+
p [:x, decoded]
|
61
|
+
|
62
|
+
# decode merge
|
63
|
+
Variety.decoded(more_data, decoded)
|
64
|
+
|
65
|
+
# Why?
|
66
|
+
|
67
|
+
Ruby deserves and needs first-class ProtoBuf support.
|
68
|
+
Other libs didn't feel very "Ruby" to me and were hard to parse.
|
69
|
+
|
70
|
+
# Caveats
|
71
|
+
|
72
|
+
Currently Beefcake doesn't parse `.proto` files. This is OK for now.
|
73
|
+
In the simple case, you can create message types by hand; This is Ruby
|
74
|
+
after all.
|
75
|
+
|
76
|
+
Example (for the above):
|
77
|
+
|
78
|
+
message Variety {
|
79
|
+
required int32 x = 1
|
80
|
+
required int32 y = 2
|
81
|
+
optional string tag = 3
|
82
|
+
|
83
|
+
...
|
84
|
+
}
|
85
|
+
|
86
|
+
In the near future, a generator would be nice. I welcome anyone willing
|
87
|
+
to work on it to submit patches. The other ruby libs lacked things I would
|
88
|
+
like, such as an optional namespace param rather than installing on (main).
|
89
|
+
|
90
|
+
This library was built with EventMachine in mind. Not just blocking-IO.
|
91
|
+
|
92
|
+
# Dev
|
93
|
+
|
94
|
+
Source:
|
95
|
+
|
96
|
+
$ git clone git://github.com/bmizerany/beefcake
|
97
|
+
|
98
|
+
Testing:
|
99
|
+
|
100
|
+
$ gem install turn
|
101
|
+
$ cd /path/to/beefcake
|
102
|
+
$ turn
|
103
|
+
|
104
|
+
VMs:
|
105
|
+
|
106
|
+
Currently Beefcake is tested and working on:
|
107
|
+
|
108
|
+
Ruby 1.8.7
|
109
|
+
Ruby 1.9.2
|
110
|
+
JRuby 1.5.6
|
111
|
+
|
112
|
+
There is a bug in Rubinius preventing proper ZigZag encoding.
|
113
|
+
The team is aware and I'm sure they're working on a fix.
|
114
|
+
|
115
|
+
Nice to have:
|
116
|
+
|
117
|
+
* `.proto` -> Ruby generator
|
118
|
+
* Groups (would be nice for accessing older protos)
|
119
|
+
|
120
|
+
# Further Reading
|
121
|
+
|
122
|
+
http://code.google.com/apis/protocolbuffers/docs/encoding.html
|
123
|
+
|
124
|
+
# Thank You
|
125
|
+
|
126
|
+
Aman Gupta (tmm1) for help with cross VM support and performance enhancements.
|
data/lib/beefcake.rb
ADDED
@@ -0,0 +1,203 @@
|
|
1
|
+
require 'beefcake/buffer'
|
2
|
+
|
3
|
+
module Beefcake
|
4
|
+
module Message
|
5
|
+
|
6
|
+
class WrongTypeError < StandardError
|
7
|
+
def initialize(name, exp, got)
|
8
|
+
super("Wrong type `#{got}` given for (#{name}). Expected #{exp}")
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
|
13
|
+
class InvalidValueError < StandardError
|
14
|
+
def initialize(name, val)
|
15
|
+
super("Invalid Value given for `#{name}`: #{val.inspect}")
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
|
20
|
+
class RequiredFieldNotSetError < StandardError
|
21
|
+
def initialize(name)
|
22
|
+
super("Field #{name} is required but nil")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
class Field < Struct.new(:rule, :name, :type, :fn, :opts)
|
28
|
+
def <=>(o)
|
29
|
+
fn <=> o.fn
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
module Dsl
|
35
|
+
def required(name, type, fn, opts={})
|
36
|
+
field(:required, name, type, fn, opts)
|
37
|
+
end
|
38
|
+
|
39
|
+
def repeated(name, type, fn, opts={})
|
40
|
+
field(:repeated, name, type, fn, opts)
|
41
|
+
end
|
42
|
+
|
43
|
+
def optional(name, type, fn, opts={})
|
44
|
+
field(:optional, name, type, fn, opts)
|
45
|
+
end
|
46
|
+
|
47
|
+
def field(rule, name, type, fn, opts)
|
48
|
+
fields[fn] = Field.new(rule, name, type, fn, opts)
|
49
|
+
attr_accessor name
|
50
|
+
end
|
51
|
+
|
52
|
+
def fields
|
53
|
+
@fields ||= {}
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
module Encode
|
58
|
+
|
59
|
+
def encode(buf = Buffer.new)
|
60
|
+
validate!
|
61
|
+
|
62
|
+
if ! buf.respond_to?(:<<)
|
63
|
+
raise ArgumentError, "buf doesn't respond to `<<`"
|
64
|
+
end
|
65
|
+
|
66
|
+
if ! buf.is_a?(Buffer)
|
67
|
+
buf = Buffer.new(buf)
|
68
|
+
end
|
69
|
+
|
70
|
+
# TODO: Error if any required fields at nil
|
71
|
+
|
72
|
+
fields.values.sort.each do |fld|
|
73
|
+
if fld.opts[:packed]
|
74
|
+
bytes = encode!(Buffer.new, fld, 0)
|
75
|
+
buf.append_info(fld.fn, Buffer.wire_for(fld.type))
|
76
|
+
buf.append_uint64(bytes.length)
|
77
|
+
buf << bytes
|
78
|
+
else
|
79
|
+
encode!(buf, fld, fld.fn)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
buf
|
84
|
+
end
|
85
|
+
|
86
|
+
def encode!(buf, fld, fn)
|
87
|
+
Array(self[fld.name]).each do |val|
|
88
|
+
case fld.type
|
89
|
+
when Class # encodable
|
90
|
+
# TODO: raise error if type != val.class
|
91
|
+
buf.append(:string, val.encode, fn)
|
92
|
+
when Module # enum
|
93
|
+
if ! valid_enum?(fld.type, val)
|
94
|
+
raise InvalidValueError.new(fld.name, val)
|
95
|
+
end
|
96
|
+
|
97
|
+
buf.append(:int32, val, fn)
|
98
|
+
else
|
99
|
+
buf.append(fld.type, val, fn)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
buf
|
104
|
+
end
|
105
|
+
|
106
|
+
def valid_enum?(mod, val)
|
107
|
+
mod.constants.any? {|name| mod.const_get(name) == val }
|
108
|
+
end
|
109
|
+
|
110
|
+
def validate!
|
111
|
+
fields.values.each do |fld|
|
112
|
+
if fld.rule == :required && self[fld.name].nil?
|
113
|
+
raise RequiredFieldNotSetError, fld.name
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
end
|
119
|
+
|
120
|
+
|
121
|
+
module Decode
|
122
|
+
def decode(buf, o=self.new)
|
123
|
+
if ! buf.is_a?(Buffer)
|
124
|
+
buf = Buffer.new(buf)
|
125
|
+
end
|
126
|
+
|
127
|
+
# TODO: test for incomplete buffer
|
128
|
+
while buf.length > 0
|
129
|
+
fn, wire = buf.read_info
|
130
|
+
|
131
|
+
fld = fields[fn]
|
132
|
+
|
133
|
+
# We don't have a field for with index fn.
|
134
|
+
# Ignore this data and move on.
|
135
|
+
if fld.nil?
|
136
|
+
buf.skip(wire)
|
137
|
+
next
|
138
|
+
end
|
139
|
+
|
140
|
+
exp = Buffer.wire_for(fld.type)
|
141
|
+
if wire != exp
|
142
|
+
raise WrongTypeError.new(fld.name, exp, wire)
|
143
|
+
end
|
144
|
+
|
145
|
+
if fld.rule == :repeated && fld.opts[:packed]
|
146
|
+
len = buf.read_uint64
|
147
|
+
tmp = Buffer.new(buf.read(len))
|
148
|
+
o[fld.name] ||= []
|
149
|
+
while tmp.length > 0
|
150
|
+
o[fld.name] << tmp.read(fld.type)
|
151
|
+
end
|
152
|
+
|
153
|
+
next
|
154
|
+
end
|
155
|
+
|
156
|
+
val = buf.read(fld.type)
|
157
|
+
if fld.rule == :repeated
|
158
|
+
(o[fld.name] ||= []) << val
|
159
|
+
else
|
160
|
+
o[fld.name] = val
|
161
|
+
end
|
162
|
+
|
163
|
+
end
|
164
|
+
|
165
|
+
# Set defaults
|
166
|
+
fields.values.each do |f|
|
167
|
+
o[f.name] ||= f.opts[:default]
|
168
|
+
end
|
169
|
+
|
170
|
+
o.validate!
|
171
|
+
|
172
|
+
o
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
|
177
|
+
def self.included(o)
|
178
|
+
o.extend Dsl
|
179
|
+
o.extend Decode
|
180
|
+
o.send(:include, Encode)
|
181
|
+
end
|
182
|
+
|
183
|
+
def initialize(attrs={})
|
184
|
+
fields.values.each do |fld|
|
185
|
+
self[fld.name] = attrs[fld.name]
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
def fields
|
190
|
+
self.class.fields
|
191
|
+
end
|
192
|
+
|
193
|
+
def [](k)
|
194
|
+
__send__(k)
|
195
|
+
end
|
196
|
+
|
197
|
+
def []=(k, v)
|
198
|
+
__send__("#{k}=", v)
|
199
|
+
end
|
200
|
+
|
201
|
+
end
|
202
|
+
|
203
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
module Beefcake
|
2
|
+
|
3
|
+
class Buffer
|
4
|
+
|
5
|
+
MinUint32 = 0
|
6
|
+
MaxUint32 = (1<<32)-1
|
7
|
+
MinInt32 = -(1<<31)
|
8
|
+
MaxInt32 = (1<<31)-1
|
9
|
+
|
10
|
+
MinUint64 = 0
|
11
|
+
MaxUint64 = (1<<64)-1
|
12
|
+
MinInt64 = -(1<<63)
|
13
|
+
MaxInt64 = (1<<63)-1
|
14
|
+
|
15
|
+
def self.wire_for(type)
|
16
|
+
case type
|
17
|
+
when :int32, :uint32, :sint32, :int64, :uint64, :sint64, :bool
|
18
|
+
0
|
19
|
+
when :fixed64, :sfixed64, :double
|
20
|
+
1
|
21
|
+
when :string, :bytes
|
22
|
+
2
|
23
|
+
when :fixed32, :sfixed32, :float
|
24
|
+
5
|
25
|
+
else
|
26
|
+
if encodable?(type)
|
27
|
+
2
|
28
|
+
else
|
29
|
+
raise UnknownType, type
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.encodable?(type)
|
35
|
+
return false if ! type.is_a?(Class)
|
36
|
+
pims = type.public_instance_methods
|
37
|
+
pims.include?(:encode) || pims.include?("encode")
|
38
|
+
end
|
39
|
+
|
40
|
+
attr_accessor :buf
|
41
|
+
|
42
|
+
alias :to_s :buf
|
43
|
+
alias :to_str :buf
|
44
|
+
|
45
|
+
class OutOfRangeError < StandardError
|
46
|
+
def initialize(n)
|
47
|
+
super("Value of of range: %d" % [n])
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
class BufferOverflowError < StandardError
|
52
|
+
def initialize(s)
|
53
|
+
super("Too many bytes read for %s" % [s])
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
class UnknownType < StandardError
|
58
|
+
def initialize(s)
|
59
|
+
super("Unknown type '%s'" % [s])
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def initialize(buf="")
|
64
|
+
self.buf = buf
|
65
|
+
end
|
66
|
+
|
67
|
+
if ''.respond_to?(:force_encoding)
|
68
|
+
def buf=(buf)
|
69
|
+
@buf = buf.force_encoding('BINARY')
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def length
|
74
|
+
@buf.respond_to?(:bytesize) ? @buf.bytesize : @buf.length
|
75
|
+
end
|
76
|
+
|
77
|
+
def <<(bytes)
|
78
|
+
buf << bytes
|
79
|
+
end
|
80
|
+
|
81
|
+
def read(n)
|
82
|
+
case n
|
83
|
+
when Symbol
|
84
|
+
__send__("read_#{n}")
|
85
|
+
else
|
86
|
+
buf.slice!(0, n)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
91
|
+
|
92
|
+
end
|