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.
@@ -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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Protobug
4
+ VERSION = "0.1.0"
5
+ 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: []