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 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,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fast
4
+ module Protowire
5
+ class Error < StandardError; end
6
+
7
+ # Bytes that do not parse as a message of the declared shape.
8
+ class DecodeError < Error; end
9
+ end
10
+ 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,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fast
4
+ module Protowire
5
+ VERSION = "0.1.0"
6
+ end
7
+ 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: []