protobuf 3.10.9 → 3.10.10

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.
@@ -1,3 +1,4 @@
1
+ require 'active_support/core_ext/class/subclasses'
1
2
  require 'protobuf/logging'
2
3
  require 'protobuf/message'
3
4
  require 'protobuf/rpc/client'
@@ -65,15 +66,18 @@ module Protobuf
65
66
  end
66
67
 
67
68
  # An array of defined service classes that contain implementation
68
- # code
69
+ # code. Walks the full descendant tree so intermediate parents
70
+ # (e.g. ApplicationService) don't hide the real services from
71
+ # discovery.
69
72
  def self.implemented_services
70
- classes = (subclasses || []).select do |subclass|
73
+ classes = descendants.select do |subclass|
71
74
  subclass.rpcs.any? do |(name, _)|
72
75
  subclass.method_defined? name
73
76
  end
74
77
  end
75
78
 
76
- classes.map(&:name)
79
+ # Dev reloads can leave nameless/duplicate classes in `descendants`.
80
+ classes.map(&:name).compact.uniq
77
81
  end
78
82
 
79
83
  # Shorthand call to configure, passing a string formatted as hostname:port
@@ -1,3 +1,5 @@
1
+ require 'active_support/core_ext/class/subclasses'
2
+
1
3
  module Protobuf
2
4
  module Rpc
3
5
  module ServiceFilters
@@ -34,23 +36,51 @@ module Protobuf
34
36
  @rescue_filters ||= {}
35
37
  end
36
38
 
39
+ # A subclass gets the filters of its parent, so a base class such as
40
+ # ApplicationService can hold shared filters. A filter that the parent
41
+ # adds later also goes to existing subclasses (after their own filters).
42
+ def inherited(subclass)
43
+ super
44
+ subclass.inherit_filters_from(self)
45
+ end
46
+
37
47
  def rescue_from(*ex_klasses, &block)
38
48
  options = ex_klasses.last.is_a?(Hash) ? ex_klasses.pop : {}
39
49
  callable = options.delete(:with) { block }
40
50
  fail ArgumentError, 'Option :with missing from rescue_from options' if callable.nil?
41
- ex_klasses.each { |ex_klass| rescue_filters[ex_klass] = callable }
51
+ ex_klasses.each do |ex_klass|
52
+ inherited_callable = rescue_filters[ex_klass]
53
+ rescue_filters[ex_klass] = callable
54
+ descendants.each do |klass|
55
+ # A subclass keeps a handler that it defined itself.
56
+ current = klass.rescue_filters[ex_klass]
57
+ klass.rescue_filters[ex_klass] = callable if current.nil? || current.equal?(inherited_callable)
58
+ end
59
+ end
42
60
  end
43
61
 
44
- private
62
+ protected
45
63
 
46
- def define_filter(type, filter, options = {})
64
+ def inherit_filters_from(parent)
65
+ parent.filters.each { |type, list| filters[type] = list.dup }
66
+ parent.defined_filters.each { |type, set| defined_filters[type] = set.dup }
67
+ rescue_filters.merge!(parent.rescue_filters)
68
+ end
69
+
70
+ def defined_filters
71
+ @defined_filters ||= Hash.new { |h, k| h[k] = Set.new }
72
+ end
73
+
74
+ def add_filter(type, filter, options)
47
75
  return if filter_defined?(type, filter)
48
76
  filters[type] << options.merge(:callable => filter)
49
77
  remember_filter(type, filter)
50
78
  end
51
79
 
52
- def defined_filters
53
- @defined_filters ||= Hash.new { |h, k| h[k] = Set.new }
80
+ private
81
+
82
+ def define_filter(type, filter, options = {})
83
+ ([self] + descendants).each { |klass| klass.add_filter(type, filter, options) }
54
84
  end
55
85
 
56
86
  # Check to see if the filter has been defined.
