protobuf 3.7.5 → 3.8.0.pre1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 9c47dbd9243cb8e69f27b6331483bad1f6eb65f2
4
- data.tar.gz: 15ef0eeeb8e59e0fdb5f72f8a3e5d1d3d279f334
3
+ metadata.gz: 36998518e999db66696a856ca6489d2e7b4063a6
4
+ data.tar.gz: 9b342418d7b1bf86249fcb2ed4924b9c2cfc28a3
5
5
  SHA512:
6
- metadata.gz: 60930731ad74a300c1ee588165ba17279286f512c6eb944a0b2c8ab4f9103c564866cd8eb2a76e25e66f0504d7fa9f5eccad8ea9a75c8816354ce90e1920c531
7
- data.tar.gz: b18f39b0728038ec14b85c256f7d9b2a07bab07eedfdadfcc622b8bb4a68d0ff88f347a5e132947815ec7fafe3b302a764c18633b0f7b4092cbb80c3d6aa422b
6
+ metadata.gz: 481bb0ddef459a46e0f34feab99b584dfdc21503e9198fd3df15bafe59d40b8d1115ef3c796bf531f67dfa60dfa112142affa4ab75c2553a4096abaa99683da4
7
+ data.tar.gz: 9db74e2292597dd416c9c69f299282dc3a7e928a4ff2c5da686a1ca2d8931b8ad5831a904dfec338c602fbd28fa3c833599e19083a3988c00f1827ed002d0422
@@ -1,5 +1,7 @@
1
1
  require 'active_support/core_ext/hash/slice'
2
2
  require 'protobuf/field/field_array'
3
+ require 'protobuf/field/field_hash'
4
+
3
5
  module Protobuf
4
6
  module Field
5
7
  class BaseField
@@ -82,6 +84,8 @@ module Protobuf
82
84
  def default_value
83
85
  @default_value ||= if optional? || required?
84
86
  typed_default_value
87
+ elsif map?
88
+ ::Protobuf::Field::FieldHash.new(self).freeze
85
89
  elsif repeated?
86
90
  ::Protobuf::Field::FieldArray.new(self).freeze
87
91
  else
@@ -113,6 +117,10 @@ module Protobuf
113
117
  false
114
118
  end
115
119
 
120
+ def map?
121
+ @is_map ||= repeated_message? && type_class.get_option(:map_entry)
122
+ end
123
+
116
124
  def optional?
117
125
  @optional
118
126
  end
@@ -136,6 +144,16 @@ module Protobuf
136
144
  # FIXME: need to cleanup (rename) this warthog of a method.
137
145
  def set(message_instance, bytes)
138
146
  return message_instance[name] = decode(bytes) unless repeated?
147
+
148
+ if map?
149
+ hash = message_instance[name]
150
+ entry = decode(bytes)
151
+ # decoded value could be nil for an
152
+ # enum value that is not recognized
153
+ hash[entry.key] = entry.value unless entry.value.nil?
154
+ return hash[entry.key]
155
+ end
156
+
139
157
  return message_instance[name] << decode(bytes) unless packed?
140
158
 
141
159
  array = message_instance[name]
