protobuf 3.10.8 → 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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +106 -22
  3. data/.rubocop.yml +30 -11
  4. data/.rubocop_todo.yml +462 -60
  5. data/CHANGES.md +31 -0
  6. data/Gemfile +22 -0
  7. data/lib/protobuf/code_generator.rb +17 -5
  8. data/lib/protobuf/decoder.rb +33 -2
  9. data/lib/protobuf/field/base_field.rb +13 -5
  10. data/lib/protobuf/field/base_field_object_definitions.rb +40 -6
  11. data/lib/protobuf/field/bool_field.rb +2 -1
  12. data/lib/protobuf/field/int32_field.rb +12 -0
  13. data/lib/protobuf/field/sint32_field.rb +10 -0
  14. data/lib/protobuf/field/uint32_field.rb +9 -0
  15. data/lib/protobuf/generators/printable.rb +10 -1
  16. data/lib/protobuf/message/serialization.rb +4 -4
  17. data/lib/protobuf/railtie.rb +48 -0
  18. data/lib/protobuf/rpc/application_service.rb +12 -0
  19. data/lib/protobuf/rpc/buffer.rb +2 -2
  20. data/lib/protobuf/rpc/service.rb +7 -3
  21. data/lib/protobuf/rpc/service_filters.rb +35 -5
  22. data/lib/protobuf/service_loader.rb +21 -0
  23. data/lib/protobuf/version.rb +1 -1
  24. data/lib/protobuf.rb +5 -0
  25. data/protobuf.gemspec +9 -27
  26. data/spec/encoding/packed_decoding_spec.rb +233 -0
  27. data/spec/encoding/wire_compat_decoding_spec.rb +113 -0
  28. data/spec/functional/railtie_boot_spec.rb +84 -0
  29. data/spec/lib/protobuf/field/bool_field_spec.rb +21 -0
  30. data/spec/lib/protobuf/field/string_field_spec.rb +0 -2
  31. data/spec/lib/protobuf/generators/service_generator_spec.rb +23 -0
  32. data/spec/lib/protobuf/message_spec.rb +0 -2
  33. data/spec/lib/protobuf/railtie_spec.rb +86 -0
  34. data/spec/lib/protobuf/rpc/application_service_spec.rb +57 -0
  35. data/spec/lib/protobuf/rpc/service_filters_inheritance_spec.rb +105 -0
  36. data/spec/lib/protobuf/service_loader_spec.rb +88 -0
  37. metadata +49 -77
@@ -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)
@@ -1,5 +1,3 @@
1
- # encoding: utf-8
2
-
3
1
  require 'spec_helper'
4
2
 
5
3
  RSpec.describe ::Protobuf::Field::StringField do
@@ -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
@@ -1,5 +1,3 @@
1
- # encoding: utf-8
2
-
3
1
  require 'stringio'
4
2
  require 'spec_helper'
5
3
  require PROTOS_PATH.join('resource.pb')