@@ -0,0 +1,21 @@
1
+ module Protobuf
2
+ # Requires every Ruby file below a directory, in sorted order. Protobuf::Railtie
3
+ # uses it when the main autoloader cannot eager-load the services directory
4
+ # (classic autoloader, Zeitwerk < 2.6.2, or a path outside the main loader).
5
+ #
6
+ # It uses `require`, not `load`. Zeitwerk tracks `require` of the files it
7
+ # manages, and a file never executes two times.
8
+ module ServiceLoader
9
+ DEFAULT_GLOB = '**/*.rb'.freeze
10
+
11
+ def self.load_services(path, glob = DEFAULT_GLOB)
12
+ return [] if path.nil?
13
+ dir = ::File.expand_path(path.to_s)
14
+ return [] unless ::File.directory?(dir)
15
+
16
+ ::Dir.glob(::File.join(dir, glob)).sort.each do |file|
17
+ require file
18
+ end
19
+ end
20
+ end
21
+ end
@@ -1,3 +1,3 @@
1
1
  module Protobuf
2
- VERSION = '3.10.9' # rubocop:disable Style/MutableConstant
2
+ VERSION = '3.10.10' # rubocop:disable Style/MutableConstant
3
3
  end
data/lib/protobuf.rb CHANGED
@@ -44,6 +44,10 @@ require 'protobuf/enum'
44
44
  require 'protobuf/message'
45
45
  require 'protobuf/descriptors'
46
46
 
47
+ # Rails runs this hook when the application class is defined. That works even
48
+ # when protobuf loads before Rails, as `rpc_server` does.
49
+ ::ActiveSupport.on_load(:before_configuration) { require 'protobuf/railtie' }
50
+
47
51
  module Protobuf
48
52
 
49
53
  class << self
@@ -114,6 +118,7 @@ end
114
118
  unless ENV.key?('PB_NO_NETWORKING')
115
119
  require 'protobuf/rpc/client'
116
120
  require 'protobuf/rpc/service'
121
+ require 'protobuf/rpc/application_service'
117
122
 
118
123
  env_connector_type = ENV.fetch('PB_CLIENT_TYPE') do
119
124
  :socket
data/protobuf.gemspec CHANGED
@@ -18,15 +18,18 @@ require "protobuf/version"
18
18
  s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
19
19
  s.require_paths = ["lib"]
20
20
 
21
- # Hack, as Rails 5 requires Ruby version >= 2.2.2.
22
- active_support_max_version = "< 5" if Gem::Version.new(RUBY_VERSION) < Gem::Version.new("2.2.2")
23
- s.add_dependency "activesupport", '>= 3.2', active_support_max_version
21
+ s.required_ruby_version = '>= 3.1'
22
+
23
+ s.add_dependency "activesupport", '>= 6.0'
24
24
  s.add_dependency 'middleware'
25
25
  s.add_dependency 'thor'
26
26
  s.add_dependency 'thread_safe'
27
27
 
28
+ # Ruby 4.0 no longer loads benchmark from the standard library.
29
+ s.add_development_dependency 'benchmark'
28
30
  s.add_development_dependency 'benchmark-ips'
29
31
  s.add_development_dependency 'ffi-rzmq'
32
+ s.add_development_dependency 'railties', '>= 6.0'
30
33
  s.add_development_dependency 'rake', '~> 13.0'
31
34
  s.add_development_dependency 'rspec', '~> 3.5'
32
35
  s.add_development_dependency "rubocop", "~> 0.81.0"
