fast-protowire 0.1.0
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 +7 -0
- data/AGENTS.md +35 -0
- data/CHANGELOG.md +9 -0
- data/README.md +82 -0
- data/lib/fast/protowire/enum.rb +42 -0
- data/lib/fast/protowire/errors.rb +10 -0
- data/lib/fast/protowire/field.rb +501 -0
- data/lib/fast/protowire/message.rb +263 -0
- data/lib/fast/protowire/reader.rb +103 -0
- data/lib/fast/protowire/version.rb +7 -0
- data/lib/fast/protowire/wire.rb +72 -0
- data/lib/fast/protowire.rb +16 -0
- metadata +53 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: a0c2962e443067325c5f4a089b72e499b9e273f8f563464e9830ed9cdf6eccfa
|
|
4
|
+
data.tar.gz: cdc454d06627bf2d1f28ddc35127024e119e06e748120cfceac91fa7e4e40860
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 5b6b5ea5c39ebc3fb9b25490fbea312dccdaaf450ab08b7a9be794056dbc3565e814f98890629664e405333e340e61d493760a57adc99b072b2cf59ff8399bc6
|
|
7
|
+
data.tar.gz: b788d96fe73458a33391b5339d35f7be57a37ec3bb203a2d645c590a5cb98d6ef4f9a5641788d8a12f20c7a51911c27287cba01cbac49e0d4b3c9e22f944be1d
|
data/AGENTS.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
Standing context for agents working in this repository.
|
|
4
|
+
|
|
5
|
+
## Commands
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bundle install
|
|
9
|
+
bundle exec sus
|
|
10
|
+
bundle exec rubocop
|
|
11
|
+
protoc --proto_path=fixtures/proto --ruby_out=fixtures/pb fixtures/proto/*.proto # regenerate the reference schemas
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## What this is
|
|
15
|
+
|
|
16
|
+
The Protocol Buffers wire format, and nothing above it: a `Wire` writer, a
|
|
17
|
+
`Reader`, and a `Message` DSL that declares fields and derives encode/decode
|
|
18
|
+
from the declaration. No descriptors, reflection, JSON mapping or generated
|
|
19
|
+
code. `google-protobuf` is a development dependency only — the parity suite
|
|
20
|
+
(`test/fast/protowire/parity.rb`) encodes the same attributes with both and
|
|
21
|
+
requires identical bytes.
|
|
22
|
+
|
|
23
|
+
## Style Rules
|
|
24
|
+
|
|
25
|
+
- 2-space indentation, `# frozen_string_literal: true` on every source file, double-quoted strings
|
|
26
|
+
- No runtime dependencies. If a feature needs one, it does not belong here.
|
|
27
|
+
- Encoded bytes must match the reference implementation for every case the
|
|
28
|
+
DSL can express; add a parity case before changing encoding behavior.
|
|
29
|
+
|
|
30
|
+
## Test Rules
|
|
31
|
+
|
|
32
|
+
- `sus`; tests are terse and exercise public interfaces only
|
|
33
|
+
- No mock/stub of the class under test
|
|
34
|
+
- Schemas used by tests live in `fixtures/proto` (source of truth), `fixtures/pb`
|
|
35
|
+
(protoc output, checked in) and `fixtures/schema.rb` (the DSL mirror)
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
- Initial release: `Wire`, `Reader`, `Enum` and the `Message` DSL covering
|
|
6
|
+
every scalar type, proto2 and proto3 presence, packed and unpacked
|
|
7
|
+
repeated fields, maps, oneofs, nested and recursive messages, unknown
|
|
8
|
+
field preservation and merge-on-decode, verified byte-for-byte against
|
|
9
|
+
`google-protobuf`. `encode` is compiled per message class.
|
data/README.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# fast-protowire
|
|
2
|
+
|
|
3
|
+
The Protocol Buffers wire format for Ruby, with nothing on top of it: declare
|
|
4
|
+
a message's fields, get `encode` and `decode` for exactly those bytes.
|
|
5
|
+
|
|
6
|
+
It is not a replacement for `google-protobuf`. There are no descriptors,
|
|
7
|
+
no reflection, no JSON mapping and no generated code. It exists for
|
|
8
|
+
libraries that emit or read a fixed, known schema and want the memory cost
|
|
9
|
+
of doing so to be roughly the size of the encoded output, rather than a
|
|
10
|
+
native message object and arena for every field, as `google-protobuf`
|
|
11
|
+
allocates.
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
gem "fast-protowire"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`require "fast/protowire"` has no dependencies.
|
|
20
|
+
|
|
21
|
+
## Quickstart
|
|
22
|
+
|
|
23
|
+
Declarations mirror the `.proto` text. Run this with `bundle exec ruby` from a project
|
|
24
|
+
that has the gem:
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
require "fast/protowire"
|
|
28
|
+
|
|
29
|
+
class LabelPair < Fast::Protowire::Message
|
|
30
|
+
field :name, :string, 1
|
|
31
|
+
field :value, :string, 2
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
class Counter < Fast::Protowire::Message
|
|
35
|
+
field :value, :double, 1
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
class Metric < Fast::Protowire::Message
|
|
39
|
+
repeated :label, LabelPair, 1
|
|
40
|
+
field :counter, Counter, 3
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
metric = Metric.new(label: [{ name: "method", value: "GET" }], counter: { value: 12.0 })
|
|
44
|
+
bytes = metric.encode
|
|
45
|
+
Metric.decode(bytes) == metric # => true
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Fields encode in field-number order, unknown fields survive a decode/encode round trip,
|
|
49
|
+
and the output is byte-identical to what `protoc`-generated code and `google-protobuf`
|
|
50
|
+
produce for the same values. The [tutorial](docs/tutorials/encode-a-message.md) walks
|
|
51
|
+
through a full schema and proves that.
|
|
52
|
+
|
|
53
|
+
## Documentation
|
|
54
|
+
|
|
55
|
+
### Tutorials
|
|
56
|
+
|
|
57
|
+
- [Tutorial: declare, encode and decode a message](docs/tutorials/encode-a-message.md) — declare a slice of the Prometheus client model, encode, decode, and check the bytes against google-protobuf.
|
|
58
|
+
|
|
59
|
+
### How-to guides
|
|
60
|
+
|
|
61
|
+
- [How to declare a schema from a .proto file](docs/how-to/declare-a-schema-from-proto.md) — the keyword-by-keyword translation, including proto2, oneofs, maps, packing and forward references.
|
|
62
|
+
- [How to stream a large repeated field](docs/how-to/stream-a-large-repeated-field.md) — append thousands of entries to a message without holding them all at once.
|
|
63
|
+
- [How to use declared messages with protocol-grpc](docs/how-to/use-with-protocol-grpc.md) — request and response classes for `protocol-grpc` and `async-grpc` stubs.
|
|
64
|
+
- [How to verify parity against google-protobuf](docs/how-to/verify-parity-against-google-protobuf.md) — add a case to the parity suite, or run the same check in your project.
|
|
65
|
+
|
|
66
|
+
### Reference
|
|
67
|
+
|
|
68
|
+
- [Reference: Message](docs/reference/message.md) — the declaration DSL, the instance API, and the errors.
|
|
69
|
+
- [Reference: field types](docs/reference/field-types.md) — every scalar type with its wire type, Ruby value, range, presence and packing rules; enums.
|
|
70
|
+
- [Reference: Wire and Reader](docs/reference/wire.md) — the primitives for writing and reading the wire format directly.
|
|
71
|
+
|
|
72
|
+
### Explanation
|
|
73
|
+
|
|
74
|
+
- [Design: the wire format and nothing else](docs/explanation/design.md) — what google-protobuf costs per message, what this gem does instead, and what it leaves out.
|
|
75
|
+
- [How encoding works](docs/explanation/encoding.md) — compiled encoders, buffers, field order, presence, and decoding.
|
|
76
|
+
|
|
77
|
+
## Development
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
bundle exec sus
|
|
81
|
+
bundle exec rubocop
|
|
82
|
+
```
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fast
|
|
4
|
+
module Protowire
|
|
5
|
+
# Builds an enum module: one constant per value, plus lookup in both
|
|
6
|
+
# directions. Fields declared with the module store Symbols for known
|
|
7
|
+
# values and Integers for values the module does not name.
|
|
8
|
+
#
|
|
9
|
+
# MetricType = Fast::Protowire::Enum.define(COUNTER: 0, GAUGE: 1)
|
|
10
|
+
# MetricType::GAUGE # => 1
|
|
11
|
+
# MetricType.lookup(1) # => :GAUGE
|
|
12
|
+
# MetricType.resolve(:GAUGE) # => 1
|
|
13
|
+
module Enum
|
|
14
|
+
def self.define(**values)
|
|
15
|
+
Module.new do
|
|
16
|
+
extend Enum
|
|
17
|
+
values.each { |name, number| const_set(name, number) }
|
|
18
|
+
@by_name = values.freeze
|
|
19
|
+
@by_number = values.invert.freeze
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def lookup(number)
|
|
24
|
+
@by_number[number]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def resolve(name)
|
|
28
|
+
@by_name[name]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def values
|
|
32
|
+
@by_name
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# The value an unset field reads as: proto3 requires a zero value,
|
|
36
|
+
# proto2 falls back to the first declared one.
|
|
37
|
+
def default
|
|
38
|
+
@by_number[0] || @by_name.each_key.first
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "errors"
|
|
4
|
+
require_relative "wire"
|
|
5
|
+
require_relative "enum"
|
|
6
|
+
|
|
7
|
+
module Fast
|
|
8
|
+
module Protowire
|
|
9
|
+
# One declared field: its number, type and rule, and how to validate,
|
|
10
|
+
# encode and decode values of it. Built by the Message DSL.
|
|
11
|
+
class Field
|
|
12
|
+
SCALAR_WIRE_TYPES = {
|
|
13
|
+
double: Wire::FIXED64, float: Wire::FIXED32,
|
|
14
|
+
int32: Wire::VARINT, int64: Wire::VARINT, uint32: Wire::VARINT, uint64: Wire::VARINT,
|
|
15
|
+
sint32: Wire::VARINT, sint64: Wire::VARINT, bool: Wire::VARINT, enum: Wire::VARINT,
|
|
16
|
+
fixed32: Wire::FIXED32, sfixed32: Wire::FIXED32, fixed64: Wire::FIXED64, sfixed64: Wire::FIXED64,
|
|
17
|
+
string: Wire::LENGTH_DELIMITED, bytes: Wire::LENGTH_DELIMITED, message: Wire::LENGTH_DELIMITED
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
20
|
+
INTEGER_RANGES = {
|
|
21
|
+
int32: (-(1 << 31))...(1 << 31), sint32: (-(1 << 31))...(1 << 31), sfixed32: (-(1 << 31))...(1 << 31),
|
|
22
|
+
uint32: 0...(1 << 32), fixed32: 0...(1 << 32),
|
|
23
|
+
int64: (-(1 << 63))...(1 << 63), sint64: (-(1 << 63))...(1 << 63), sfixed64: (-(1 << 63))...(1 << 63),
|
|
24
|
+
uint64: 0...(1 << 64), fixed64: 0...(1 << 64)
|
|
25
|
+
}.freeze
|
|
26
|
+
|
|
27
|
+
FIXED_FORMATS = { double: "E", float: "e", fixed32: "L<", sfixed32: "l<", fixed64: "Q<", sfixed64: "q<" }.freeze
|
|
28
|
+
|
|
29
|
+
PACKABLE = (SCALAR_WIRE_TYPES.keys - %i[string bytes message]).freeze
|
|
30
|
+
|
|
31
|
+
MAP_KEY_TYPES = %i[int32 int64 uint32 uint64 sint32 sint64 fixed32 fixed64 sfixed32 sfixed64 bool string].freeze
|
|
32
|
+
|
|
33
|
+
private_constant :SCALAR_WIRE_TYPES, :INTEGER_RANGES, :FIXED_FORMATS, :PACKABLE, :MAP_KEY_TYPES
|
|
34
|
+
|
|
35
|
+
attr_reader :name, :number, :type, :rule, :oneof, :ivar, :enum
|
|
36
|
+
|
|
37
|
+
# +type+ is a scalar Symbol, an Enum module, a Message class, or a
|
|
38
|
+
# String / Proc naming a Message class resolved on first use (for
|
|
39
|
+
# recursive schemas). +rule+ is :implicit, :optional, :required,
|
|
40
|
+
# :repeated or :map.
|
|
41
|
+
def initialize(name, type, number, rule:, owner:, packed: nil, default: nil, oneof: nil, key_type: nil)
|
|
42
|
+
@name = name
|
|
43
|
+
@number = number
|
|
44
|
+
@rule = rule
|
|
45
|
+
@owner = owner
|
|
46
|
+
@oneof = oneof
|
|
47
|
+
@ivar = :"@#{name}"
|
|
48
|
+
resolve_type(type)
|
|
49
|
+
@packed = rule == :repeated && (packed.nil? ? owner.syntax == :proto3 && packable? : packed)
|
|
50
|
+
@default = default
|
|
51
|
+
@tag = Wire.tag(number, packed? ? Wire::LENGTH_DELIMITED : wire_type)
|
|
52
|
+
return unless map?
|
|
53
|
+
|
|
54
|
+
raise ArgumentError, "map key type #{key_type.inspect} is not allowed" unless MAP_KEY_TYPES.include?(key_type)
|
|
55
|
+
|
|
56
|
+
@key_field = Field.new(:key, key_type, 1, rule: :optional, owner: owner)
|
|
57
|
+
@value_field = Field.new(:value, type, 2, rule: :optional, owner: owner)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def message_class
|
|
61
|
+
@message_class ||= case @message_ref
|
|
62
|
+
when Proc then @message_ref.call
|
|
63
|
+
when String then namespace_of(@owner).const_get(@message_ref)
|
|
64
|
+
else @message_ref
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def wire_type
|
|
69
|
+
map? ? Wire::LENGTH_DELIMITED : SCALAR_WIRE_TYPES.fetch(type)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def repeated?
|
|
73
|
+
rule == :repeated
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def map?
|
|
77
|
+
rule == :map
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def packed?
|
|
81
|
+
@packed
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def packable?
|
|
85
|
+
PACKABLE.include?(type)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Whether an unset field is distinguishable from one set to its default.
|
|
89
|
+
def explicit_presence?
|
|
90
|
+
rule != :implicit || !oneof.nil? || type == :message
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def default_value
|
|
94
|
+
case rule
|
|
95
|
+
when :repeated then []
|
|
96
|
+
when :map then {}
|
|
97
|
+
else scalar_default
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# True when a field without presence carries nothing worth emitting.
|
|
102
|
+
# Floats compare bitwise, as the reference encoder does: -0.0 is sent.
|
|
103
|
+
def omit?(value)
|
|
104
|
+
return value.empty? if repeated? || map?
|
|
105
|
+
|
|
106
|
+
case type
|
|
107
|
+
when :enum then enum_number(value).zero?
|
|
108
|
+
when :message then false
|
|
109
|
+
when :double, :float then value.zero? && (1.0 / value).positive?
|
|
110
|
+
else value == scalar_default
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# A step of the compiled Message#encode: a lambda taking the message
|
|
115
|
+
# and the buffer that appends this field, or nothing when the field is
|
|
116
|
+
# unset or at a default it need not send. Everything the step needs
|
|
117
|
+
# (ivar name, tag bytes, enum, nested writers) is captured when it is
|
|
118
|
+
# built, so encoding does no per-field dispatch.
|
|
119
|
+
def encoder_step
|
|
120
|
+
ivar = @ivar
|
|
121
|
+
case rule
|
|
122
|
+
when :repeated then repeated_step(ivar)
|
|
123
|
+
when :map then map_step(ivar)
|
|
124
|
+
else singular_step(ivar)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Validates and normalizes a value the way an assignment would.
|
|
129
|
+
def coerce(value)
|
|
130
|
+
case rule
|
|
131
|
+
when :repeated
|
|
132
|
+
raise ::TypeError, "#{name} expects an Array" unless value.respond_to?(:to_ary)
|
|
133
|
+
|
|
134
|
+
value.to_ary.map { |v| coerce_one(v) }
|
|
135
|
+
when :map
|
|
136
|
+
raise ::TypeError, "#{name} expects a Hash" unless value.respond_to?(:to_hash)
|
|
137
|
+
|
|
138
|
+
value.to_hash.to_h { |k, v| [@key_field.coerce_one(k), @value_field.coerce_one(v)] }
|
|
139
|
+
else
|
|
140
|
+
value.nil? ? nil : coerce_one(value)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def encode(buffer, value)
|
|
145
|
+
case rule
|
|
146
|
+
when :repeated then encode_repeated(buffer, value)
|
|
147
|
+
when :map then encode_map(buffer, value)
|
|
148
|
+
else encode_one(buffer, value)
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Reads one occurrence of this field into +current+ (the value already
|
|
153
|
+
# held) and returns the value to store.
|
|
154
|
+
def decode(reader, wire_type, current)
|
|
155
|
+
case rule
|
|
156
|
+
when :repeated then decode_repeated(reader, wire_type, current)
|
|
157
|
+
when :map then decode_map_entry(reader, wire_type, current)
|
|
158
|
+
else
|
|
159
|
+
if type == :message && current
|
|
160
|
+
current.merge_from(Reader.new(expect(reader, wire_type).read_length_delimited))
|
|
161
|
+
else
|
|
162
|
+
read_one(reader, wire_type)
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
protected
|
|
168
|
+
|
|
169
|
+
def coerce_one(value)
|
|
170
|
+
case type
|
|
171
|
+
when :string then coerce_string(value)
|
|
172
|
+
when :bytes then coerce_bytes(value)
|
|
173
|
+
when :double, :float then coerce_float(value)
|
|
174
|
+
when :bool then coerce_bool(value)
|
|
175
|
+
when :enum then coerce_enum(value)
|
|
176
|
+
when :message then coerce_message(value)
|
|
177
|
+
else coerce_integer(value)
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def encode_one(buffer, value)
|
|
182
|
+
case type
|
|
183
|
+
when :message then Wire.append_length_delimited(buffer, @tag, value.encode)
|
|
184
|
+
when :string, :bytes then Wire.append_length_delimited(buffer, @tag, value)
|
|
185
|
+
else
|
|
186
|
+
buffer << @tag
|
|
187
|
+
append_scalar(buffer, value)
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def read_one(reader, wire_type)
|
|
192
|
+
expect(reader, wire_type)
|
|
193
|
+
case type
|
|
194
|
+
when :message then message_class.decode(reader.read_length_delimited)
|
|
195
|
+
when :string then reader.read_length_delimited.force_encoding(Encoding::UTF_8)
|
|
196
|
+
when :bytes then reader.read_length_delimited
|
|
197
|
+
else read_scalar(reader)
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
private
|
|
202
|
+
|
|
203
|
+
def resolve_type(type)
|
|
204
|
+
case type
|
|
205
|
+
when Symbol
|
|
206
|
+
unless SCALAR_WIRE_TYPES.key?(type) && type != :message
|
|
207
|
+
raise ArgumentError,
|
|
208
|
+
"unknown field type #{type.inspect}"
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
@type = type
|
|
212
|
+
when Module
|
|
213
|
+
if type.is_a?(Enum)
|
|
214
|
+
@type = :enum
|
|
215
|
+
@enum = type
|
|
216
|
+
else
|
|
217
|
+
@type = :message
|
|
218
|
+
@message_ref = type
|
|
219
|
+
end
|
|
220
|
+
when String, Proc
|
|
221
|
+
@type = :message
|
|
222
|
+
@message_ref = type
|
|
223
|
+
else
|
|
224
|
+
raise ArgumentError, "unknown field type #{type.inspect}"
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# A String type names a class relative to where the owner is declared,
|
|
229
|
+
# so a message can refer to itself or a sibling declared later.
|
|
230
|
+
def namespace_of(owner)
|
|
231
|
+
namespace = owner.name.to_s.rpartition("::").first
|
|
232
|
+
namespace.empty? ? Object : Object.const_get(namespace)
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def scalar_default
|
|
236
|
+
return @default unless @default.nil?
|
|
237
|
+
|
|
238
|
+
case type
|
|
239
|
+
when :string then ""
|
|
240
|
+
when :bytes then "".b
|
|
241
|
+
when :double, :float then 0.0
|
|
242
|
+
when :bool then false
|
|
243
|
+
when :enum then @enum.default
|
|
244
|
+
when :message then nil
|
|
245
|
+
else 0
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def enum_number(value)
|
|
250
|
+
value.is_a?(Symbol) ? @enum.resolve(value) : value
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# -- compiled encoder steps --------------------------------------------
|
|
254
|
+
|
|
255
|
+
def singular_step(ivar)
|
|
256
|
+
write = writer
|
|
257
|
+
if explicit_presence?
|
|
258
|
+
->(message, buffer) { (value = message.instance_variable_get(ivar)).nil? || write.call(buffer, value) }
|
|
259
|
+
else
|
|
260
|
+
omit = omitter
|
|
261
|
+
lambda do |message, buffer|
|
|
262
|
+
value = message.instance_variable_get(ivar)
|
|
263
|
+
write.call(buffer, value) unless value.nil? || omit.call(value)
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def repeated_step(ivar)
|
|
269
|
+
if packed?
|
|
270
|
+
tag = @tag
|
|
271
|
+
scalar = scalar_writer
|
|
272
|
+
lambda do |message, buffer|
|
|
273
|
+
values = message.instance_variable_get(ivar)
|
|
274
|
+
next if values.empty?
|
|
275
|
+
|
|
276
|
+
payload = String.new
|
|
277
|
+
values.each { |value| scalar.call(payload, value) }
|
|
278
|
+
Wire.append_length_delimited(buffer, tag, payload)
|
|
279
|
+
end
|
|
280
|
+
else
|
|
281
|
+
write = writer
|
|
282
|
+
->(message, buffer) { message.instance_variable_get(ivar).each { |value| write.call(buffer, value) } }
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def map_step(ivar)
|
|
287
|
+
tag = @tag
|
|
288
|
+
key = @key_field.writer
|
|
289
|
+
value = @value_field.writer
|
|
290
|
+
lambda do |message, buffer|
|
|
291
|
+
message.instance_variable_get(ivar).each do |k, v|
|
|
292
|
+
entry = String.new
|
|
293
|
+
key.call(entry, k)
|
|
294
|
+
value.call(entry, v)
|
|
295
|
+
Wire.append_length_delimited(buffer, tag, entry)
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
# Appends the tag and one value.
|
|
301
|
+
def writer
|
|
302
|
+
tag = @tag
|
|
303
|
+
case type
|
|
304
|
+
when :message then ->(buffer, value) { Wire.append_length_delimited(buffer, tag, value.encode) }
|
|
305
|
+
when :string, :bytes then ->(buffer, value) { Wire.append_length_delimited(buffer, tag, value) }
|
|
306
|
+
else
|
|
307
|
+
scalar = scalar_writer
|
|
308
|
+
lambda do |buffer, value|
|
|
309
|
+
buffer << tag
|
|
310
|
+
scalar.call(buffer, value)
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
# Appends one scalar value without its tag, as packed fields need.
|
|
316
|
+
def scalar_writer
|
|
317
|
+
case type
|
|
318
|
+
when :int32, :int64, :uint32, :uint64 then ->(buffer, value) { Wire.append_varint(buffer, value) }
|
|
319
|
+
when :sint32 then ->(buffer, value) { Wire.append_varint(buffer, Wire.zigzag32(value)) }
|
|
320
|
+
when :sint64 then ->(buffer, value) { Wire.append_varint(buffer, Wire.zigzag64(value)) }
|
|
321
|
+
when :bool then ->(buffer, value) { buffer << (value ? 1 : 0) }
|
|
322
|
+
when :enum
|
|
323
|
+
enum = @enum
|
|
324
|
+
->(buffer, value) { Wire.append_varint(buffer, value.is_a?(Symbol) ? enum.resolve(value) : value) }
|
|
325
|
+
else
|
|
326
|
+
format = FIXED_FORMATS.fetch(type)
|
|
327
|
+
->(buffer, value) { [value].pack(format, buffer: buffer) }
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
# Decides whether a field without presence is at a value it need not
|
|
332
|
+
# send. Floats compare bitwise, as the reference encoder does.
|
|
333
|
+
def omitter
|
|
334
|
+
case type
|
|
335
|
+
when :string, :bytes then ->(value) { value.empty? } # rubocop:disable Style/SymbolProc
|
|
336
|
+
when :bool then ->(value) { !value } # rubocop:disable Style/SymbolProc
|
|
337
|
+
when :double, :float then ->(value) { value.zero? && (1.0 / value).positive? }
|
|
338
|
+
when :enum
|
|
339
|
+
enum = @enum
|
|
340
|
+
->(value) { (value.is_a?(Symbol) ? enum.resolve(value) : value).zero? }
|
|
341
|
+
else
|
|
342
|
+
default = scalar_default
|
|
343
|
+
->(value) { value == default }
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
# -- validation --------------------------------------------------------
|
|
348
|
+
|
|
349
|
+
def coerce_string(value)
|
|
350
|
+
raise ::TypeError, "#{name} expects a String, got #{value.class}" unless value.is_a?(String)
|
|
351
|
+
|
|
352
|
+
string = case value.encoding
|
|
353
|
+
when Encoding::UTF_8 then value
|
|
354
|
+
when Encoding::BINARY then value.dup.force_encoding(Encoding::UTF_8)
|
|
355
|
+
else value.encode(Encoding::UTF_8)
|
|
356
|
+
end
|
|
357
|
+
raise ::ArgumentError, "#{name}: string is not valid UTF-8" unless string.valid_encoding?
|
|
358
|
+
|
|
359
|
+
string
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
def coerce_bytes(value)
|
|
363
|
+
raise ::TypeError, "#{name} expects a String, got #{value.class}" unless value.is_a?(String)
|
|
364
|
+
|
|
365
|
+
value.encoding == Encoding::BINARY ? value : value.b
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
def coerce_float(value)
|
|
369
|
+
raise ::TypeError, "#{name} expects a number, got #{value.class}" unless value.is_a?(Numeric)
|
|
370
|
+
|
|
371
|
+
value.to_f
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
def coerce_bool(value)
|
|
375
|
+
return value if [true, false].include?(value)
|
|
376
|
+
|
|
377
|
+
raise ::TypeError, "#{name} expects true or false, got #{value.class}"
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
def coerce_enum(value)
|
|
381
|
+
case value
|
|
382
|
+
when Symbol
|
|
383
|
+
raise ::RangeError, "#{name}: unknown enum value #{value.inspect}" unless @enum.resolve(value)
|
|
384
|
+
|
|
385
|
+
value
|
|
386
|
+
when Integer
|
|
387
|
+
raise ::RangeError, "#{name}: #{value} is out of range" unless INTEGER_RANGES[:int32].cover?(value)
|
|
388
|
+
|
|
389
|
+
@enum.lookup(value) || value
|
|
390
|
+
else
|
|
391
|
+
raise ::TypeError, "#{name} expects a Symbol or Integer, got #{value.class}"
|
|
392
|
+
end
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
def coerce_message(value)
|
|
396
|
+
return message_class.new(value) if value.is_a?(Hash)
|
|
397
|
+
return value if value.is_a?(message_class)
|
|
398
|
+
|
|
399
|
+
raise ::TypeError, "#{name} expects a #{message_class}, got #{value.class}"
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
def coerce_integer(value)
|
|
403
|
+
raise ::TypeError, "#{name} expects an Integer, got #{value.class}" unless value.is_a?(Numeric)
|
|
404
|
+
|
|
405
|
+
integer = value.to_i
|
|
406
|
+
raise ::RangeError, "#{name}: #{value} is not an integer" unless integer == value
|
|
407
|
+
raise ::RangeError, "#{name}: #{value} is out of range" unless INTEGER_RANGES.fetch(type).cover?(integer)
|
|
408
|
+
|
|
409
|
+
integer
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
# -- encoding ----------------------------------------------------------
|
|
413
|
+
|
|
414
|
+
def encode_repeated(buffer, values)
|
|
415
|
+
return if values.empty?
|
|
416
|
+
|
|
417
|
+
if packed?
|
|
418
|
+
Wire.append_length_delimited_from(buffer, @tag) { |b| values.each { |v| append_scalar(b, v) } }
|
|
419
|
+
else
|
|
420
|
+
values.each { |v| encode_one(buffer, v) }
|
|
421
|
+
end
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
def encode_map(buffer, hash)
|
|
425
|
+
hash.each do |key, value|
|
|
426
|
+
Wire.append_length_delimited_from(buffer, @tag) do |entry|
|
|
427
|
+
@key_field.encode_one(entry, key)
|
|
428
|
+
@value_field.encode_one(entry, value)
|
|
429
|
+
end
|
|
430
|
+
end
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
def append_scalar(buffer, value)
|
|
434
|
+
case type
|
|
435
|
+
when :int32, :int64, :uint32, :uint64 then Wire.append_varint(buffer, value)
|
|
436
|
+
when :sint32 then Wire.append_varint(buffer, Wire.zigzag32(value))
|
|
437
|
+
when :sint64 then Wire.append_varint(buffer, Wire.zigzag64(value))
|
|
438
|
+
when :bool then buffer << (value ? 1 : 0)
|
|
439
|
+
when :enum then Wire.append_varint(buffer, enum_number(value))
|
|
440
|
+
else buffer << [value].pack(FIXED_FORMATS.fetch(type))
|
|
441
|
+
end
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
# -- decoding ----------------------------------------------------------
|
|
445
|
+
|
|
446
|
+
def decode_repeated(reader, wire_type, values)
|
|
447
|
+
if packable? && wire_type == Wire::LENGTH_DELIMITED
|
|
448
|
+
packed = reader.read_packed
|
|
449
|
+
values << read_scalar(packed) until packed.eof?
|
|
450
|
+
else
|
|
451
|
+
values << read_one(reader, wire_type)
|
|
452
|
+
end
|
|
453
|
+
values
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
def decode_map_entry(reader, wire_type, hash)
|
|
457
|
+
entry = Reader.new(expect(reader, wire_type).read_length_delimited)
|
|
458
|
+
key = @key_field.default_value
|
|
459
|
+
value = @value_field.default_value
|
|
460
|
+
until entry.eof?
|
|
461
|
+
number, entry_wire_type = entry.read_tag
|
|
462
|
+
case number
|
|
463
|
+
when 1 then key = @key_field.read_one(entry, entry_wire_type)
|
|
464
|
+
when 2 then value = @value_field.decode(entry, entry_wire_type, value)
|
|
465
|
+
else entry.skip(entry_wire_type)
|
|
466
|
+
end
|
|
467
|
+
end
|
|
468
|
+
hash[key] = value
|
|
469
|
+
hash
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
def expect(reader, wire_type)
|
|
473
|
+
return reader if wire_type == self.wire_type
|
|
474
|
+
|
|
475
|
+
raise DecodeError, "field #{name} (#{number}) has wire type #{wire_type}, expected #{self.wire_type}"
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
def read_scalar(reader)
|
|
479
|
+
case type
|
|
480
|
+
when :int32 then signed(reader.read_varint, 32)
|
|
481
|
+
when :enum then coerce_enum(signed(reader.read_varint, 32))
|
|
482
|
+
when :int64 then signed(reader.read_varint, 64)
|
|
483
|
+
when :uint32 then reader.read_varint & 0xFFFF_FFFF
|
|
484
|
+
when :uint64 then reader.read_varint
|
|
485
|
+
when :sint32, :sint64 then Wire.unzigzag(reader.read_varint)
|
|
486
|
+
when :bool then reader.read_varint != 0
|
|
487
|
+
when :double, :fixed64, :sfixed64 then reader.read_bytes(8).unpack1(FIXED_FORMATS.fetch(type))
|
|
488
|
+
else reader.read_bytes(4).unpack1(FIXED_FORMATS.fetch(type))
|
|
489
|
+
end
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
def signed(value, bits)
|
|
493
|
+
value &= (1 << bits) - 1
|
|
494
|
+
value >= (1 << (bits - 1)) ? value - (1 << bits) : value
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
# A map's key and value fields build the entry writer together.
|
|
498
|
+
protected :writer
|
|
499
|
+
end
|
|
500
|
+
end
|
|
501
|
+
end
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "errors"
|
|
4
|
+
require_relative "wire"
|
|
5
|
+
require_relative "reader"
|
|
6
|
+
require_relative "field"
|
|
7
|
+
|
|
8
|
+
module Fast
|
|
9
|
+
module Protowire
|
|
10
|
+
# Base class for declared messages. Subclasses describe their fields with
|
|
11
|
+
# the class-level DSL, mirroring the .proto text:
|
|
12
|
+
#
|
|
13
|
+
# class LabelPair < Fast::Protowire::Message
|
|
14
|
+
# syntax :proto2
|
|
15
|
+
# optional :name, :string, 1
|
|
16
|
+
# optional :value, :string, 2
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
# class Metric < Fast::Protowire::Message
|
|
20
|
+
# repeated :label, LabelPair, 1
|
|
21
|
+
# optional :counter, Counter, 3
|
|
22
|
+
# end
|
|
23
|
+
#
|
|
24
|
+
# +field+ declares proto3 implicit presence (omitted when at the default);
|
|
25
|
+
# +optional+ / +required+ declare explicit presence (has_x? tells set from
|
|
26
|
+
# unset); +repeated+ and +map+ hold Arrays and Hashes; +oneof+ groups
|
|
27
|
+
# fields so setting one clears the others. Fields encode in field-number
|
|
28
|
+
# order, unknown fields survive decode and re-encode, so bytes match what
|
|
29
|
+
# the reference implementation produces for the same values.
|
|
30
|
+
class Message
|
|
31
|
+
class << self
|
|
32
|
+
attr_reader :fields, :fields_by_number, :oneofs
|
|
33
|
+
|
|
34
|
+
def inherited(subclass)
|
|
35
|
+
super
|
|
36
|
+
subclass.instance_variable_set(:@fields, {})
|
|
37
|
+
subclass.instance_variable_set(:@fields_by_number, {})
|
|
38
|
+
subclass.instance_variable_set(:@oneofs, {})
|
|
39
|
+
subclass.instance_variable_set(:@syntax, :proto3)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def syntax(value = nil)
|
|
43
|
+
return @syntax if value.nil?
|
|
44
|
+
|
|
45
|
+
raise ArgumentError, "syntax must be :proto2 or :proto3" unless %i[proto2 proto3].include?(value)
|
|
46
|
+
|
|
47
|
+
@syntax = value
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# proto3 `type name = N;` — implicit presence. Under proto2 syntax
|
|
51
|
+
# this is the same as +optional+, since proto2 has no implicit fields.
|
|
52
|
+
def field(name, type, number, **options)
|
|
53
|
+
add(name, type, number, rule: @syntax == :proto2 ? :optional : :implicit, **options)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def optional(name, type, number, **options)
|
|
57
|
+
add(name, type, number, rule: :optional, **options)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def required(name, type, number, **options)
|
|
61
|
+
add(name, type, number, rule: :required, **options)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def repeated(name, type, number, packed: nil)
|
|
65
|
+
add(name, type, number, rule: :repeated, packed: packed)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def map(name, key_type, value_type, number)
|
|
69
|
+
add(name, value_type, number, rule: :map, key_type: key_type)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Fields declared inside the block are members of the oneof +name+;
|
|
73
|
+
# the message gains a +name+ reader returning the set member's name.
|
|
74
|
+
def oneof(name, &block)
|
|
75
|
+
raise ArgumentError, "oneof declarations do not nest" if @current_oneof
|
|
76
|
+
|
|
77
|
+
@current_oneof = name
|
|
78
|
+
@oneofs[name] = []
|
|
79
|
+
instance_eval(&block)
|
|
80
|
+
members = @oneofs[name]
|
|
81
|
+
define_method(name) { members.find { |member| !instance_variable_get(:"@#{member}").nil? } }
|
|
82
|
+
ensure
|
|
83
|
+
@current_oneof = nil
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def decode(bytes)
|
|
87
|
+
new.merge_from(Reader.new(bytes))
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def encode(message)
|
|
91
|
+
message.encode
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Defines this class's own #encode from one step per field in number
|
|
95
|
+
# order (see Field#encoder_step). Runs once, on the first encode after
|
|
96
|
+
# the last declaration.
|
|
97
|
+
def compile_encoder
|
|
98
|
+
steps = sorted_fields.map(&:encoder_step)
|
|
99
|
+
define_method(:encode) do |buffer = String.new|
|
|
100
|
+
steps.each { |step| step.call(self, buffer) }
|
|
101
|
+
buffer << @unknown_fields if @unknown_fields
|
|
102
|
+
buffer
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def sorted_fields
|
|
107
|
+
@sorted_fields ||= @fields.values.sort_by(&:number).freeze
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def container_fields
|
|
111
|
+
@container_fields ||= @fields.values.select { |field| field.repeated? || field.map? }.freeze
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
private
|
|
115
|
+
|
|
116
|
+
def add(name, type, number, rule:, **options)
|
|
117
|
+
raise ArgumentError, "field #{name} already declared" if @fields.key?(name)
|
|
118
|
+
raise ArgumentError, "field number #{number} already used" if @fields_by_number.key?(number)
|
|
119
|
+
|
|
120
|
+
field = Field.new(name, type, number, rule: rule, owner: self, oneof: @current_oneof, **options)
|
|
121
|
+
@fields[name] = field
|
|
122
|
+
@fields_by_number[number] = field
|
|
123
|
+
@oneofs[@current_oneof] << name if @current_oneof
|
|
124
|
+
@sorted_fields = nil
|
|
125
|
+
@container_fields = nil
|
|
126
|
+
remove_method(:encode) if instance_methods(false).include?(:encode)
|
|
127
|
+
define_accessors(field)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def define_accessors(field)
|
|
131
|
+
ivar = field.ivar
|
|
132
|
+
define_method(field.name) do
|
|
133
|
+
value = instance_variable_get(ivar)
|
|
134
|
+
value.nil? ? field.default_value : value
|
|
135
|
+
end
|
|
136
|
+
define_method(:"#{field.name}=") { |value| write_field(field, field.coerce(value)) }
|
|
137
|
+
return unless field.explicit_presence?
|
|
138
|
+
|
|
139
|
+
define_method(:"has_#{field.name}?") { !instance_variable_get(ivar).nil? }
|
|
140
|
+
define_method(:"clear_#{field.name}") { instance_variable_set(ivar, nil) }
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Scalar and message ivars stay unset until written (an unset ivar reads
|
|
145
|
+
# as nil); repeated and map fields get their container up front so it
|
|
146
|
+
# can be mutated in place.
|
|
147
|
+
def initialize(attributes = nil, **keywords)
|
|
148
|
+
self.class.container_fields.each { |field| instance_variable_set(field.ivar, field.default_value) }
|
|
149
|
+
@unknown_fields = nil
|
|
150
|
+
(attributes || keywords).each do |name, value|
|
|
151
|
+
field = self.class.fields[name.to_sym]
|
|
152
|
+
raise ArgumentError, "unknown field #{name.inspect} for #{self.class}" unless field
|
|
153
|
+
|
|
154
|
+
write_field(field, field.coerce(value))
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
attr_reader :unknown_fields
|
|
159
|
+
|
|
160
|
+
# Appends this message's bytes to +buffer+ and returns it. The first
|
|
161
|
+
# call compiles the class's own encode (see compile_encoder); this
|
|
162
|
+
# generic one is only ever reached before that.
|
|
163
|
+
def encode(buffer = String.new)
|
|
164
|
+
self.class.compile_encoder
|
|
165
|
+
encode(buffer)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def to_proto(buffer = String.new)
|
|
169
|
+
encode(buffer)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Reads fields from +reader+ into this message (protobuf merge
|
|
173
|
+
# semantics: later scalars win, repeated fields append, nested messages
|
|
174
|
+
# merge) and returns self.
|
|
175
|
+
def merge_from(reader)
|
|
176
|
+
until reader.eof?
|
|
177
|
+
number, wire_type = reader.read_tag
|
|
178
|
+
field = self.class.fields_by_number[number]
|
|
179
|
+
if field
|
|
180
|
+
write_field(field, field.decode(reader, wire_type, instance_variable_get(field.ivar)))
|
|
181
|
+
else
|
|
182
|
+
(@unknown_fields ||= String.new) << Wire.varint((number << 3) | wire_type) << reader.skip(wire_type)
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
self
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def to_h
|
|
189
|
+
self.class.fields.each_value.with_object({}) do |field, hash|
|
|
190
|
+
value = instance_variable_get(field.ivar)
|
|
191
|
+
next if value.nil? && field.explicit_presence?
|
|
192
|
+
|
|
193
|
+
hash[field.name] = hashify(value.nil? ? field.default_value : value)
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Two messages are equal when every field reads the same; an implicit
|
|
198
|
+
# field set to its default is the same as one never set.
|
|
199
|
+
def ==(other)
|
|
200
|
+
other.class == self.class && comparable_values == other.comparable_values
|
|
201
|
+
end
|
|
202
|
+
alias eql? ==
|
|
203
|
+
|
|
204
|
+
def hash
|
|
205
|
+
comparable_values.hash
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def inspect
|
|
209
|
+
set = self.class.fields.each_value.filter_map do |field|
|
|
210
|
+
value = instance_variable_get(field.ivar)
|
|
211
|
+
"#{field.name}: #{value.inspect}" unless value.nil? || field.omit?(value)
|
|
212
|
+
end
|
|
213
|
+
"#<#{self.class.name} #{set.join(', ')}>"
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def initialize_copy(source)
|
|
217
|
+
super
|
|
218
|
+
self.class.fields.each_value do |field|
|
|
219
|
+
value = source.instance_variable_get(field.ivar)
|
|
220
|
+
instance_variable_set(field.ivar, deep_copy(value))
|
|
221
|
+
end
|
|
222
|
+
@unknown_fields = source.unknown_fields&.dup
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
protected
|
|
226
|
+
|
|
227
|
+
def comparable_values
|
|
228
|
+
values = self.class.fields.each_value.map do |field|
|
|
229
|
+
value = instance_variable_get(field.ivar)
|
|
230
|
+
value.nil? && !field.explicit_presence? ? field.default_value : value
|
|
231
|
+
end
|
|
232
|
+
values << @unknown_fields
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
private
|
|
236
|
+
|
|
237
|
+
def write_field(field, value)
|
|
238
|
+
if field.oneof && !value.nil?
|
|
239
|
+
self.class.oneofs[field.oneof].each { |member| instance_variable_set(:"@#{member}", nil) }
|
|
240
|
+
end
|
|
241
|
+
instance_variable_set(field.ivar, value)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def hashify(value)
|
|
245
|
+
case value
|
|
246
|
+
when Message then value.to_h
|
|
247
|
+
when Array then value.map { |v| v.is_a?(Message) ? v.to_h : v }
|
|
248
|
+
when Hash then value.transform_values { |v| v.is_a?(Message) ? v.to_h : v }
|
|
249
|
+
else value
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def deep_copy(value)
|
|
254
|
+
case value
|
|
255
|
+
when Message, String then value.dup
|
|
256
|
+
when Array then value.map { |v| deep_copy(v) }
|
|
257
|
+
when Hash then value.to_h { |k, v| [k, deep_copy(v)] }
|
|
258
|
+
else value
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "errors"
|
|
4
|
+
require_relative "wire"
|
|
5
|
+
|
|
6
|
+
module Fast
|
|
7
|
+
module Protowire
|
|
8
|
+
# A cursor over an encoded message. Reads the primitive wire values and
|
|
9
|
+
# skips what it is not asked to interpret.
|
|
10
|
+
class Reader
|
|
11
|
+
def initialize(buffer, position = 0, limit = buffer.bytesize)
|
|
12
|
+
@buffer = buffer
|
|
13
|
+
@position = position
|
|
14
|
+
@limit = limit
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
attr_reader :position
|
|
18
|
+
|
|
19
|
+
def eof?
|
|
20
|
+
@position >= @limit
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Returns [field number, wire type].
|
|
24
|
+
def read_tag
|
|
25
|
+
key = read_varint
|
|
26
|
+
[key >> 3, key & 0x7]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def read_varint
|
|
30
|
+
result = 0
|
|
31
|
+
shift = 0
|
|
32
|
+
loop do
|
|
33
|
+
raise DecodeError, "truncated varint" if @position >= @limit
|
|
34
|
+
|
|
35
|
+
byte = @buffer.getbyte(@position)
|
|
36
|
+
@position += 1
|
|
37
|
+
result |= (byte & 0x7f) << shift
|
|
38
|
+
return result if byte < 0x80
|
|
39
|
+
|
|
40
|
+
shift += 7
|
|
41
|
+
raise DecodeError, "varint too long" if shift > 63
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def read_fixed32
|
|
46
|
+
read_bytes(4).unpack1("L<")
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def read_fixed64
|
|
50
|
+
read_bytes(8).unpack1("Q<")
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def read_bytes(length)
|
|
54
|
+
raise DecodeError, "truncated field" if @position + length > @limit
|
|
55
|
+
|
|
56
|
+
bytes = @buffer.byteslice(@position, length)
|
|
57
|
+
@position += length
|
|
58
|
+
bytes
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def read_length_delimited
|
|
62
|
+
read_bytes(read_varint)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# A reader bounded to the next length-delimited value, for packed fields.
|
|
66
|
+
def read_packed
|
|
67
|
+
length = read_varint
|
|
68
|
+
raise DecodeError, "truncated packed field" if @position + length > @limit
|
|
69
|
+
|
|
70
|
+
reader = Reader.new(@buffer, @position, @position + length)
|
|
71
|
+
@position += length
|
|
72
|
+
reader
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Skips one value of +wire_type+ and returns its raw bytes, so unknown
|
|
76
|
+
# fields survive a decode/encode round trip.
|
|
77
|
+
def skip(wire_type)
|
|
78
|
+
start = @position
|
|
79
|
+
case wire_type
|
|
80
|
+
when Wire::VARINT then read_varint
|
|
81
|
+
when Wire::FIXED64 then read_bytes(8)
|
|
82
|
+
when Wire::LENGTH_DELIMITED then read_length_delimited
|
|
83
|
+
when Wire::START_GROUP then skip_group
|
|
84
|
+
when Wire::FIXED32 then read_bytes(4)
|
|
85
|
+
else raise DecodeError, "unknown wire type #{wire_type}"
|
|
86
|
+
end
|
|
87
|
+
@buffer.byteslice(start, @position - start)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
def skip_group
|
|
93
|
+
loop do
|
|
94
|
+
number, wire_type = read_tag
|
|
95
|
+
return if wire_type == Wire::END_GROUP
|
|
96
|
+
raise DecodeError, "invalid group" if number.zero?
|
|
97
|
+
|
|
98
|
+
skip(wire_type)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fast
|
|
4
|
+
module Protowire
|
|
5
|
+
# The wire format itself: tags, varints, zigzag, fixed-width values and
|
|
6
|
+
# length prefixes, appended to a binary String buffer. Everything above
|
|
7
|
+
# this module is bookkeeping about which field gets which of these.
|
|
8
|
+
module Wire
|
|
9
|
+
VARINT = 0
|
|
10
|
+
FIXED64 = 1
|
|
11
|
+
LENGTH_DELIMITED = 2
|
|
12
|
+
START_GROUP = 3
|
|
13
|
+
END_GROUP = 4
|
|
14
|
+
FIXED32 = 5
|
|
15
|
+
|
|
16
|
+
UINT64_MASK = (1 << 64) - 1
|
|
17
|
+
private_constant :UINT64_MASK
|
|
18
|
+
|
|
19
|
+
module_function
|
|
20
|
+
|
|
21
|
+
# The key for +number+ / +wire_type+ as frozen bytes.
|
|
22
|
+
def tag(number, wire_type)
|
|
23
|
+
varint((number << 3) | wire_type).freeze
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def varint(value)
|
|
27
|
+
append_varint(String.new(capacity: 10), value)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Base-128 little-endian; negative values are sign-extended to 64 bits
|
|
31
|
+
# exactly as int32/int64 fields require (ten bytes).
|
|
32
|
+
def append_varint(buffer, value)
|
|
33
|
+
value &= UINT64_MASK if value.negative?
|
|
34
|
+
while value > 0x7f
|
|
35
|
+
buffer << ((value & 0x7f) | 0x80)
|
|
36
|
+
value >>= 7
|
|
37
|
+
end
|
|
38
|
+
buffer << value
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def zigzag32(value)
|
|
42
|
+
(value << 1) ^ (value >> 31)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def zigzag64(value)
|
|
46
|
+
(value << 1) ^ (value >> 63)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def unzigzag(value)
|
|
50
|
+
(value >> 1) ^ -(value & 1)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# A length-delimited field: +tag+ then the varint size of +bytes+ then
|
|
54
|
+
# +bytes+. Non-ASCII text is appended as its bytes so the binary buffer
|
|
55
|
+
# never trips Ruby's encoding compatibility check.
|
|
56
|
+
def append_length_delimited(buffer, tag, bytes)
|
|
57
|
+
buffer << tag
|
|
58
|
+
append_varint(buffer, bytes.bytesize)
|
|
59
|
+
buffer << (bytes.ascii_only? ? bytes : bytes.b)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# A length-delimited field whose payload the block writes into a fresh
|
|
63
|
+
# buffer. (Writing into the parent and inserting the size afterwards
|
|
64
|
+
# looks cheaper but String#insert costs O(size) on the parent.)
|
|
65
|
+
def append_length_delimited_from(buffer, tag)
|
|
66
|
+
payload = String.new
|
|
67
|
+
yield payload
|
|
68
|
+
append_length_delimited(buffer, tag, payload)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "protowire/version"
|
|
4
|
+
require_relative "protowire/errors"
|
|
5
|
+
require_relative "protowire/wire"
|
|
6
|
+
require_relative "protowire/reader"
|
|
7
|
+
require_relative "protowire/enum"
|
|
8
|
+
require_relative "protowire/field"
|
|
9
|
+
require_relative "protowire/message"
|
|
10
|
+
|
|
11
|
+
module Fast
|
|
12
|
+
# Protocol Buffers wire format: declare messages, encode and decode bytes.
|
|
13
|
+
# No descriptors, reflection or JSON — the wire layer only.
|
|
14
|
+
module Protowire
|
|
15
|
+
end
|
|
16
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: fast-protowire
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Eric Jacobs
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
executables: []
|
|
13
|
+
extensions: []
|
|
14
|
+
extra_rdoc_files: []
|
|
15
|
+
files:
|
|
16
|
+
- AGENTS.md
|
|
17
|
+
- CHANGELOG.md
|
|
18
|
+
- README.md
|
|
19
|
+
- lib/fast/protowire.rb
|
|
20
|
+
- lib/fast/protowire/enum.rb
|
|
21
|
+
- lib/fast/protowire/errors.rb
|
|
22
|
+
- lib/fast/protowire/field.rb
|
|
23
|
+
- lib/fast/protowire/message.rb
|
|
24
|
+
- lib/fast/protowire/reader.rb
|
|
25
|
+
- lib/fast/protowire/version.rb
|
|
26
|
+
- lib/fast/protowire/wire.rb
|
|
27
|
+
homepage: https://github.com/jetpks/fast-protowire
|
|
28
|
+
licenses:
|
|
29
|
+
- MIT
|
|
30
|
+
metadata:
|
|
31
|
+
homepage_uri: https://github.com/jetpks/fast-protowire
|
|
32
|
+
source_code_uri: https://github.com/jetpks/fast-protowire
|
|
33
|
+
changelog_uri: https://github.com/jetpks/fast-protowire/blob/main/CHANGELOG.md
|
|
34
|
+
rubygems_mfa_required: 'true'
|
|
35
|
+
rdoc_options: []
|
|
36
|
+
require_paths:
|
|
37
|
+
- lib
|
|
38
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
39
|
+
requirements:
|
|
40
|
+
- - ">="
|
|
41
|
+
- !ruby/object:Gem::Version
|
|
42
|
+
version: '3.3'
|
|
43
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - ">="
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '0'
|
|
48
|
+
requirements: []
|
|
49
|
+
rubygems_version: 4.0.10
|
|
50
|
+
specification_version: 4
|
|
51
|
+
summary: 'Protocol Buffers wire format for Ruby: declare messages, encode and decode
|
|
52
|
+
bytes, no runtime.'
|
|
53
|
+
test_files: []
|