@@ -0,0 +1,104 @@
1
+ module Protobuf
2
+ module Field
3
+ class FieldHash < Hash
4
+
5
+ ##
6
+ # Attributes
7
+ #
8
+
9
+ attr_reader :field, :key_field, :value_field
10
+
11
+ ##
12
+ # Constructor
13
+ #
14
+
15
+ def initialize(field)
16
+ @field = field
17
+ @key_field = field.type_class.get_field(:key)
18
+ @value_field = field.type_class.get_field(:value)
19
+ end
20
+
21
+ ##
22
+ # Public Instance Methods
23
+ #
24
+
25
+ def []=(key, val)
26
+ super(normalize_key(key), normalize_val(val))
27
+ end
28
+
29
+ alias store []=
30
+
31
+ def replace(val)
32
+ raise_type_error(val) unless val.is_a?(Hash)
33
+ clear
34
+ update(val)
35
+ end
36
+
37
+ def merge!(other)
38
+ raise_type_error(other) unless other.is_a?(Hash)
39
+ # keys and values will be normalized by []= above
40
+ other.each { |k, v| self[k] = v }
41
+ end
42
+
43
+ alias update merge!
44
+
45
+ # Return a hash-representation of the given values for this field type.
46
+ # The value in this case would be the hash itself, right? Unfortunately
47
+ # not because the values of the map could be messages themselves that we
48
+ # need to transform.
49
+ def to_hash_value
50
+ each_with_object({}) do |(key, value), hash|
51
+ hash[key] = value.respond_to?(:to_hash_value) ? value.to_hash_value : value
52
+ end
53
+ end
54
+
55
+ def to_s
56
+ "{#{field.name}}"
57
+ end
58
+
59
+ private
60
+
61
+ ##
62
+ # Private Instance Methods
63
+ #
64
+
65
+ def normalize_key(key)
66
+ normalize(:key, key, key_field)
67
+ end
68
+
69
+ def normalize_val(value)
70
+ normalize(:value, value, value_field)
71
+ end
72
+
73
+ def normalize(what, value, normalize_field)
74
+ raise_type_error(value) if value.nil?
75
+ value = value.to_proto if value.respond_to?(:to_proto)
76
+ fail TypeError, "Unacceptable #{what} #{value} for field #{field.name} of type #{normalize_field.type_class}" unless normalize_field.acceptable?(value)
77
+
78
+ if normalize_field.is_a?(::Protobuf::Field::EnumField)
79
+ fetch_enum(normalize_field.type_class, value)
80
+ elsif normalize_field.is_a?(::Protobuf::Field::MessageField) && value.is_a?(normalize_field.type_class)
81
+ value
82
+ elsif normalize_field.is_a?(::Protobuf::Field::MessageField) && value.respond_to?(:to_hash)
83
+ normalize_field.type_class.new(value.to_hash)
84
+ else
85
+ value
86
+ end
87
+ end
88
+
89
+ def fetch_enum(type, val)
90
+ en = type.fetch(val)
91
+ raise_type_error(val) if en.nil?
92
+ en
93
+ end
94
+
95
+ def raise_type_error(val)
96
+ fail TypeError, <<-TYPE_ERROR
97
+ Expected map value of type '#{key_field.type_class} -> #{value_field.type_class}'
98
+ Got '#{val.class}' for map protobuf field #{field.name}
99
+ TYPE_ERROR
100
+ end
101
+
102
+ end
103
+ end
104
+ end
@@ -19,6 +19,11 @@ module Protobuf
19
19
  #
20
20
  attr_reader :field_options
21
21
 
22
+ def initialize(field_descriptor, enclosing_msg_descriptor, indent_level)
23
+ super(field_descriptor, indent_level)
24
+ @enclosing_msg_descriptor = enclosing_msg_descriptor
25
+ end
26
+
22
27
  def applicable_options
23
28
  # Note on the strange use of `#inspect`:
24
29
  # :boom.inspect #=> ":boom"
@@ -60,7 +65,11 @@ module Protobuf
60
65
 
61
66
  def compile
62
67
  run_once(:compile) do
63
- field_definition = ["#{label} #{type_name}", name, number, applicable_options]
68
+ field_definition = if map?
69
+ ["map #{map_key_type_name}", map_value_type_name, name, number, applicable_options]
70
+ else
71
+ ["#{label} #{type_name}", name, number, applicable_options]
72
+ end
64
73
  puts field_definition.flatten.compact.join(', ')
65
74
  end
66
75
  end
@@ -101,15 +110,28 @@ module Protobuf
101
110
 
102
111
  # Determine the field type
103
112
  def type_name
104
- @type_name ||= begin
105
- case descriptor.type.name
106
- when :TYPE_MESSAGE, :TYPE_ENUM, :TYPE_GROUP then
107
- modulize(descriptor.type_name)
108
- else
109
- type_name = descriptor.type.name.to_s.downcase.sub(/type_/, '')
110
- ":#{type_name}"
111
- end
112
- end
113
+ @type_name ||= determine_type_name(descriptor)
114
+ end
115
+
116
+ # If this field is a map field, this returns a message descriptor that
117
+ # represents the entries in the map. Returns nil if this field is not
118
+ # a map field.
119
+ def map_entry
120
+ @map_entry ||= determine_map_entry
121
+ end
122
+
123
+ def map?
124
+ !map_entry.nil?
125
+ end
126
+
127
+ def map_key_type_name
128
+ return nil if map_entry.nil?
129
+ determine_type_name(map_entry.field.find { |v| v.name == 'key' && v.number == 1 })
130
+ end
131
+
132
+ def map_value_type_name
133
+ return nil if map_entry.nil?
134
+ determine_type_name(map_entry.field.find { |v| v.name == 'value' && v.number == 2 })
113
135
  end
114
136
 
115
137
  private