@@ -0,0 +1,86 @@
1
+ require 'spec_helper'
2
+ require 'protobuf/railtie'
3
+
4
+ RSpec.describe Protobuf::Railtie do
5
+ let(:config) do
6
+ double('config', :protobuf => ::ActiveSupport::OrderedOptions.new)
7
+ end
8
+
9
+ let(:app) { double('app', :config => config, :root => Pathname.new('/srv/app')) }
10
+
11
+ describe '.services_path' do
12
+ it 'is nil when not configured (opt-in)' do
13
+ expect(described_class.services_path(app)).to be_nil
14
+ end
15
+
16
+ it 'is nil when configured as an empty string' do
17
+ config.protobuf.services_path = ''
18
+ expect(described_class.services_path(app)).to be_nil
19
+ end
20
+
21
+ it 'honors an absolute path' do
22
+ config.protobuf.services_path = '/custom/services'
23
+ expect(described_class.services_path(app)).to eq('/custom/services')
24
+ end
25
+
26
+ it 'resolves a relative path against the app root' do
27
+ config.protobuf.services_path = 'app/services/rpc'
28
+ expect(described_class.services_path(app)).to eq('/srv/app/app/services/rpc')
29
+ end
30
+
31
+ it 'accepts a Pathname' do
32
+ config.protobuf.services_path = Pathname.new('/custom/services')
33
+ expect(described_class.services_path(app)).to eq('/custom/services')
34
+ end
35
+ end
36
+
37
+ describe '.load_services' do
38
+ let(:loader) { double('autoloader') }
39
+
40
+ before do
41
+ allow(described_class).to receive(:services_path).and_return('/svc')
42
+ allow(::File).to receive(:directory?).and_call_original
43
+ allow(::File).to receive(:directory?).with('/svc').and_return(true)
44
+ stub_const('Rails', double('Rails', :autoloaders => double(:main => loader)))
45
+ end
46
+
47
+ it 'does nothing when no path is configured' do
48
+ allow(described_class).to receive(:services_path).and_return(nil)
49
+ expect(::Protobuf::ServiceLoader).not_to receive(:load_services)
50
+ described_class.load_services(app)
51
+ end
52
+
53
+ it 'does nothing when the directory is missing' do
54
+ allow(::File).to receive(:directory?).with('/svc').and_return(false)
55
+ expect(::Protobuf::ServiceLoader).not_to receive(:load_services)
56
+ described_class.load_services(app)
57
+ end
58
+
59
+ it 'eager-loads through the autoloader when eager_load_dir is available' do
60
+ allow(loader).to receive(:respond_to?).with(:eager_load_dir).and_return(true)
61
+ expect(loader).to receive(:eager_load_dir).with('/svc')
62
+ expect(::Protobuf::ServiceLoader).not_to receive(:load_services)
63
+ described_class.load_services(app)
64
+ end
65
+
66
+ it 'falls back to ServiceLoader when eager_load_dir is unavailable' do
67
+ allow(loader).to receive(:respond_to?).with(:eager_load_dir).and_return(false)
68
+ expect(::Protobuf::ServiceLoader).to receive(:load_services).with('/svc')
69
+ described_class.load_services(app)
70
+ end
71
+
72
+ it 'falls back to ServiceLoader with the classic autoloader (no main loader)' do
73
+ stub_const('Rails', double('Rails', :autoloaders => double(:main => nil)))
74
+ expect(::Protobuf::ServiceLoader).to receive(:load_services).with('/svc')
75
+ described_class.load_services(app)
76
+ end
77
+
78
+ it 'falls back to ServiceLoader when the path is not autoloader-managed' do
79
+ stub_const('Zeitwerk::Error', Class.new(StandardError))
80
+ allow(loader).to receive(:respond_to?).with(:eager_load_dir).and_return(true)
81
+ allow(loader).to receive(:eager_load_dir).and_raise(::Zeitwerk::Error)
82
+ expect(::Protobuf::ServiceLoader).to receive(:load_services).with('/svc')
83
+ described_class.load_services(app)
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,57 @@
1
+ require 'spec_helper'
2
+ require SUPPORT_PATH.join('resource_service')
3
+
4
+ RSpec.describe Protobuf::Rpc::ApplicationService do
5
+ it 'is a subclass of Protobuf::Rpc::Service' do
6
+ expect(described_class.ancestors).to include(Protobuf::Rpc::Service)
7
+ end
8
+
9
+ it 'inherits Service class methods (host, port, configure)' do
10
+ expect(described_class).to respond_to(:host, :port, :configure, :rpc, :rpcs)
11
+ end
12
+
13
+ describe 'integration with implemented_services' do
14
+ let!(:app_subclass) do
15
+ Class.new(described_class) do
16
+ def self.name
17
+ 'ImplementedAppSubclassService'
18
+ end
19
+
20
+ rpc :find, Test::ResourceFindRequest, Test::Resource
21
+ def find; end
22
+ end
23
+ end
24
+
25
+ let!(:abstract_subclass) do
26
+ Class.new(described_class) do
27
+ def self.name
28
+ 'AbstractAppSubclassService'
29
+ end
30
+
31
+ rpc :find, Test::ResourceFindRequest, Test::Resource
32
+ # no #find method defined -> not "implemented"
33
+ end
34
+ end
35
+
36
+ after do
37
+ # `descendants` holds these classes until GC, which can run late. Remove
38
+ # the rpc method so other specs do not see this class as implemented.
39
+ app_subclass.send(:remove_method, :find)
40
+ end
41
+
42
+ it 'is discovered by Service.implemented_services (descendants traversal)' do
43
+ names = Protobuf::Rpc::Service.implemented_services
44
+ expect(names).to include('ImplementedAppSubclassService')
45
+ end
46
+
47
+ it 'omits subclasses with no implemented rpc methods' do
48
+ names = Protobuf::Rpc::Service.implemented_services
49
+ expect(names).not_to include('AbstractAppSubclassService')
50
+ end
51
+
52
+ it 'does not list the ApplicationService base itself' do
53
+ names = Protobuf::Rpc::Service.implemented_services
54
+ expect(names).not_to include(described_class.name)
55
+ end
56
+ end
57
+ end