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.
- checksums.yaml +4 -4
- data/.circleci/config.yml +106 -22
- data/.rubocop.yml +8 -0
- data/.rubocop_todo.yml +6 -0
- data/CHANGES.md +31 -0
- data/Gemfile +9 -10
- data/lib/protobuf/code_generator.rb +17 -5
- data/lib/protobuf/decoder.rb +33 -2
- data/lib/protobuf/field/base_field.rb +13 -5
- data/lib/protobuf/field/base_field_object_definitions.rb +40 -6
- data/lib/protobuf/field/bool_field.rb +2 -1
- data/lib/protobuf/field/int32_field.rb +12 -0
- data/lib/protobuf/field/sint32_field.rb +10 -0
- data/lib/protobuf/field/uint32_field.rb +9 -0
- data/lib/protobuf/generators/printable.rb +10 -1
- data/lib/protobuf/message/serialization.rb +4 -4
- data/lib/protobuf/railtie.rb +48 -0
- data/lib/protobuf/rpc/application_service.rb +12 -0
- data/lib/protobuf/rpc/service.rb +7 -3
- data/lib/protobuf/rpc/service_filters.rb +35 -5
- data/lib/protobuf/service_loader.rb +21 -0
- data/lib/protobuf/version.rb +1 -1
- data/lib/protobuf.rb +5 -0
- data/protobuf.gemspec +6 -3
- data/spec/encoding/packed_decoding_spec.rb +233 -0
- data/spec/encoding/wire_compat_decoding_spec.rb +113 -0
- data/spec/functional/railtie_boot_spec.rb +84 -0
- data/spec/lib/protobuf/field/bool_field_spec.rb +21 -0
- data/spec/lib/protobuf/generators/service_generator_spec.rb +23 -0
- data/spec/lib/protobuf/railtie_spec.rb +86 -0
- data/spec/lib/protobuf/rpc/application_service_spec.rb +57 -0
- data/spec/lib/protobuf/rpc/service_filters_inheritance_spec.rb +105 -0
- data/spec/lib/protobuf/service_loader_spec.rb +88 -0
- metadata +51 -9
data/lib/protobuf/rpc/service.rb
CHANGED
|
@@ -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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
62
|
+
protected
|
|
45
63
|
|
|
46
|
-
def
|
|
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
|
-
|
|
53
|
-
|
|
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
|
data/lib/protobuf/version.rb
CHANGED
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
|
-
|
|
22
|
-
|
|
23
|
-
s.add_dependency "activesupport", '>=
|
|
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
|