@@ -145,6 +167,27 @@ module Protobuf
145
167
  descriptor.default_value
146
168
  end
147
169
 
170
+ def determine_type_name(descriptor)
171
+ case descriptor.type.name
172
+ when :TYPE_MESSAGE, :TYPE_ENUM, :TYPE_GROUP then
173
+ modulize(descriptor.type_name)
174
+ else
175
+ type_name = descriptor.type.name.to_s.downcase.sub(/^type_/, '')
176
+ ":#{type_name}"
177
+ end
178
+ end
179
+
180
+ def determine_map_entry
181
+ return nil if @enclosing_msg_descriptor.nil?
182
+ return nil unless descriptor.label.name == :LABEL_REPEATED && descriptor.type.name == :TYPE_MESSAGE
183
+ # find nested message type
184
+ name_parts = descriptor.type_name.split(".")
185
+ return nil if name_parts.size < 2 || name_parts[-2] != @enclosing_msg_descriptor.name
186
+ nested = @enclosing_msg_descriptor.nested_type.find { |e| e.name == name_parts[-1] }
187
+ return nested if !nested.nil? && nested.options.try(:map_entry?)
188
+ nil
189
+ end
190
+
148
191
  end
149
192
  end
150
193
  end
@@ -46,7 +46,7 @@ module Protobuf
46
46
 
47
47
  def add_extension_fields(field_descriptors)
48
48
  field_descriptors.each do |field_descriptor|
49
- @groups[:extension_field] << FieldGenerator.new(field_descriptor, indent_level)
49
+ @groups[:extension_field] << FieldGenerator.new(field_descriptor, nil, indent_level)
50
50
  end
51
51
  end
52
52
 
@@ -61,18 +61,22 @@ module Protobuf
61
61
 
62
62
  def add_message_declarations(descriptors)
63
63
  descriptors.each do |descriptor|
64
+ # elide synthetic map entry messages (we handle map fields differently)
65
+ next if descriptor.options.try(:map_entry?) { false }
64
66
  @groups[:message_declaration] << MessageGenerator.new(descriptor, indent_level, :declaration => true)
65
67
  end
66
68
  end
67
69
 
68
- def add_message_fields(field_descriptors)
70
+ def add_message_fields(field_descriptors, msg_descriptor)
69
71
  field_descriptors.each do |field_descriptor|
70
- @groups[:field] << FieldGenerator.new(field_descriptor, indent_level)
72
+ @groups[:field] << FieldGenerator.new(field_descriptor, msg_descriptor, indent_level)
71
73
  end
72
74
  end
73
75
 
74
76
  def add_messages(descriptors, options = {})
75
77
  descriptors.each do |descriptor|
78
+ # elide synthetic map entry message (we handle map fields differently)
79
+ next if descriptor.options.try(:map_entry?) { false }
76
80
  @groups[:message] << MessageGenerator.new(descriptor, indent_level, options)
77
81
  end
78
82
  end
@@ -44,7 +44,7 @@ module Protobuf
44
44
  group.add_messages(descriptor.nested_type, :extension_fields => @extension_fields, :namespace => type_namespace)
45
45
  group.add_comment(:options, 'Message Options')
46
46
  group.add_options(descriptor.options) if options?
47
- group.add_message_fields(descriptor.field)
47
+ group.add_message_fields(descriptor.field, descriptor)
48
48
  self.class.validate_tags(fully_qualified_type_namespace, descriptor.field.map(&:number))
49
49
 
50
50
  group.add_comment(:extension_range, 'Extension Fields')
@@ -50,7 +50,7 @@ module Protobuf
50
50
 
51
51
  def clear!
52
52
  @values.delete_if do |_, value|
53
- if value.is_a?(::Protobuf::Field::FieldArray)
53
+ if value.is_a?(::Protobuf::Field::FieldArray) || value.is_a?(::Protobuf::Field::FieldHash)
54
54
  value.clear
55
55
  false
56
56
  else
@@ -86,6 +86,17 @@ module Protobuf
86
86
  fail ::Protobuf::SerializationError, "Required field #{self.class.name}##{field.name} does not have a value." if field.required?
87
87
  next
88
88
  end
89
+ if field.map?
90
+ # on-the-wire, maps are represented like an array of entries where
91
+ # each entry is a message of two fields, key and value.
92
+ array = Array.new(value.size)
93
+ i = 0
94
+ value.each do |k, v|
95
+ array[i] = field.type_class.new(:key => k, :value => v)
96
+ i += 1
97
+ end
98
+ value = array
99
+ end
89
100
 