@@ -0,0 +1,233 @@
1
+ require 'spec_helper'
2
+
3
+ # The protobuf spec: "Protobuf parsers must be able to parse repeated fields
4
+ # that were compiled as packed as if they were not packed, and vice versa."
5
+ # https://protobuf.dev/programming-guides/encoding/#packed
6
+ #
7
+ # proto3 encoders (Go, Java, C++, google-protobuf) pack repeated scalars by
8
+ # default, so these fields must decode packed input although they do not
9
+ # declare `packed`.
10
+ RSpec.describe 'Decoding packed and unpacked repeated scalar fields' do
11
+ module PackedDecodingSpec
12
+ class Color < ::Protobuf::Enum
13
+ define :RED, 0
14
+ define :GREEN, 1
15
+ define :BLUE, 2
16
+ end
17
+
18
+ class Unpacked < ::Protobuf::Message
19
+ optional :string, :name, 15
20
+ repeated :int32, :r_i32, 21
21
+ repeated :int64, :r_i64, 22
22
+ repeated :uint32, :r_u32, 23
23
+ repeated :uint64, :r_u64, 24
24
+ repeated :sint32, :r_s32, 25
25
+ repeated :sint64, :r_s64, 26
26
+ repeated :bool, :r_b, 27
27
+ repeated ::PackedDecodingSpec::Color, :r_color, 28
28
+ repeated :fixed32, :r_f32, 29
29
+ repeated :fixed64, :r_f64, 30
30
+ repeated :sfixed32, :r_sf32, 31
31
+ repeated :sfixed64, :r_sf64, 32
32
+ repeated :float, :r_flt, 33
33
+ repeated :double, :r_dbl, 34
34
+ repeated :string, :r_str, 35
35
+ end
36
+
37
+ class Packed < ::Protobuf::Message
38
+ repeated :int32, :r_i32, 21, :packed => true
39
+ repeated :bool, :r_b, 27, :packed => true
40
+ repeated :double, :r_dbl, 34, :packed => true
41
+ end
42
+
43
+ class Singular < ::Protobuf::Message
44
+ optional :int32, :i32, 1
45
+ optional :bool, :b, 7
46
+ optional :string, :str, 15
47
+ map :string, :int32, :counts, 16
48
+ end
49
+ end
50
+
51
+ def tag(field, wire_type)
52
+ ::Protobuf::Varint.encode((field << 3) | wire_type)
53
+ end
54
+
55
+ def varint(value)
56
+ ::Protobuf::Varint.encode(value)
57
+ end
58
+
59
+ def len(field, bytes)
60
+ tag(field, ::Protobuf::WireType::LENGTH_DELIMITED) + varint(bytes.bytesize) + bytes
61
+ end
62
+
63
+ def unpacked_varints(field, values)
64
+ values.map { |v| tag(field, ::Protobuf::WireType::VARINT) + varint(v) }.join.b
65
+ end
66
+
67
+ # Bytes made by google-protobuf 4.36.2 (proto3, so every repeated scalar is
68
+ # packed) for:
69
+ # r_i32: [1, -1, 300], r_i64: [5, -9_000_000_000],
70
+ # r_u32: [0, 4_294_967_295], r_u64: [18_446_744_073_709_551_615],
71
+ # r_s32: [-1, 2, -300], r_s64: [-5_000_000_000], r_b: [true, false, true],
72
+ # r_color: [GREEN, BLUE], r_f32: [7, 4_294_967_295], r_f64: [8],
73
+ # r_sf32: [-7], r_sf64: [-8], r_flt: [1.5, -2.25], r_dbl: [3.5, -0.125]
74
+ let(:google_protobuf_packed) do
75
+ hex = 'aa010d01ffffffffffffffffff01ac02b2010b0580ccbbbcdeffffffff01ba010600ffffffff0f' \
76
+ 'c2010affffffffffffffffff01ca01040104d704d20105ffc7afa025da0103010001e201020102' \
77
+ 'ea010807000000fffffffff201080800000000000000fa0104f9ffffff820208f8ffffffffffffff' \
78
+ '8a02080000c03f000010c09202100000000000000c40000000000000c0bf'
79
+ [hex].pack('H*')
80
+ end
81
+
82
+ describe 'packed input for fields that do not declare packed' do
83
+ subject(:message) { PackedDecodingSpec::Unpacked.decode(google_protobuf_packed) }
84
+
85
+ {
86
+ :r_i32 => [1, -1, 300],
87
+ :r_i64 => [5, -9_000_000_000],
88
+ :r_u32 => [0, 4_294_967_295],
89
+ :r_u64 => [18_446_744_073_709_551_615],
90
+ :r_s32 => [-1, 2, -300],
91
+ :r_s64 => [-5_000_000_000],
92
+ :r_b => [true, false, true],
93
+ :r_f32 => [7, 4_294_967_295],
94
+ :r_f64 => [8],
95
+ :r_sf32 => [-7],
96
+ :r_sf64 => [-8],
97
+ :r_flt => [1.5, -2.25],
98
+ :r_dbl => [3.5, -0.125],
99
+ }.each do |field, expected|
100
+ it "decodes #{field}" do
101
+ expect(message.public_send(field).to_a).to eq(expected)
102
+ end
103
+ end
104
+
105
+ it 'decodes enums' do
106
+ expect(message.r_color.to_a).to eq([PackedDecodingSpec::Color::GREEN, PackedDecodingSpec::Color::BLUE])
107
+ end
108
+
109
+ it 'decodes the report case: packed bools are not all false' do
110
+ expect(PackedDecodingSpec::Unpacked.decode(len(27, "\x01".b)).r_b.to_a).to eq([true])
111
+ expect(PackedDecodingSpec::Unpacked.decode(len(27, "\x01\x01".b)).r_b.to_a).to eq([true, true])
112
+ end
113
+
114
+ it 'decodes the report case: packed int64 does not raise' do
115
+ expect(PackedDecodingSpec::Unpacked.decode(len(22, "\x05\x09".b)).r_i64.to_a).to eq([5, 9])
116
+ end
117
+ end
118
+
119
+ describe 'packed records' do
120
+ it 'concatenates a packed run that is split over two records' do
121
+ bytes = len(21, "\x01\x02".b) + len(15, 'x') + len(21, "\x03".b)
122
+ expect(PackedDecodingSpec::Unpacked.decode(bytes).r_i32.to_a).to eq([1, 2, 3])
123
+ end
124
+
125
+ it 'accepts packed and unpacked records for the same field' do
126
+ bytes = len(21, "\x01\x02".b) + unpacked_varints(21, [3])
127
+ expect(PackedDecodingSpec::Unpacked.decode(bytes).r_i32.to_a).to eq([1, 2, 3])
128
+ end
129
+
130
+ it 'adds no values for an empty packed record' do
131
+ expect(PackedDecodingSpec::Unpacked.decode(len(21, ''.b)).r_i32.to_a).to eq([])
132
+ end
133
+
134
+ it 'does not change other fields' do
135
+ bytes = len(15, 'name') + len(21, "\x01".b) + len(35, 'a') + len(35, 'b')
136
+ message = PackedDecodingSpec::Unpacked.decode(bytes)
137
+ expect([message.name, message.r_i32.to_a, message.r_str.to_a]).to eq(['name', [1], %w[a b]])
138
+ end
139
+ end
140
+
141
+ describe 'unpacked input for fields that declare packed' do
142
+ it 'decodes varints' do
143
+ bytes = unpacked_varints(21, [1, 2])
144
+ expect(PackedDecodingSpec::Packed.decode(bytes).r_i32.to_a).to eq([1, 2])
145
+ end
146
+
147
+ it 'decodes bools' do
148
+ bytes = unpacked_varints(27, [1, 0])
149
+ expect(PackedDecodingSpec::Packed.decode(bytes).r_b.to_a).to eq([true, false])
150
+ end
151
+
152
+ it 'decodes fixed64 values' do
153
+ bytes = [3.5, -0.125].map { |v| tag(34, ::Protobuf::WireType::FIXED64) + [v].pack('E') }.join.b
154
+ expect(PackedDecodingSpec::Packed.decode(bytes).r_dbl.to_a).to eq([3.5, -0.125])
155
+ end
156
+ end
157
+
158
+ describe 'unchanged behavior' do
159
+ it 'decodes unpacked input for fields that do not declare packed' do
160
+ bytes = unpacked_varints(21, [1, 2]) + unpacked_varints(27, [1])
161
+ message = PackedDecodingSpec::Unpacked.decode(bytes)
162
+ expect([message.r_i32.to_a, message.r_b.to_a]).to eq([[1, 2], [true]])
163
+ end
164
+
165
+ it 'decodes packed input for fields that declare packed' do
166
+ expect(PackedDecodingSpec::Packed.decode(len(21, "\x01\x02".b)).r_i32.to_a).to eq([1, 2])
167
+ end
168
+
169
+ it 'round-trips both declarations' do
170
+ unpacked = PackedDecodingSpec::Unpacked.new(:r_i32 => [1, -1], :r_b => [true, false], :r_dbl => [1.5])
171
+ packed = PackedDecodingSpec::Packed.new(:r_i32 => [1, -1], :r_b => [true, false], :r_dbl => [1.5])
172
+ expect(PackedDecodingSpec::Unpacked.decode(unpacked.encode)).to eq(unpacked)
173
+ expect(PackedDecodingSpec::Packed.decode(packed.encode)).to eq(packed)
174
+ end
175
+
176
+ it 'decodes across declarations: packed writer, unpacked reader and back' do
177
+ packed = PackedDecodingSpec::Packed.new(:r_i32 => [1, 300], :r_b => [true, false], :r_dbl => [1.5, 2.5])
178
+ unpacked = PackedDecodingSpec::Unpacked.decode(packed.encode)
179
+ expect([unpacked.r_i32.to_a, unpacked.r_b.to_a, unpacked.r_dbl.to_a]).to eq([[1, 300], [true, false], [1.5, 2.5]])
180
+
181
+ back = PackedDecodingSpec::Packed.decode(unpacked.encode)
182
+ expect(back).to eq(packed)
183
+ end
184
+
185
+ it 'does not change encoding' do
186
+ message = PackedDecodingSpec::Unpacked.new(:r_i32 => [1, 2])
187
+ expect(message.encode).to eq(unpacked_varints(21, [1, 2]))
188
+ end
189
+ end
190
+
191
+ describe 'a record with the wrong wire type for a known field' do
192
+ it 'is skipped for a singular scalar field' do
193
+ bytes = len(1, 'xx') + len(7, "\x01".b) + len(15, 'ok')
194
+ message = PackedDecodingSpec::Singular.decode(bytes)
195
+ expect([message.i32, message.field?(:i32), message.b, message.field?(:b), message.str]).to eq([0, false, false, false, 'ok'])
196
+ end
197
+
198
+ it 'is skipped for a repeated scalar field' do
199
+ bytes = tag(29, ::Protobuf::WireType::VARINT) + varint(5) + len(21, "\x01".b)
200
+ message = PackedDecodingSpec::Unpacked.decode(bytes)
201
+ expect([message.r_f32.to_a, message.r_i32.to_a]).to eq([[], [1]])
202
+ end
203
+
204
+ it 'is skipped for a repeated string field' do
205
+ bytes = tag(35, ::Protobuf::WireType::VARINT) + varint(5) + len(35, 'ok')
206
+ expect(PackedDecodingSpec::Unpacked.decode(bytes).r_str.to_a).to eq(['ok'])
207
+ end
208
+
209
+ it 'is skipped for a map field' do
210
+ entry = len(1, 'a') + tag(2, ::Protobuf::WireType::VARINT) + varint(3)
211
+ bytes = tag(16, ::Protobuf::WireType::VARINT) + varint(5) + len(16, entry)
212
+ expect(PackedDecodingSpec::Singular.decode(bytes).counts.to_h).to eq('a' => 3)
213
+ end
214
+ end
215
+
216
+ describe 'callers that do not know the wire type' do
217
+ it 'Decoder.decode_each_field still works with a two-argument block' do
218
+ seen = []
219
+ ::Protobuf::Decoder.decode_each_field(StringIO.new(unpacked_varints(21, [7]))) { |tag, bytes| seen << [tag, bytes] }
220
+ expect(seen).to eq([[21, 7]])
221
+ end
222
+
223
+ it 'BaseField#set without a wire type uses the field declaration' do
224
+ message = PackedDecodingSpec::Packed.new
225
+ PackedDecodingSpec::Packed.get_field(:r_i32).set(message, "\x01\x02".b)
226
+ expect(message.r_i32.to_a).to eq([1, 2])
227
+
228
+ unpacked = PackedDecodingSpec::Unpacked.new
229
+ PackedDecodingSpec::Unpacked.get_field(:r_i32).set(unpacked, 3)
230
+ expect(unpacked.r_i32.to_a).to eq([3])
231
+ end
232
+ end
233
+ end
@@ -0,0 +1,113 @@
1
+ require 'spec_helper'
2
+
3
+ # Input that other protobuf implementations decode, but that made this gem
4
+ # raise for the whole message.
5
+ RSpec.describe 'Decoding input from other protobuf implementations' do
6
+ module WireCompatDecodingSpec
7
+ class Message < ::Protobuf::Message
8
+ optional :int32, :i32, 1
9
+ optional :uint32, :u32, 2
10
+ optional :sint32, :s32, 3
11
+ repeated :int32, :r_i32, 4
12
+ optional :int64, :i64, 5
13
+ optional :string, :str, 15
14
+ end
15
+ end
16
+
17
+ def tag(field, wire_type)
18
+ ::Protobuf::Varint.encode((field << 3) | wire_type)
19
+ end
20
+
21
+ def varint(field, value)
22
+ tag(field, ::Protobuf::WireType::VARINT) + ::Protobuf::Varint.encode(value)
23
+ end
24
+
25
+ def len(field, bytes)
26
+ tag(field, ::Protobuf::WireType::LENGTH_DELIMITED) + ::Protobuf::Varint.encode(bytes.bytesize) + bytes
27
+ end
28
+
29
+ def decode(bytes)
30
+ WireCompatDecodingSpec::Message.decode(bytes.b)
31
+ end
32
+
33
+ # The language guide calls int32, uint32, int64, and uint64 compatible: a
34
+ # parser keeps the low 32 bits, as in a C++ cast.
35
+ describe 'a 32-bit field that gets a wider varint' do
36
+ it 'keeps the low 32 bits of an int32' do
37
+ expect(decode(varint(1, 0xffff_ffff)).i32).to eq(-1)
38
+ expect(decode(varint(1, 0x1_0000_0005)).i32).to eq(5)
39
+ expect(decode(varint(1, 0xffff_ffff_8000_0000)).i32).to eq(-2_147_483_648)
40
+ end
41
+
42
+ it 'keeps the low 32 bits of a uint32' do
43
+ expect(decode(varint(2, 0x1_0000_0001)).u32).to eq(1)
44
+ end
45
+
46
+ it 'keeps the low 32 bits of a sint32 before the zigzag decode' do
47
+ # zigzag(-2) is 3; the bits above 32 are dropped.
48
+ expect(decode(varint(3, 0x4_0000_0003)).s32).to eq(-2)
49
+ end
50
+
51
+ it 'keeps the low 32 bits in a packed run' do
52
+ run = ::Protobuf::Varint.encode(0x1_0000_0007) + ::Protobuf::Varint.encode(1)
53
+ expect(decode(len(4, run)).r_i32.to_a).to eq([7, 1])
54
+ end
55
+
56
+ it 'decodes the other fields of the message' do
57
+ message = decode(varint(1, 0x1_0000_0005) + len(15, 'ok'))
58
+ expect([message.i32, message.str]).to eq([5, 'ok'])
59
+ end
60
+
61
+ it 'does not change values in range' do
62
+ message = WireCompatDecodingSpec::Message.new(
63
+ :i32 => -2_147_483_648, :u32 => 4_294_967_295, :s32 => -7, :r_i32 => [-1, 2_147_483_647], :i64 => -2**40,
64
+ )
65
+ expect(WireCompatDecodingSpec::Message.decode(message.encode)).to eq(message)
66
+ end
67
+ end
68
+
69
+ # Groups (wire types 3 and 4) come from proto2 group fields and from
70
+ # editions DELIMITED encoding. This gem has no group fields, so it skips a
71
+ # group as an unknown field.
72
+ describe 'a group' do
73
+ let(:start_group) { tag(95, ::Protobuf::WireType::START_GROUP) }
74
+ let(:end_group) { tag(95, ::Protobuf::WireType::END_GROUP) }
75
+
76
+ it 'is skipped, with the fields inside it' do
77
+ bytes = start_group + varint(1, 9) + len(15, 'in') + end_group + varint(1, 5)
78
+ message = decode(bytes)
79
+ expect([message.i32, message.str]).to eq([5, ''])
80
+ end
81
+
82
+ it 'is skipped with every wire type inside it' do
83
+ inner = varint(1, 1) + tag(2, ::Protobuf::WireType::FIXED64) + ("\x00" * 8) +
84
+ tag(3, ::Protobuf::WireType::FIXED32) + ("\x00" * 4) + len(4, 'abc')
85
+ expect(decode(start_group + inner + end_group + len(15, 'ok')).str).to eq('ok')
86
+ end
87
+
88
+ it 'is skipped with groups inside it' do
89
+ inner_start = tag(7, ::Protobuf::WireType::START_GROUP)
90
+ inner_end = tag(7, ::Protobuf::WireType::END_GROUP)
91
+ bytes = start_group + inner_start + varint(1, 9) + inner_end + end_group + varint(1, 5)
92
+ expect(decode(bytes).i32).to eq(5)
93
+ end
94
+
95
+ it 'is skipped when its field number is a known field' do
96
+ bytes = tag(1, ::Protobuf::WireType::START_GROUP) + varint(1, 9) + tag(1, ::Protobuf::WireType::END_GROUP)
97
+ expect(decode(bytes).field?(:i32)).to be(false)
98
+ end
99
+
100
+ it 'raises when the group has no end' do
101
+ expect { decode(start_group + varint(1, 9)) }.to raise_error(::Protobuf::InvalidWireType)
102
+ end
103
+
104
+ it 'raises when the end does not match the start' do
105
+ bytes = start_group + tag(96, ::Protobuf::WireType::END_GROUP)
106
+ expect { decode(bytes) }.to raise_error(::Protobuf::InvalidWireType)
107
+ end
108
+
109
+ it 'raises for an end group without a start' do
110
+ expect { decode(end_group) }.to raise_error(::Protobuf::InvalidWireType)
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,84 @@
1
+ require 'spec_helper'
2
+ require 'open3'
3
+ require 'rbconfig'
4
+ require 'tmpdir'
5
+ require 'fileutils'
6
+
7
+ # Boots a real Rails app in a subprocess, with the load order that the
8
+ # `rpc_server` CLI uses: protobuf loads first, then the app file loads Rails.
9
+ RSpec.describe 'Protobuf::Railtie in a booted Rails app' do
10
+ let(:lib_dir) { ::File.expand_path('../../../lib', __FILE__) }
11
+
12
+ def write(root, relative_path, contents)
13
+ full = ::File.join(root, relative_path)
14
+ ::FileUtils.mkdir_p(::File.dirname(full))
15
+ ::File.write(full, contents)
16
+ end
17
+
18
+ def build_app(root, services_path)
19
+ config_line = services_path ? "config.protobuf.services_path = #{services_path.inspect}" : ''
20
+
21
+ write(root, 'config/environment.rb', <<-RUBY)
22
+ require 'rails'
23
+ class RailtieBootApp < Rails::Application
24
+ config.root = #{root.inspect}
25
+ config.eager_load = false
26
+ config.logger = Logger.new(nil)
27
+ config.secret_key_base = 'x' * 64
28
+ #{config_line}
29
+ end
30
+ Rails.application.initialize!
31
+ RUBY
32
+
33
+ # app/services is a Zeitwerk root, so app/services/rpc/ is the Rpc namespace.
34
+ write(root, 'app/services/rpc/echo_service.rb', <<-RUBY)
35
+ module Rpc
36
+ class EchoService < ::Protobuf::Rpc::ApplicationService
37
+ rpc :echo, ::Protobuf::Socketrpc::Request, ::Protobuf::Socketrpc::Response
38
+ def echo; end
39
+ end
40
+ end
41
+ RUBY
42
+
43
+ write(root, 'app/services/plain_object.rb', <<-RUBY)
44
+ class PlainObject
45
+ $stdout.puts 'PLAIN_OBJECT_LOADED'
46
+ end
47
+ RUBY
48
+ end
49
+
50
+ def boot(root)
51
+ script = <<-RUBY
52
+ require 'protobuf/socket'
53
+ require #{::File.join(root, 'config/environment').inspect}
54
+ puts "BEFORE_BIND=\#{::Protobuf::Rpc::Service.implemented_services.inspect}"
55
+ ::ActiveSupport::Notifications.instrument('before_server_bind')
56
+ puts "AFTER_BIND=\#{::Protobuf::Rpc::Service.implemented_services.inspect}"
57
+ RUBY
58
+
59
+ Open3.capture2e(::RbConfig.ruby, '-I', lib_dir, '-e', script, :chdir => root)
60
+ end
61
+
62
+ it 'loads the configured services directory when the server binds' do
63
+ ::Dir.mktmpdir do |root|
64
+ build_app(root, 'app/services/rpc')
65
+ output, status = boot(root)
66
+
67
+ expect(status).to be_success, output
68
+ expect(output).to include('BEFORE_BIND=[]')
69
+ expect(output).to include('AFTER_BIND=["Rpc::EchoService"]')
70
+ expect(output).not_to include('PLAIN_OBJECT_LOADED')
71
+ end
72
+ end
73
+
74
+ it 'loads nothing when services_path is not configured' do
75
+ ::Dir.mktmpdir do |root|
76
+ build_app(root, nil)
77
+ output, status = boot(root)
78
+
79
+ expect(status).to be_success, output
80
+ expect(output).to include('AFTER_BIND=[]')
81
+ expect(output).not_to include('PLAIN_OBJECT_LOADED')
82
+ end
83
+ end
84
+ end
@@ -53,6 +53,27 @@ RSpec.describe Protobuf::Field::BoolField do
53
53
  end
