protobuf 3.7.5 → 3.8.0.pre1

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.
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