90
101
  yield(field, value)
91
102
  end
@@ -151,8 +162,9 @@ module Protobuf
151
162
 
152
163
  def [](name)
153
164
  field = self.class.get_field(name, true)
154
- return @values[field.fully_qualified_name] ||= ::Protobuf::Field::FieldArray.new(field) if field.repeated?
155
165
 
166
+ return @values[field.fully_qualified_name] ||= ::Protobuf::Field::FieldHash.new(field) if field.map?
167
+ return @values[field.fully_qualified_name] ||= ::Protobuf::Field::FieldArray.new(field) if field.repeated?
156
168
  @values.fetch(field.fully_qualified_name, field.default_value)
157
169
  rescue # not having a field should be the exceptional state
158
170
  raise if field
@@ -184,9 +196,24 @@ module Protobuf
184
196
 
185
197
  private
186
198
 
199
+ # rubocop:disable Metrics/MethodLength
187
200
  def set_field(name, value, ignore_nil_for_repeated)
188
201
  if (field = self.class.get_field(name, true))
189
- if field.repeated?
202
+ if field.map?
203
+ unless value.is_a?(Hash)
204
+ fail TypeError, <<-TYPE_ERROR
205
+ Expected map value
206
+ Got '#{value.class}' for map protobuf field #{field.name}
207
+ TYPE_ERROR
208
+ end
209
+
210
+ if value.empty?
211
+ @values.delete(field.fully_qualified_name)
212
+ else
213
+ @values[field.fully_qualified_name] ||= ::Protobuf::Field::FieldHash.new(field)
214
+ @values[field.fully_qualified_name].replace(value)
215
+ end
216
+ elsif field.repeated?
190
217
  if value.nil? && ignore_nil_for_repeated
191
218
  ::Protobuf.deprecator.deprecation_warning("#{self.class}#[#{name}]=nil", "use an empty array instead of nil")
192
219
  return
@@ -44,6 +44,19 @@ module Protobuf
44
44
  define_field(:required, type_class, name, tag, options)
45
45
  end
46
46
 
47
+ # Define a map field.
48
+ #
49
+ def map(key_type_class, value_type_class, name, tag, options = {})
50
+ # manufacture a message that represents the map entry, used for
51
+ # serialization and deserialization
52
+ entry_type = Class.new(::Protobuf::Message) do
53
+ set_option :map_entry, true
54
+ optional key_type_class, :key, 1
55
+ optional value_type_class, :value, 2
56
+ end
57
+ define_field(:repeated, entry_type, name, tag, options)
58
+ end
59
+
47
60
  # Define an extension range.
48
61
  #
49
62
  def extensions(range)
@@ -1,3 +1,3 @@
1
1
  module Protobuf
2
- VERSION = '3.7.5' # rubocop:disable Style/MutableConstant
2
+ VERSION = '3.8.0.pre1' # rubocop:disable Style/MutableConstant
3
3
  end
@@ -24,6 +24,26 @@ RSpec.describe 'code generation' do
24
24
  expect(code_generator.response_bytes).to eq(expected_output)
25
25
  end
26
26
 
27
+ it "generates code for map types" do
28
+ input_descriptor = ::Google::Protobuf::FileDescriptorSet.decode(
29
+ IO.read(PROTOS_PATH.join('map-test.bin'), :mode => 'rb'))
30
+ request = ::Google::Protobuf::Compiler::CodeGeneratorRequest.new(:file_to_generate => ['map-test.proto'],
31
+ :proto_file => input_descriptor.file)
32
+
33
+ file_name = "map-test.pb.rb"
34
+ file_content = File.open(PROTOS_PATH.join(file_name), "r:UTF-8", &:read)
35
+ expected_file_output =
36
+ ::Google::Protobuf::Compiler::CodeGeneratorResponse::File.new(
37
+ :name => file_name, :content => file_content)
38
+
39
+ expected_response =
40
+ ::Google::Protobuf::Compiler::CodeGeneratorResponse.encode(:file => [expected_file_output])
41
+
42
+ code_generator = ::Protobuf::CodeGenerator.new(request.encode)
43
+ code_generator.eval_unknown_extensions!
44
+ expect(code_generator.response_bytes).to eq(expected_response)
45
+ end
46
+
27
47
  it "generates code (including service stubs) with custom field and method options" do
28
48
  expected_unittest_custom_options =
29
49
  File.open(PROTOS_PATH.join('google_unittest_custom_options.pb.rb'), "r:UTF-8", &:read)
