chrisk-protopuffs 0.2.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.txt +18 -0
- data/README.rdoc +3 -0
- data/VERSION.yml +4 -0
- data/lib/protopuffs/message/base.rb +90 -0
- data/lib/protopuffs/message/field.rb +226 -0
- data/lib/protopuffs/message/wire_type.rb +10 -0
- data/lib/protopuffs/parser/parser.rb +24 -0
- data/lib/protopuffs/parser/protocol_buffer.treetop +124 -0
- data/lib/protopuffs.rb +27 -0
- data/test/abstract_syntax_tree_test.rb +142 -0
- data/test/fixtures/proto/person.proto +5 -0
- data/test/message_base_test.rb +98 -0
- data/test/message_field_test.rb +34 -0
- data/test/parse_tree_test.rb +206 -0
- data/test/protopuffs_test.rb +36 -0
- data/test/test_helper.rb +71 -0
- data/test/wire_format_test.rb +231 -0
- metadata +105 -0
data/LICENSE.txt
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
2
|
+
a copy of this software and associated documentation files (the
|
3
|
+
"Software"), to deal in the Software without restriction, including
|
4
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
5
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
6
|
+
permit persons to whom the Software is furnished to do so, subject to
|
7
|
+
the following conditions:
|
8
|
+
|
9
|
+
The above copyright notice and this permission notice shall be
|
10
|
+
included in all copies or substantial portions of the Software.
|
11
|
+
|
12
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
13
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
14
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
15
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
16
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
17
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
18
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
data/VERSION.yml
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
module Protopuffs
|
2
|
+
module Message
|
3
|
+
|
4
|
+
class Base
|
5
|
+
|
6
|
+
class << self
|
7
|
+
attr_reader :fields
|
8
|
+
|
9
|
+
def define_message_class(name, fields)
|
10
|
+
name = name.capitalize
|
11
|
+
self.check_fields_for_errors(name, fields)
|
12
|
+
Message.send(:remove_const, name) if Message.const_defined?(name)
|
13
|
+
klass = Message.const_set(name, Class.new(self))
|
14
|
+
klass.instance_variable_set(:@fields, fields)
|
15
|
+
fields.each do |field|
|
16
|
+
klass.send(:attr_accessor, field.identifier)
|
17
|
+
end
|
18
|
+
klass
|
19
|
+
end
|
20
|
+
|
21
|
+
def check_fields_for_errors(name, fields)
|
22
|
+
tags = fields.map { |field| field.tag }
|
23
|
+
if tags.uniq.size != fields.size
|
24
|
+
raise ParseError, "A tag was reused in the descriptor for message #{name}. Please check that each tag is unique."
|
25
|
+
end
|
26
|
+
|
27
|
+
if tags.any? { |tag| tag < 1 || tag > 536_870_911 }
|
28
|
+
raise ParseError, "A tag is out of range in the descriptor for message #{name}. Please check that each tag is in the range 1 <= x <= 536,870,911."
|
29
|
+
end
|
30
|
+
|
31
|
+
if tags.any? { |tag| (19000..19999).include?(tag) }
|
32
|
+
raise ParseError, "A tag is invalid in the descriptor for message #{name}. Please check that each tag is not in the reserved range 19,000 <= x <= 19,999."
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
attr_reader :buffer
|
38
|
+
|
39
|
+
def initialize
|
40
|
+
if self.class == Base
|
41
|
+
raise "#{self.class} should not be instantiated directly. Use the factory #{self.class}.define_message_class instead."
|
42
|
+
end
|
43
|
+
@buffer = StringIO.new
|
44
|
+
end
|
45
|
+
|
46
|
+
def to_wire_format
|
47
|
+
self.class.fields.each do |field|
|
48
|
+
value = send(field.identifier)
|
49
|
+
@buffer.write field.to_wire_format_with_value(value) unless value.nil?
|
50
|
+
end
|
51
|
+
@buffer.string
|
52
|
+
end
|
53
|
+
|
54
|
+
def from_wire_format(buffer)
|
55
|
+
@buffer = buffer
|
56
|
+
until @buffer.eof?
|
57
|
+
tag, value_bytes = MessageField.shift_tag_and_value_bytes(@buffer)
|
58
|
+
field = self.class.fields.find { |field| field.tag == tag }
|
59
|
+
next if field.nil?
|
60
|
+
|
61
|
+
value = field.decode(value_bytes)
|
62
|
+
if field.repeated? && send(field.identifier).nil?
|
63
|
+
send("#{field.identifier}=", [value])
|
64
|
+
elsif field.repeated?
|
65
|
+
send(field.identifier) << value
|
66
|
+
else
|
67
|
+
send("#{field.identifier}=", value)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
set_values_for_missing_optional_fields
|
71
|
+
end
|
72
|
+
|
73
|
+
def set_values_for_missing_optional_fields
|
74
|
+
self.class.fields.select { |field| field.optional? }.each do |field|
|
75
|
+
send("#{field.identifier}=", field.default) if send(field.identifier).nil?
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def ==(other)
|
80
|
+
return false if self.class != other.class
|
81
|
+
self.class.fields.each do |field|
|
82
|
+
return false if send(field.identifier) != other.send(field.identifier)
|
83
|
+
end
|
84
|
+
true
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,226 @@
|
|
1
|
+
module Protopuffs
|
2
|
+
|
3
|
+
class MessageField
|
4
|
+
attr_reader :modifier, :type, :identifier, :tag, :default
|
5
|
+
|
6
|
+
def initialize(modifier, type, identifier, tag, default = nil)
|
7
|
+
@modifier = modifier
|
8
|
+
@type = type
|
9
|
+
@identifier = identifier
|
10
|
+
@tag = tag
|
11
|
+
@default = default
|
12
|
+
|
13
|
+
set_default_for_type if optional? && @default.nil?
|
14
|
+
end
|
15
|
+
|
16
|
+
def set_default_for_type
|
17
|
+
if numeric?
|
18
|
+
@default = 0
|
19
|
+
elsif @type == "string"
|
20
|
+
@default = ""
|
21
|
+
elsif @type == "bool"
|
22
|
+
@default = false
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def wire_type
|
27
|
+
case @type
|
28
|
+
when "int32", "int64", "uint32", "uint64", "bool" then WireType::VARINT
|
29
|
+
when "double", "fixed64" then WireType::FIXED64
|
30
|
+
when "string", "bytes" then WireType::LENGTH_DELIMITED
|
31
|
+
when "float", "fixed32" then WireType::FIXED32
|
32
|
+
else WireType::LENGTH_DELIMITED # embedded messages
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def key
|
37
|
+
(@tag << 3) | wire_type
|
38
|
+
end
|
39
|
+
|
40
|
+
def repeated?
|
41
|
+
@modifier == "repeated"
|
42
|
+
end
|
43
|
+
|
44
|
+
def optional?
|
45
|
+
@modifier == "optional"
|
46
|
+
end
|
47
|
+
|
48
|
+
def numeric?
|
49
|
+
%w(double float int32 int64 uint32 unit64 sint32 sint64 fixed32 fixed64
|
50
|
+
sfixed32 sfixed64).include?(@type)
|
51
|
+
end
|
52
|
+
|
53
|
+
def embedded_message?
|
54
|
+
wire_type == WireType::LENGTH_DELIMITED && @type != "string" && @type != "bytes"
|
55
|
+
end
|
56
|
+
|
57
|
+
def to_wire_format_with_value(value)
|
58
|
+
field_encoder = lambda { |val| self.class.varint_encode(key) + encode(val) }
|
59
|
+
|
60
|
+
if repeated?
|
61
|
+
value.map(&field_encoder).join
|
62
|
+
else
|
63
|
+
field_encoder.call(value)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def encode(value)
|
68
|
+
case wire_type
|
69
|
+
when WireType::VARINT
|
70
|
+
value = (value ? 1 : 0) if @type == "bool"
|
71
|
+
value_bytes = self.class.varint_encode(value)
|
72
|
+
when WireType::LENGTH_DELIMITED
|
73
|
+
if value.respond_to?(:to_wire_format)
|
74
|
+
embedded_bytes = value.to_wire_format
|
75
|
+
value_bytes = self.class.varint_encode(embedded_bytes.size) + embedded_bytes
|
76
|
+
else
|
77
|
+
value_bytes = self.class.varint_encode(value.size)
|
78
|
+
value_bytes += self.class.string_encode(value) if @type == "string"
|
79
|
+
value_bytes += value if @type == "bytes"
|
80
|
+
end
|
81
|
+
when WireType::FIXED32
|
82
|
+
value_bytes = self.class.float_encode(value) if @type == "float"
|
83
|
+
value_bytes = self.class.fixed32_encode(value) if @type == "fixed32"
|
84
|
+
when WireType::FIXED64
|
85
|
+
value_bytes = self.class.double_encode(value) if @type == "double"
|
86
|
+
value_bytes = self.class.fixed64_encode(value) if @type == "fixed64"
|
87
|
+
end
|
88
|
+
value_bytes
|
89
|
+
end
|
90
|
+
|
91
|
+
|
92
|
+
def self.varint_encode(value)
|
93
|
+
return [0].pack('C') if value.zero?
|
94
|
+
bytes = []
|
95
|
+
until value.zero?
|
96
|
+
byte = 0
|
97
|
+
7.times do |i|
|
98
|
+
byte |= (value & 1) << i
|
99
|
+
value >>= 1
|
100
|
+
end
|
101
|
+
byte |= 0b10000000
|
102
|
+
bytes << byte
|
103
|
+
end
|
104
|
+
bytes[-1] &= 0b01111111
|
105
|
+
bytes.pack('C*')
|
106
|
+
end
|
107
|
+
|
108
|
+
def self.string_encode(value)
|
109
|
+
value.unpack('U*').pack('C*')
|
110
|
+
end
|
111
|
+
|
112
|
+
def self.float_encode(value)
|
113
|
+
[value].pack('e')
|
114
|
+
end
|
115
|
+
|
116
|
+
def self.double_encode(value)
|
117
|
+
[value].pack('E')
|
118
|
+
end
|
119
|
+
|
120
|
+
def self.fixed64_encode(value)
|
121
|
+
[value].pack('Q')
|
122
|
+
end
|
123
|
+
|
124
|
+
def self.fixed32_encode(value)
|
125
|
+
[value].pack('V')
|
126
|
+
end
|
127
|
+
|
128
|
+
# note: returns two values
|
129
|
+
def self.shift_tag_and_value_bytes(buffer)
|
130
|
+
bits = 0
|
131
|
+
bytes = shift_varint(buffer)
|
132
|
+
bytes.each_with_index do |byte, index|
|
133
|
+
byte &= 0b01111111
|
134
|
+
bits |= byte << (7 * index)
|
135
|
+
end
|
136
|
+
wire_type = bits & 0b00000111
|
137
|
+
tag = bits >> 3
|
138
|
+
|
139
|
+
case wire_type
|
140
|
+
when WireType::VARINT
|
141
|
+
value_bytes = shift_varint(buffer)
|
142
|
+
when WireType::LENGTH_DELIMITED
|
143
|
+
value_bytes = shift_length_delimited(buffer)
|
144
|
+
when WireType::FIXED32
|
145
|
+
value_bytes = shift_fixed32(buffer)
|
146
|
+
when WireType::FIXED64
|
147
|
+
value_bytes = shift_fixed64(buffer)
|
148
|
+
end
|
149
|
+
|
150
|
+
[tag, value_bytes]
|
151
|
+
end
|
152
|
+
|
153
|
+
def self.shift_varint(buffer)
|
154
|
+
bytes = []
|
155
|
+
begin
|
156
|
+
byte = buffer.readchar
|
157
|
+
bytes << (byte & 0b01111111)
|
158
|
+
end while byte >> 7 == 1
|
159
|
+
bytes
|
160
|
+
end
|
161
|
+
|
162
|
+
def self.shift_length_delimited(buffer)
|
163
|
+
bytes = shift_varint(buffer)
|
164
|
+
value_length = self.varint_decode(bytes)
|
165
|
+
buffer.read(value_length)
|
166
|
+
end
|
167
|
+
|
168
|
+
def self.shift_fixed32(buffer)
|
169
|
+
buffer.read(4)
|
170
|
+
end
|
171
|
+
|
172
|
+
def self.shift_fixed64(buffer)
|
173
|
+
buffer.read(8)
|
174
|
+
end
|
175
|
+
|
176
|
+
def decode(value_bytes)
|
177
|
+
case wire_type
|
178
|
+
when WireType::VARINT
|
179
|
+
value = self.class.varint_decode(value_bytes)
|
180
|
+
if @type == "bool"
|
181
|
+
value = true if value == 1
|
182
|
+
value = false if value == 0
|
183
|
+
end
|
184
|
+
when WireType::LENGTH_DELIMITED
|
185
|
+
if embedded_message?
|
186
|
+
value = Message.const_get(@type).new
|
187
|
+
value.from_wire_format(StringIO.new(value_bytes))
|
188
|
+
else
|
189
|
+
value = value_bytes
|
190
|
+
end
|
191
|
+
when WireType::FIXED32
|
192
|
+
value = self.class.float_decode(value_bytes) if @type == "float"
|
193
|
+
value = self.class.fixed32_decode(value_bytes) if @type == "fixed32"
|
194
|
+
when WireType::FIXED64
|
195
|
+
value = self.class.double_decode(value_bytes) if @type == "double"
|
196
|
+
value = self.class.fixed64_decode(value_bytes) if @type == "fixed64"
|
197
|
+
end
|
198
|
+
value
|
199
|
+
end
|
200
|
+
|
201
|
+
def self.varint_decode(bytes)
|
202
|
+
value = 0
|
203
|
+
bytes.each_with_index do |byte, index|
|
204
|
+
value |= byte << (7 * index)
|
205
|
+
end
|
206
|
+
value
|
207
|
+
end
|
208
|
+
|
209
|
+
def self.float_decode(bytes)
|
210
|
+
bytes.unpack('e').first
|
211
|
+
end
|
212
|
+
|
213
|
+
def self.double_decode(bytes)
|
214
|
+
bytes.unpack('E').first
|
215
|
+
end
|
216
|
+
|
217
|
+
def self.fixed64_decode(bytes)
|
218
|
+
bytes.unpack('Q').first
|
219
|
+
end
|
220
|
+
|
221
|
+
def self.fixed32_decode(bytes)
|
222
|
+
bytes.unpack('V').first
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Protopuffs
|
2
|
+
|
3
|
+
# The Protopuffs::Parser module holds wrapper classes that parse input
|
4
|
+
# strings using Treetop. They also set off the building of an abstract
|
5
|
+
# syntax tree from Treetop's parse tree.
|
6
|
+
module Parser
|
7
|
+
|
8
|
+
class ProtocolBufferDescriptor
|
9
|
+
def initialize
|
10
|
+
Treetop.load "lib/protopuffs/parser/protocol_buffer"
|
11
|
+
@parser = Protopuffs::ProtocolBufferParser.new
|
12
|
+
end
|
13
|
+
|
14
|
+
def parse(input)
|
15
|
+
parse_tree = @parser.parse(input)
|
16
|
+
parse_tree.build
|
17
|
+
parse_tree
|
18
|
+
rescue
|
19
|
+
raise ParseError
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
module Protopuffs
|
2
|
+
grammar ProtocolBuffer
|
3
|
+
|
4
|
+
rule proto
|
5
|
+
s? proto_entries s? {
|
6
|
+
def messages
|
7
|
+
proto_entries.entries.select { |p| p.respond_to?(:message?) }
|
8
|
+
end
|
9
|
+
|
10
|
+
def build
|
11
|
+
messages.each { |message| message.build }
|
12
|
+
end
|
13
|
+
}
|
14
|
+
end
|
15
|
+
|
16
|
+
rule proto_entries
|
17
|
+
proto_entry rest:(s? proto_entry)* {
|
18
|
+
def entries
|
19
|
+
rest.elements.inject([proto_entry]) { |a, s_and_pe| a << s_and_pe.proto_entry }
|
20
|
+
end
|
21
|
+
}
|
22
|
+
end
|
23
|
+
|
24
|
+
rule proto_entry
|
25
|
+
message {
|
26
|
+
def message?
|
27
|
+
true
|
28
|
+
end
|
29
|
+
}
|
30
|
+
end
|
31
|
+
|
32
|
+
rule message
|
33
|
+
"message" s identifier s "{" s? body:message_body? s? "}" {
|
34
|
+
def name
|
35
|
+
identifier.text_value
|
36
|
+
end
|
37
|
+
|
38
|
+
def build
|
39
|
+
Message::Base.define_message_class(name, body.empty? ? [] : body.build)
|
40
|
+
end
|
41
|
+
}
|
42
|
+
end
|
43
|
+
|
44
|
+
rule message_body
|
45
|
+
field rest:(s? field)* {
|
46
|
+
def fields
|
47
|
+
rest.elements.inject([field]) { |a, s_and_field| a << s_and_field.field }
|
48
|
+
end
|
49
|
+
|
50
|
+
def build
|
51
|
+
fields.map { |field| field.build }
|
52
|
+
end
|
53
|
+
}
|
54
|
+
end
|
55
|
+
|
56
|
+
rule field
|
57
|
+
modifier s type s identifier s "=" s integer s? option:field_option? ";" {
|
58
|
+
def default
|
59
|
+
option.empty? ? nil : option.literal.obj
|
60
|
+
end
|
61
|
+
|
62
|
+
def build
|
63
|
+
MessageField.new(modifier.text_value, type.text_value, identifier.text_value,
|
64
|
+
integer.text_value.to_i, default)
|
65
|
+
end
|
66
|
+
}
|
67
|
+
end
|
68
|
+
|
69
|
+
rule field_option
|
70
|
+
"[" s? "default" s? "=" s? literal s? "]"
|
71
|
+
end
|
72
|
+
|
73
|
+
rule modifier
|
74
|
+
"required" / "optional" / "repeated"
|
75
|
+
end
|
76
|
+
|
77
|
+
rule type
|
78
|
+
"double" / "float" / "int32" / "int64" / "uint32" / "uint64" /
|
79
|
+
"sint32" / "sint64" / "fixed32" / "fixed64" / "sfixed32" /
|
80
|
+
"sfixed64" / "bool" / "string" / "bytes" / identifier
|
81
|
+
end
|
82
|
+
|
83
|
+
rule literal
|
84
|
+
integer / string
|
85
|
+
end
|
86
|
+
|
87
|
+
rule integer
|
88
|
+
[\d]+ {
|
89
|
+
def obj
|
90
|
+
text_value.to_i
|
91
|
+
end
|
92
|
+
}
|
93
|
+
end
|
94
|
+
|
95
|
+
rule string
|
96
|
+
quote chars:[\w]* quote {
|
97
|
+
def obj
|
98
|
+
chars.text_value
|
99
|
+
end
|
100
|
+
}
|
101
|
+
end
|
102
|
+
|
103
|
+
rule quote
|
104
|
+
["']
|
105
|
+
end
|
106
|
+
|
107
|
+
rule identifier
|
108
|
+
[A-Za-z_] [\w_]*
|
109
|
+
end
|
110
|
+
|
111
|
+
rule s
|
112
|
+
(whitespace / comment_to_eol)+
|
113
|
+
end
|
114
|
+
|
115
|
+
rule comment_to_eol
|
116
|
+
'//' (!"\n" .)*
|
117
|
+
end
|
118
|
+
|
119
|
+
rule whitespace
|
120
|
+
[ \t\n\r]
|
121
|
+
end
|
122
|
+
|
123
|
+
end
|
124
|
+
end
|
data/lib/protopuffs.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
require "protopuffs/message/base"
|
2
|
+
require "protopuffs/message/field"
|
3
|
+
require "protopuffs/message/wire_type"
|
4
|
+
require "protopuffs/parser/parser"
|
5
|
+
|
6
|
+
|
7
|
+
module Protopuffs
|
8
|
+
class ParseError < StandardError; end
|
9
|
+
|
10
|
+
def self.proto_load_path
|
11
|
+
@proto_load_path ||= []
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.proto_load_path=(paths)
|
15
|
+
@proto_load_path = paths
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.load_message_classes
|
19
|
+
parser = Protopuffs::Parser::ProtocolBufferDescriptor.new
|
20
|
+
|
21
|
+
@proto_load_path.each do |path|
|
22
|
+
Dir.glob(File.join(path, "**/*.proto")) do |descriptor_path|
|
23
|
+
parser.parse(File.read(descriptor_path))
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,142 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/test_helper'
|
2
|
+
|
3
|
+
class AbstractSyntaxTreeTest < Test::Unit::TestCase
|
4
|
+
|
5
|
+
context "a protocol buffer descriptor" do
|
6
|
+
setup do
|
7
|
+
@parser = Protopuffs::Parser::ProtocolBufferDescriptor.new
|
8
|
+
end
|
9
|
+
|
10
|
+
context "with an empty Person message" do
|
11
|
+
setup do
|
12
|
+
@descriptor = "message Person {}"
|
13
|
+
end
|
14
|
+
|
15
|
+
should "create one MessageDescriptor" do
|
16
|
+
Protopuffs::Message::Base.expects(:define_message_class).once.with("Person", [])
|
17
|
+
@parser.parse(@descriptor)
|
18
|
+
end
|
19
|
+
|
20
|
+
should "not create any MessageFields" do
|
21
|
+
Protopuffs::MessageField.expects(:new).never
|
22
|
+
@parser.parse(@descriptor)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
context "with two empty messages Apple and Orange" do
|
28
|
+
setup do
|
29
|
+
@descriptor = <<-proto
|
30
|
+
message Apple {}
|
31
|
+
message Orange {}
|
32
|
+
proto
|
33
|
+
end
|
34
|
+
|
35
|
+
should "create two MessageDescriptors" do
|
36
|
+
Protopuffs::Message::Base.expects(:define_message_class).once.with("Apple", []).in_sequence
|
37
|
+
Protopuffs::Message::Base.expects(:define_message_class).once.with("Orange", []).in_sequence
|
38
|
+
@parser.parse(@descriptor)
|
39
|
+
end
|
40
|
+
|
41
|
+
should "not create any MessageFields" do
|
42
|
+
Protopuffs::MessageField.expects(:new).never
|
43
|
+
@parser.parse(@descriptor)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
context "with a Person message including a name field" do
|
49
|
+
setup do
|
50
|
+
@descriptor = <<-proto
|
51
|
+
message Person {
|
52
|
+
required string name = 1;
|
53
|
+
}
|
54
|
+
proto
|
55
|
+
end
|
56
|
+
|
57
|
+
should "create one 'Person' MessageDescriptor with one field" do
|
58
|
+
Protopuffs::Message::Base.expects(:define_message_class).once.with("Person", responds_with(:size, 1))
|
59
|
+
@parser.parse(@descriptor)
|
60
|
+
end
|
61
|
+
|
62
|
+
should "create one MessageField with correct options" do
|
63
|
+
Protopuffs::MessageField.expects(:new).once.with("required", "string", "name", 1, nil).returns(stub(:tag => 1, :identifier => "name"))
|
64
|
+
@parser.parse(@descriptor)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
|
69
|
+
context "with a Person message including three fields" do
|
70
|
+
setup do
|
71
|
+
@descriptor = <<-proto
|
72
|
+
message Person {
|
73
|
+
required string name = 1;
|
74
|
+
required int32 id = 2;
|
75
|
+
optional string email = 3;
|
76
|
+
}
|
77
|
+
proto
|
78
|
+
end
|
79
|
+
|
80
|
+
should "create one 'Person' MessageDescriptor with three fields" do
|
81
|
+
Protopuffs::Message::Base.expects(:define_message_class).once.with("Person", responds_with(:size, 3))
|
82
|
+
@parser.parse(@descriptor)
|
83
|
+
end
|
84
|
+
|
85
|
+
should "create three MessageFields with correct options" do
|
86
|
+
Protopuffs::MessageField.expects(:new).once.with("required", "string", "name", 1, nil).in_sequence.returns(stub(:tag => 1, :identifier => "name"))
|
87
|
+
Protopuffs::MessageField.expects(:new).once.with("required", "int32", "id", 2, nil).in_sequence.returns(stub(:tag => 2, :identifier => "id"))
|
88
|
+
Protopuffs::MessageField.expects(:new).once.with("optional", "string", "email", 3, nil).in_sequence.returns(stub(:tag => 3, :identifier => "email"))
|
89
|
+
@parser.parse(@descriptor)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
|
94
|
+
context "with a message including two fields with defaults and one without" do
|
95
|
+
setup do
|
96
|
+
@descriptor = <<-proto
|
97
|
+
message Person {
|
98
|
+
required string name = 1;
|
99
|
+
optional string language = 2 [default = "en"];
|
100
|
+
optional int32 account_code = 3 [default = 0];
|
101
|
+
}
|
102
|
+
proto
|
103
|
+
end
|
104
|
+
|
105
|
+
should "create one 'Person' MessageDescriptor with three fields" do
|
106
|
+
Protopuffs::Message::Base.expects(:define_message_class).once.with("Person", responds_with(:size, 3))
|
107
|
+
@parser.parse(@descriptor)
|
108
|
+
end
|
109
|
+
|
110
|
+
should "create three MessageFields with correct options" do
|
111
|
+
Protopuffs::MessageField.expects(:new).once.with("required", "string", "name", 1, nil).in_sequence.returns(stub(:tag => 1, :identifier => "name"))
|
112
|
+
Protopuffs::MessageField.expects(:new).once.with("optional", "string", "language", 2, "en").in_sequence.returns(stub(:tag => 2, :identifier => "language"))
|
113
|
+
Protopuffs::MessageField.expects(:new).once.with("optional", "int32", "account_code", 3, 0).in_sequence.returns(stub(:tag => 3, :identifier => "account_code"))
|
114
|
+
@parser.parse(@descriptor)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
|
119
|
+
context "with a message including a user-typed field" do
|
120
|
+
setup do
|
121
|
+
@descriptor = <<-proto
|
122
|
+
message Person {
|
123
|
+
required string name = 1;
|
124
|
+
repeated Address addresses = 2;
|
125
|
+
}
|
126
|
+
proto
|
127
|
+
end
|
128
|
+
|
129
|
+
should "create one 'Person' MessageDescriptor with two fields" do
|
130
|
+
Protopuffs::Message::Base.expects(:define_message_class).once.with("Person", responds_with(:size, 2))
|
131
|
+
@parser.parse(@descriptor)
|
132
|
+
end
|
133
|
+
|
134
|
+
should "create two MessageFields with correct options" do
|
135
|
+
Protopuffs::MessageField.expects(:new).once.with("required", "string", "name", 1, nil).in_sequence.returns(stub(:tag => 1, :identifier => "name"))
|
136
|
+
Protopuffs::MessageField.expects(:new).once.with("repeated", "Address", "addresses", 2, nil).in_sequence.returns(stub(:tag => 2, :identifier => "addresses"))
|
137
|
+
@parser.parse(@descriptor)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
end
|