54
54
  end
55
55
 
56
+ describe '#decode' do
57
+ subject { described_class.new(SomeBoolMessage, :optional, described_class, :b, 1, :b, {}) }
58
+
59
+ it 'decodes 0 as false' do
60
+ expect(subject.decode(0)).to be(false)
61
+ end
62
+
63
+ it 'decodes 1 as true' do
64
+ expect(subject.decode(1)).to be(true)
65
+ end
66
+
67
+ it 'decodes any other non-zero varint as true, like the reference parsers' do
68
+ expect(subject.decode(2)).to be(true)
69
+ expect(subject.decode(0xffff_ffff_ffff_ffff)).to be(true)
70
+ end
71
+ end
72
+
73
+ it 'decodes a non-canonical varint (2) on the wire as true' do
74
+ expect(SomeBoolMessage.decode("\x08\x02".b).some_bool).to be(true)
75
+ end
76
+
56
77
  it 'defines ? method' do
57
78
  instance.required_bool = false
58
79
  expect(instance.required_bool?).to be(false)
@@ -95,5 +95,28 @@ EOF
95
95
  expect(subject.to_s).to eq(compiled)
96
96
  end
97
97
  end
98
+
99
+ context 'with PB_SERVICE_PARENT in the environment' do
100
+ let(:service_parent) { ' ::Protobuf::Rpc::ApplicationService ' }
101
+
102
+ before do
103
+ allow(ENV).to receive(:[]).and_call_original
104
+ allow(ENV).to receive(:[]).with('PB_SERVICE_PARENT').and_return(service_parent)
105
+ end
106
+
107
+ it 'uses the given parent class' do
108
+ subject.compile
109
+ expect(subject.to_s).to start_with("class TestService < ::Protobuf::Rpc::ApplicationService\n")
110
+ end
111
+
112
+ context 'when it is empty' do
113
+ let(:service_parent) { '' }
114
+
115
+ it 'uses ::Protobuf::Rpc::Service' do
116
+ subject.compile
117
+ expect(subject.to_s).to eq(compiled)
118
+ end
119
+ end
120
+ end
98
121
  end
99
122
  end