protobug 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/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/LICENSE.txt +21 -0
- data/README.md +39 -0
- data/lib/protobug/base_descriptor.rb +7 -0
- data/lib/protobug/binary_encoding.rb +108 -0
- data/lib/protobug/enum.rb +146 -0
- data/lib/protobug/errors.rb +32 -0
- data/lib/protobug/field.rb +746 -0
- data/lib/protobug/message.rb +344 -0
- data/lib/protobug/registry.rb +36 -0
- data/lib/protobug/version.rb +5 -0
- data/lib/protobug.rb +14 -0
- metadata +60 -0
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "binary_encoding"
|
|
4
|
+
require_relative "errors"
|
|
5
|
+
require_relative "field"
|
|
6
|
+
require "stringio"
|
|
7
|
+
|
|
8
|
+
module Protobug
|
|
9
|
+
UNSET = Object.new
|
|
10
|
+
def UNSET.inspect
|
|
11
|
+
"<UNSET>"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def UNSET.===(other)
|
|
15
|
+
other.equal? UNSET
|
|
16
|
+
end
|
|
17
|
+
UNSET.freeze
|
|
18
|
+
|
|
19
|
+
module Message
|
|
20
|
+
def self.extended(base)
|
|
21
|
+
base.class_eval do
|
|
22
|
+
@full_name = nil
|
|
23
|
+
@fields_by_number = {}
|
|
24
|
+
@fields_by_json_name = {}
|
|
25
|
+
@fields_by_name = {}
|
|
26
|
+
@reserved_ranges = []
|
|
27
|
+
@oneofs = {}
|
|
28
|
+
extend BaseDescriptor
|
|
29
|
+
include Protobug::Message::InstanceMethods
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
attr_accessor :full_name
|
|
34
|
+
attr_reader :fields_by_number, :fields_by_name, :fields_by_json_name, :reserved_ranges, :oneofs
|
|
35
|
+
|
|
36
|
+
def freeze
|
|
37
|
+
fields_by_number.freeze
|
|
38
|
+
fields_by_name.freeze
|
|
39
|
+
fields_by_json_name.freeze
|
|
40
|
+
full_name.freeze
|
|
41
|
+
reserved_ranges.freeze
|
|
42
|
+
oneofs.each_value(&:freeze)
|
|
43
|
+
oneofs.freeze
|
|
44
|
+
super
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def optional(number, name, **kwargs)
|
|
48
|
+
if kwargs[:cardinality] && kwargs[:cardinality] != :optional
|
|
49
|
+
raise DefinitionError,
|
|
50
|
+
"expected cardinality: :optional, got #{kwargs[:cardinality].inspect}"
|
|
51
|
+
end
|
|
52
|
+
field(number, name, cardinality: :optional, **kwargs)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def repeated(number, name, **kwargs)
|
|
56
|
+
if kwargs[:cardinality] && kwargs[:cardinality] != :repeated
|
|
57
|
+
raise DefinitionError,
|
|
58
|
+
"expected cardinality: :repeated, got #{kwargs[:cardinality].inspect}"
|
|
59
|
+
end
|
|
60
|
+
field(number, name, cardinality: :repeated, **kwargs)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def map(number, name, **kwargs)
|
|
64
|
+
if kwargs[:type] && kwargs[:type] != :map
|
|
65
|
+
raise DefinitionError,
|
|
66
|
+
"expected type: :map, got #{kwargs[:type].inspect}"
|
|
67
|
+
end
|
|
68
|
+
repeated(number, name, type: :map, **kwargs)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def required(number, name, **kwargs)
|
|
72
|
+
if kwargs[:cardinality] && kwargs[:cardinality] != :required
|
|
73
|
+
raise DefinitionError,
|
|
74
|
+
"expected cardinality: :required, got #{kwargs[:cardinality].inspect}"
|
|
75
|
+
end
|
|
76
|
+
field(number, name, cardinality: :required, **kwargs)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def reserved_range(range)
|
|
80
|
+
raise DefinitionError, "expected Range, got #{range.inspect}" unless range.is_a? Range
|
|
81
|
+
|
|
82
|
+
reserved_ranges << range
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def decode_json(json, registry:, ignore_unknown_fields: false)
|
|
86
|
+
require "json"
|
|
87
|
+
hash = begin
|
|
88
|
+
JSON.parse(json, allow_blank: false, create_additions: false, allow_nan: false, allow_infinity: false)
|
|
89
|
+
rescue JSON::ParserError => e
|
|
90
|
+
raise DecodeError, "JSON failed to parse: #{e.message}"
|
|
91
|
+
end
|
|
92
|
+
raise DecodeError, "expected hash, got #{hash.inspect}" unless hash.is_a? Hash
|
|
93
|
+
|
|
94
|
+
decode_json_hash(hash, registry: registry, ignore_unknown_fields: ignore_unknown_fields)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def decode_json_hash(json, registry:, ignore_unknown_fields: false)
|
|
98
|
+
return UNSET if json.nil?
|
|
99
|
+
raise DecodeError, "expected hash for #{self} (#{full_name}), got #{json.inspect}" unless json.is_a? Hash
|
|
100
|
+
|
|
101
|
+
message = new
|
|
102
|
+
|
|
103
|
+
json.each do |key, value|
|
|
104
|
+
field = fields_by_json_name[key]
|
|
105
|
+
unless field
|
|
106
|
+
next if ignore_unknown_fields
|
|
107
|
+
|
|
108
|
+
raise(UnknownFieldError, "unknown field #{key.inspect} in #{full_name}")
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
if field.oneof && message.send(field.oneof) && !value.nil?
|
|
112
|
+
raise DecodeError, "multiple oneof fields set in #{full_name}: #{message.send(field.oneof)} and #{field.name}"
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
field.json_decode(value, message, ignore_unknown_fields, registry)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
message
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def decode(binary, registry:, object: new)
|
|
122
|
+
binary.binmode
|
|
123
|
+
loop do
|
|
124
|
+
header = BinaryEncoding.decode_varint(binary)
|
|
125
|
+
break if header.nil?
|
|
126
|
+
|
|
127
|
+
wire_type = header & 0b111
|
|
128
|
+
number = (header ^ wire_type) >> 3
|
|
129
|
+
|
|
130
|
+
unless number.positive?
|
|
131
|
+
raise DecodeError,
|
|
132
|
+
"unexpected field number #{number} in #{full_name || fields_by_name.inspect}"
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
field = fields_by_number[number]
|
|
136
|
+
|
|
137
|
+
if field
|
|
138
|
+
field.binary_decode(binary, object, registry, wire_type)
|
|
139
|
+
else
|
|
140
|
+
object.unknown_fields << [number, wire_type, BinaryEncoding.read_field_value(binary, wire_type)]
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
object
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def encode(message)
|
|
147
|
+
raise EncodeError, "expected #{self}, got #{message.inspect}" unless message.is_a? self
|
|
148
|
+
|
|
149
|
+
buf = fields_by_number.each_with_object("".b) do |(_number, field), outbuf|
|
|
150
|
+
next unless message.send(field.haser)
|
|
151
|
+
|
|
152
|
+
value = message.instance_variable_get(field.ivar)
|
|
153
|
+
|
|
154
|
+
field.binary_encode(value, outbuf)
|
|
155
|
+
end
|
|
156
|
+
message.unknown_fields.each_with_object(buf) do |(number, wire_type, value), outbuf|
|
|
157
|
+
BinaryEncoding.encode_varint((number << 3) | wire_type, outbuf)
|
|
158
|
+
case wire_type
|
|
159
|
+
when 0, 5
|
|
160
|
+
BinaryEncoding.encode_varint(value, outbuf)
|
|
161
|
+
when 2
|
|
162
|
+
BinaryEncoding.encode_length(value, outbuf)
|
|
163
|
+
else
|
|
164
|
+
raise EncodeError, "unknown wire_type: #{wire_type}"
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def field(number, name, type:, **kwargs)
|
|
170
|
+
field =
|
|
171
|
+
case type
|
|
172
|
+
when :message
|
|
173
|
+
Field::MessageField
|
|
174
|
+
when :enum
|
|
175
|
+
Field::EnumField
|
|
176
|
+
when :bytes
|
|
177
|
+
Field::BytesField
|
|
178
|
+
when :string
|
|
179
|
+
Field::StringField
|
|
180
|
+
when :map
|
|
181
|
+
kwargs.delete(:cardinality) if kwargs[:cardinality] == :repeated
|
|
182
|
+
Field::MapField
|
|
183
|
+
when :int64
|
|
184
|
+
Field::Int64Field
|
|
185
|
+
when :uint64
|
|
186
|
+
Field::UInt64Field
|
|
187
|
+
when :sint64
|
|
188
|
+
Field::SInt64Field
|
|
189
|
+
when :fixed64
|
|
190
|
+
Field::Fixed64Field
|
|
191
|
+
when :sfixed64
|
|
192
|
+
Field::SFixed64Field
|
|
193
|
+
when :int32
|
|
194
|
+
Field::Int32Field
|
|
195
|
+
when :uint32
|
|
196
|
+
Field::UInt32Field
|
|
197
|
+
when :sint32
|
|
198
|
+
Field::SInt32Field
|
|
199
|
+
when :fixed32
|
|
200
|
+
Field::Fixed32Field
|
|
201
|
+
when :sfixed32
|
|
202
|
+
Field::SFixed32Field
|
|
203
|
+
when :bool
|
|
204
|
+
Field::BoolField
|
|
205
|
+
when :float
|
|
206
|
+
Field::FloatField
|
|
207
|
+
when :double
|
|
208
|
+
Field::DoubleField
|
|
209
|
+
when :group
|
|
210
|
+
Field::GroupField
|
|
211
|
+
else
|
|
212
|
+
raise ArgumentError, "Unknown field type #{type.inspect}"
|
|
213
|
+
end.new(number, name, **kwargs).freeze
|
|
214
|
+
|
|
215
|
+
raise DefinitionError, "duplicate field number #{number}" if fields_by_number[number]
|
|
216
|
+
|
|
217
|
+
fields_by_number[number] = field
|
|
218
|
+
raise DefinitionError, "duplicate field name #{name}" if fields_by_name[name]
|
|
219
|
+
|
|
220
|
+
fields_by_name[name] = field
|
|
221
|
+
|
|
222
|
+
fields_by_json_name[name] = field
|
|
223
|
+
fields_by_json_name[field.json_name] = field
|
|
224
|
+
|
|
225
|
+
define_method(field.setter) do |value|
|
|
226
|
+
return instance_variable_set(field.ivar, UNSET) if value.nil? && field.optional? && field.proto3_optional?
|
|
227
|
+
|
|
228
|
+
field.validate!(value, self)
|
|
229
|
+
instance_variable_set(field.ivar, value)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
define_method(name) do
|
|
233
|
+
value = instance_variable_get(field.ivar)
|
|
234
|
+
UNSET == value ? field.default : value
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
define_method(field.haser) do
|
|
238
|
+
value = instance_variable_get(field.ivar)
|
|
239
|
+
return false if UNSET == value
|
|
240
|
+
|
|
241
|
+
return false if (!field.optional? || !field.proto3_optional?) && !field.oneof && field.default == value
|
|
242
|
+
|
|
243
|
+
if field.repeated?
|
|
244
|
+
!value.empty?
|
|
245
|
+
else
|
|
246
|
+
true
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
define_method(field.clearer) do
|
|
251
|
+
instance_variable_set(field.ivar, UNSET)
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
field.define_adder(self) if field.repeated?
|
|
255
|
+
|
|
256
|
+
return unless field.oneof
|
|
257
|
+
|
|
258
|
+
unless oneofs[field.oneof]
|
|
259
|
+
oneofs[field.oneof] = ary = []
|
|
260
|
+
define_method(field.oneof) do
|
|
261
|
+
ary.find { |f| send(f.haser) }&.name
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
oneofs[field.oneof] << field
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
module InstanceMethods
|
|
268
|
+
def ==(other)
|
|
269
|
+
self.class.full_name == other.class.full_name &&
|
|
270
|
+
self.class.fields_by_name.all? do |name, _|
|
|
271
|
+
send(name) == other.send(name)
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
alias eql? ==
|
|
275
|
+
|
|
276
|
+
attr_reader :unknown_fields
|
|
277
|
+
|
|
278
|
+
def initialize
|
|
279
|
+
super
|
|
280
|
+
self.class.fields_by_name.each_value do |field|
|
|
281
|
+
instance_variable_set(field.ivar, UNSET)
|
|
282
|
+
end
|
|
283
|
+
@unknown_fields = []
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def pretty_print(pp)
|
|
287
|
+
fields_with_values = self.class.fields_by_name.select do |_name, field|
|
|
288
|
+
send(field.haser)
|
|
289
|
+
end
|
|
290
|
+
pp.group 2, "#{self.class}.new(", ")" do
|
|
291
|
+
pp.breakable
|
|
292
|
+
fields_with_values.each_with_index do |(name, field), idx|
|
|
293
|
+
pp.nest 2 do
|
|
294
|
+
unless idx.zero?
|
|
295
|
+
pp.text ","
|
|
296
|
+
pp.breakable " "
|
|
297
|
+
end
|
|
298
|
+
pp.text "#{name}: "
|
|
299
|
+
pp.pp send(field.name)
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def hash
|
|
306
|
+
self.class.fields_by_name.map { |name, _| send(name) }.hash
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def to_text
|
|
310
|
+
fields_with_values = self.class.fields_by_name.select do |_name, field|
|
|
311
|
+
send(field.haser)
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
fields_with_values.map do |_name, field|
|
|
315
|
+
value = send(field.name)
|
|
316
|
+
field.to_text(value)
|
|
317
|
+
end.join("\n")
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
def to_proto
|
|
321
|
+
self.class.encode(self)
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def as_json(print_unknown_fields: false)
|
|
325
|
+
fields_with_values = self.class.fields_by_name.select do |_name, field|
|
|
326
|
+
send(field.haser)
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
fields_with_values.to_h do |_name, field|
|
|
330
|
+
value = send(field.name)
|
|
331
|
+
|
|
332
|
+
[field.json_name, field.json_encode(value, print_unknown_fields: print_unknown_fields)]
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def to_json(print_unknown_fields: false)
|
|
337
|
+
require "json"
|
|
338
|
+
JSON.generate(as_json(print_unknown_fields: print_unknown_fields), allow_infinity: true)
|
|
339
|
+
rescue JSON::GeneratorError => e
|
|
340
|
+
raise EncodeError, "failed to generate JSON: #{e}"
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Protobug
|
|
4
|
+
class Registry
|
|
5
|
+
def initialize(&blk)
|
|
6
|
+
@registry = {}
|
|
7
|
+
return unless blk
|
|
8
|
+
|
|
9
|
+
yield self
|
|
10
|
+
freeze
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def freeze
|
|
14
|
+
@registry.freeze
|
|
15
|
+
super
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def register(klass)
|
|
19
|
+
unless klass.is_a? Protobug::BaseDescriptor
|
|
20
|
+
raise ArgumentError,
|
|
21
|
+
"expected Protobug::BaseDescriptor, got #{klass.inspect}"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
full_name = klass.full_name
|
|
25
|
+
existing = @registry[full_name]
|
|
26
|
+
raise ArgumentError, "duplicate class #{full_name}" if existing && existing != klass
|
|
27
|
+
|
|
28
|
+
@registry[full_name] = klass
|
|
29
|
+
klass.freeze
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def fetch(...)
|
|
33
|
+
@registry.fetch(...)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
data/lib/protobug.rb
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "protobug/version"
|
|
4
|
+
|
|
5
|
+
module Protobug
|
|
6
|
+
class Error < StandardError; end
|
|
7
|
+
# Your code goes here...
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
require_relative "protobug/base_descriptor"
|
|
11
|
+
require_relative "protobug/enum"
|
|
12
|
+
require_relative "protobug/message"
|
|
13
|
+
require_relative "protobug/registry"
|
|
14
|
+
require "date"
|
metadata
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: protobug
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Samuel Giddins
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2024-04-25 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description:
|
|
14
|
+
email:
|
|
15
|
+
- segiddins@segiddins.me
|
|
16
|
+
executables: []
|
|
17
|
+
extensions: []
|
|
18
|
+
extra_rdoc_files: []
|
|
19
|
+
files:
|
|
20
|
+
- CHANGELOG.md
|
|
21
|
+
- CODE_OF_CONDUCT.md
|
|
22
|
+
- LICENSE.txt
|
|
23
|
+
- README.md
|
|
24
|
+
- lib/protobug.rb
|
|
25
|
+
- lib/protobug/base_descriptor.rb
|
|
26
|
+
- lib/protobug/binary_encoding.rb
|
|
27
|
+
- lib/protobug/enum.rb
|
|
28
|
+
- lib/protobug/errors.rb
|
|
29
|
+
- lib/protobug/field.rb
|
|
30
|
+
- lib/protobug/message.rb
|
|
31
|
+
- lib/protobug/registry.rb
|
|
32
|
+
- lib/protobug/version.rb
|
|
33
|
+
homepage: https://github.com/segiddins/protobug
|
|
34
|
+
licenses:
|
|
35
|
+
- MIT
|
|
36
|
+
metadata:
|
|
37
|
+
allowed_push_host: https://rubygems.org/
|
|
38
|
+
homepage_uri: https://github.com/segiddins/protobug
|
|
39
|
+
source_code_uri: https://github.com/segiddins/protobug
|
|
40
|
+
rubygems_mfa_required: 'true'
|
|
41
|
+
post_install_message:
|
|
42
|
+
rdoc_options: []
|
|
43
|
+
require_paths:
|
|
44
|
+
- lib
|
|
45
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
46
|
+
requirements:
|
|
47
|
+
- - ">="
|
|
48
|
+
- !ruby/object:Gem::Version
|
|
49
|
+
version: 3.0.0
|
|
50
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - ">="
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '0'
|
|
55
|
+
requirements: []
|
|
56
|
+
rubygems_version: 3.5.9
|
|
57
|
+
signing_key:
|
|
58
|
+
specification_version: 4
|
|
59
|
+
summary: An embeddable protobuf compiler & runtime for Ruby
|
|
60
|
+
test_files: []
|