@@ -2,21 +2,39 @@ require 'spec_helper'
2
2
 
3
3
  RSpec.describe Protobuf::Field::FieldArray do
4
4
 
5
- class SomeBasicMessage < ::Protobuf::Message
6
- optional :string, :field, 1
5
+ let(:basic_message) do
6
+ Class.new(::Protobuf::Message) do
7
+ optional :string, :field, 1
8
+ end
7
9
  end
8
10
 
9
- class MoreComplexMessage < SomeBasicMessage
11
+ let(:more_complex_message) do
12
+ Class.new(BasicMessage) do
13
+ end
10
14
  end
11
15
 
12
- class OtherBasicMessage < ::Protobuf::Message
13
- optional :string, :other_field, 1
16
+ let(:some_enum) do
17
+ Class.new(::Protobuf::Enum) do
18
+ define :FOO, 1
19
+ define :BAR, 2
20
+ define :BAZ, 3
21
+ end
14
22
  end
15
23
 
16
- class SomeRepeatMessage < ::Protobuf::Message
17
- optional :string, :some_string, 1
18
- repeated :string, :multiple_strings, 2
19
- repeated SomeBasicMessage, :multiple_basic_msgs, 3
24
+ let(:repeat_message) do
25
+ Class.new(::Protobuf::Message) do
26
+ optional :string, :some_string, 1
27
+ repeated :string, :multiple_strings, 2
28
+ repeated BasicMessage, :multiple_basic_msgs, 3
29
+ repeated SomeEnum, :multiple_enums, 4
30
+ end
31
+ end
32
+
33
+ before do
34
+ stub_const('BasicMessage', basic_message)
35
+ stub_const('MoreComplexMessage', more_complex_message)
36
+ stub_const('SomeEnum', some_enum)
37
+ stub_const('SomeRepeatMessage', repeat_message)
20
38
  end
21
39
 
22
40
  let(:instance) { SomeRepeatMessage.new }
@@ -40,17 +58,21 @@ RSpec.describe Protobuf::Field::FieldArray do
40
58
  context 'when applied to a MessageField field array' do
41
59
  it 'adds a MessageField object' do
42
60
  expect(instance.multiple_basic_msgs).to be_empty
43
- basic_msg1 = SomeBasicMessage.new
61
+ basic_msg1 = BasicMessage.new
44
62
  instance.multiple_basic_msgs.send(method, basic_msg1)
45
63
  expect(instance.multiple_basic_msgs).to eq([basic_msg1])
46
- basic_msg2 = SomeBasicMessage.new
64
+ basic_msg2 = BasicMessage.new
47
65
  instance.multiple_basic_msgs.send(method, basic_msg2)
48
66
  expect(instance.multiple_basic_msgs).to eq([basic_msg1, basic_msg2])
49
67
  end
50
68
 
69
+ it 'fails if not adding a MessageField' do
70
+ expect { instance.multiple_basic_msgs.send(method, 100.0) }.to raise_error(TypeError)
71
+ end
72
+
51
73
  it 'adds a Hash from a MessageField object' do
52
74
  expect(instance.multiple_basic_msgs).to be_empty
53
- basic_msg1 = SomeBasicMessage.new
75
+ basic_msg1 = BasicMessage.new
54
76
  basic_msg1.field = 'my value'
55
77
  instance.multiple_basic_msgs.send(method, basic_msg1.to_hash)
56
78
  expect(instance.multiple_basic_msgs).to eq([basic_msg1])
@@ -64,6 +86,20 @@ RSpec.describe Protobuf::Field::FieldArray do
64
86
  expect(instance.multiple_basic_msgs.first).to be_a(MoreComplexMessage)
65
87
  end
66
88
  end
89
+
90
+ context 'when applied to an EnumField field array' do
91
+ it 'adds an EnumField object' do
92
+ expect(instance.multiple_enums).to be_empty
93
+ instance.multiple_enums.send(method, SomeEnum::FOO)
94
+ expect(instance.multiple_enums).to eq([SomeEnum::FOO])
95
+ instance.multiple_enums.send(method, SomeEnum::BAR)
96
+ expect(instance.multiple_enums).to eq([SomeEnum::FOO, SomeEnum::BAR])
97
+ end
98
+
99
+ it 'fails if not adding an EnumField' do
100
+ expect { instance.multiple_basic_msgs.send(method, 100.0) }.to raise_error(TypeError)
101
+ end
102
+ end
67
103
  end
68
104
  end
69
105
  end