protobug 0.1.0 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bb1c2e1db6e805829a6f6fb22f5d2738669059a8b68a3125dbc9166c6d562dec
4
- data.tar.gz: '09653af0a53f28125bdd2181e3c0f64130d26dfc54d5fd0ca5ae7965f884b730'
3
+ metadata.gz: 04bd54e56a0b5d9f3bdc255e2b134257f7d2c35710e718cd50555834e9df482d
4
+ data.tar.gz: e6fbdee91e29f5f6c498004ad8137c2e59fc6d743933f22d834d923245632c6b
5
5
  SHA512:
6
- metadata.gz: 771857441842b42c38e44a8fccf8091eaa8ae0188ab031817c7cfeb91944f1123f9d9ceca5c494518f427213bbc22e3dc9a5475e037b29b24daa69dc2cc77df2
7
- data.tar.gz: a319557c088eccbda2f822c56f1e7bc944140ce212d43a84dc7e4e5ea26715f531f036e825c1ee129162402021c9e26024c19ad56dfff1dad52de2970cdcf3a6
6
+ metadata.gz: a417bfdfca7b8f9429f252cc698313141c8e3b0c2de5a40f4358a91f6c0834e692376cd5e57bb205c2c978b818f5ea4cabe752c7c04e807073d2d96252525b57
7
+ data.tar.gz: 1b766a9b88e937e6615573ef0dc1842e2341b81d020c28ee47aec99aa29729fb9bae351c227d81797eae80ac41b4ac7d7145b066ff6f0237410d963569077589
data/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.2.0] - 2026-06-18
4
+
5
+ ### Added
6
+
7
+ - New compiled proto gems: `protobug_fulcio_protos`, `protobug_googleapis_annotations_protos`,
8
+ `protobug_in_toto_attestation_protos`, and `protobug_protoc_gen_openapiv2_protos`.
9
+ - Bump sigstore protobuf-specs to v0.5.1, adding `SigningConfig` v0.2 along with the
10
+ `Service`, `ServiceSelector`, and `ServiceConfiguration` messages.
11
+ - Significantly expanded test coverage.
12
+
13
+ ### Fixed
14
+
15
+ - Decode a truncated varint as an error (`EOFError`) instead of silently returning `nil`,
16
+ and reject varints whose 10th byte overflows 64 bits.
17
+ - Encode fixed-width integers (`fixed64`/`sfixed64`/`sfixed32`) in little-endian order,
18
+ correcting the binary wire format on big-endian hosts.
19
+ - Re-encode unknown fields of wire types 1 and 5 as raw bytes rather than varints.
20
+ - Raise a structured `InvalidValueError` on invalid boolean values.
21
+ - Accept decimal and exponent notation when parsing floats from JSON.
22
+
23
+ ### Changed
24
+
25
+ - Generate message field accessors via `class_eval` rather than `define_method` closures.
26
+ - `Enum#==` returns `false` for unrecognized operand types instead of raising.
27
+
3
28
  ## [0.1.0] - 2024-02-21
4
29
 
5
30
  - Initial release
@@ -10,19 +10,27 @@ module Protobug
10
10
  raise EncodeError, "expected integer, got #{value.inspect}" unless value.is_a? Integer
11
11
  raise RangeError, "expected 64-bit integer" if value > (2**64) - 1 || value < -2**63
12
12
 
13
- negative = value.negative?
14
- value = (2**64) + value if negative
15
- out = []
13
+ value += 2**64 if value < 0
16
14
  loop do
17
15
  if value.bit_length > 7
18
- out << (0b1000_0000 | (value & 0b0111_1111))
16
+ outbuf << (0b1000_0000 | (value & 0b0111_1111))
19
17
  value >>= 7
20
18
  else
21
- out << (value & 0b0111_1111)
19
+ outbuf << (value & 0b0111_1111)
22
20
  break
23
21
  end
24
22
  end
25
- out.pack("c*", buffer: outbuf)
23
+ end
24
+
25
+ if RUBY_ENGINE == "truffleruby"
26
+ # see https://github.com/oracle/truffleruby/issues/3559
27
+ def pack(ary, format, buffer:)
28
+ buffer.concat ary.pack(format)
29
+ end
30
+ else
31
+ def pack(ary, format, buffer:)
32
+ ary.pack(format, buffer: buffer)
33
+ end
26
34
  end
27
35
 
28
36
  def encode_zigzag(size, value, outbuf)
@@ -34,7 +42,7 @@ module Protobug
34
42
  end
35
43
 
36
44
  encoded = 2 * value.abs
37
- encoded -= 1 if value.negative?
45
+ encoded -= 1 if value < 0
38
46
  encode_varint encoded, outbuf
39
47
  end
40
48
 
@@ -43,20 +51,80 @@ module Protobug
43
51
  outbuf << contents
44
52
  end
45
53
 
46
- def decode_varint(binary)
47
- byte = binary.getbyte
48
- return unless byte
49
-
50
- value = 0
51
- bl = 0
52
- loop do
53
- raise DecodeError, "varint too large" if bl > 63
54
- return value if byte.nil?
55
- return value | (byte << bl) if (byte & 0b1000_0000).zero? # no continuation bit set
54
+ def varint_eof!
55
+ raise EOFError, "unexpected EOF in the middle of a varint"
56
+ end
56
57
 
57
- value |= ((byte & 0b0111_1111) << bl)
58
- bl += 7
59
- byte = binary.getbyte || raise(EOFError, "unexpected EOF")
58
+ def decode_varint(binary)
59
+ # Only the first byte may legitimately be absent (clean EOF, signaled by
60
+ # returning nil). Once a continuation bit is set, any subsequent missing
61
+ # byte is a truncated varint and must be rejected.
62
+ if (byte0 = binary.getbyte || return) < 0x80
63
+ byte0
64
+ elsif (byte1 = binary.getbyte || varint_eof!) < 0x80
65
+ (byte1 << 7) | (byte0 & 0x7F)
66
+ elsif (byte2 = binary.getbyte || varint_eof!) < 0x80
67
+ (byte2 << 14) |
68
+ ((byte1 & 0x7F) << 7) |
69
+ (byte0 & 0x7F)
70
+ elsif (byte3 = binary.getbyte || varint_eof!) < 0x80
71
+ (byte3 << 21) |
72
+ ((byte2 & 0x7F) << 14) |
73
+ ((byte1 & 0x7F) << 7) |
74
+ (byte0 & 0x7F)
75
+ elsif (byte4 = binary.getbyte || varint_eof!) < 0x80
76
+ (byte4 << 28) |
77
+ ((byte3 & 0x7F) << 21) |
78
+ ((byte2 & 0x7F) << 14) |
79
+ ((byte1 & 0x7F) << 7) |
80
+ (byte0 & 0x7F)
81
+ elsif (byte5 = binary.getbyte || varint_eof!) < 0x80
82
+ (byte5 << 35) |
83
+ ((byte4 & 0x7F) << 28) |
84
+ ((byte3 & 0x7F) << 21) |
85
+ ((byte2 & 0x7F) << 14) |
86
+ ((byte1 & 0x7F) << 7) |
87
+ (byte0 & 0x7F)
88
+ elsif (byte6 = binary.getbyte || varint_eof!) < 0x80
89
+ (byte6 << 42) |
90
+ ((byte5 & 0x7F) << 35) |
91
+ ((byte4 & 0x7F) << 28) |
92
+ ((byte3 & 0x7F) << 21) |
93
+ ((byte2 & 0x7F) << 14) |
94
+ ((byte1 & 0x7F) << 7) |
95
+ (byte0 & 0x7F)
96
+ elsif (byte7 = binary.getbyte || varint_eof!) < 0x80
97
+ (byte7 << 49) |
98
+ ((byte6 & 0x7F) << 42) |
99
+ ((byte5 & 0x7F) << 35) |
100
+ ((byte4 & 0x7F) << 28) |
101
+ ((byte3 & 0x7F) << 21) |
102
+ ((byte2 & 0x7F) << 14) |
103
+ ((byte1 & 0x7F) << 7) |
104
+ (byte0 & 0x7F)
105
+ elsif (byte8 = binary.getbyte || varint_eof!) < 0x80
106
+ (byte8 << 56) |
107
+ ((byte7 & 0x7F) << 49) |
108
+ ((byte6 & 0x7F) << 42) |
109
+ ((byte5 & 0x7F) << 35) |
110
+ ((byte4 & 0x7F) << 28) |
111
+ ((byte3 & 0x7F) << 21) |
112
+ ((byte2 & 0x7F) << 14) |
113
+ ((byte1 & 0x7F) << 7) |
114
+ (byte0 & 0x7F)
115
+ elsif (byte9 = binary.getbyte || varint_eof!) < 0x80
116
+ raise DecodeError, "varint overflow: 10th byte #{byte9} exceeds 64 bits" if byte9 > 1
117
+
118
+ (byte9 << 63) |
119
+ ((byte8 & 0x7F) << 56) |
120
+ ((byte7 & 0x7F) << 49) |
121
+ ((byte6 & 0x7F) << 42) |
122
+ ((byte5 & 0x7F) << 35) |
123
+ ((byte4 & 0x7F) << 28) |
124
+ ((byte3 & 0x7F) << 21) |
125
+ ((byte2 & 0x7F) << 14) |
126
+ ((byte1 & 0x7F) << 7) |
127
+ (byte0 & 0x7F)
60
128
  end
61
129
  end
62
130
 
data/lib/protobug/enum.rb CHANGED
@@ -123,10 +123,15 @@ module Protobug
123
123
  value == other.value
124
124
  when Integer
125
125
  value == other
126
- when String, Symbol
127
- name.to_s == other.to_s
126
+ when String
127
+ # name is already a frozen String (see #initialize), so no allocation here.
128
+ name == other
129
+ when Symbol
130
+ # Symbol#name returns the frozen interned string, avoiding the allocation
131
+ # that Symbol#to_s would incur.
132
+ name == other.name
128
133
  else
129
- raise "expected #{self.class}, got #{other.inspect}"
134
+ false
130
135
  end
131
136
  end
132
137
 
@@ -4,6 +4,8 @@ require_relative "binary_encoding"
4
4
 
5
5
  module Protobug
6
6
  class Field
7
+ PACKABLE_WIRE_TYPES = [0, 1, 5].freeze
8
+
7
9
  attr_accessor :number, :name, :json_name, :cardinality, :oneof, :ivar, :setter,
8
10
  :adder, :haser, :clearer
9
11
 
@@ -61,19 +63,55 @@ module Protobug
61
63
  @proto3_optional
62
64
  end
63
65
 
64
- def define_adder(message)
65
- field = self
66
- message.define_method(adder) do |value|
67
- field.validate!(value, self)
66
+ def method_definitions
67
+ str = +""
68
68
 
69
- existing = instance_variable_get(field.ivar)
70
- if UNSET == existing
71
- existing = field.default
72
- instance_variable_set(field.ivar, existing)
73
- end
69
+ str << "def #{setter}(value)\n"
70
+ str << " return #{ivar} = ::Protobug::UNSET if value.nil?\n" if optional? && proto3_optional?
71
+ str << " field = self.class.fields_by_name.fetch(#{name.to_s.dump})\n"
72
+ str << " field.validate!(value, self)\n"
73
+ str << " #{ivar} = value\n"
74
+ str << "end\n"
74
75
 
75
- existing << value
76
+ str << "def #{name}\n"
77
+ str << " value = #{ivar}\n"
78
+ str << " ::Protobug::UNSET == value ? self.class.fields_by_name.fetch(#{name.to_s.dump}).default : value\n"
79
+ str << "end\n"
80
+
81
+ str << "def #{haser}\n"
82
+ str << " value = #{ivar}\n"
83
+ str << " return false if ::Protobug::UNSET == value\n"
84
+ if (!optional? || !proto3_optional?) && !oneof
85
+ str << " field = self.class.fields_by_name.fetch(#{name.to_s.dump})\n"
86
+ str << " return false if field.default == value\n"
76
87
  end
88
+ str << if repeated?
89
+ " !value.empty?\n"
90
+ else
91
+ " true\n"
92
+ end
93
+ str << "end\n"
94
+
95
+ str << "def #{clearer}\n"
96
+ str << " #{ivar} = ::Protobug::UNSET\n"
97
+ str << "end\n"
98
+
99
+ adder_method_definition(str) if repeated?
100
+
101
+ str
102
+ end
103
+
104
+ def adder_method_definition(str)
105
+ str << "def #{adder}(value)\n"
106
+ str << " existing = #{ivar}\n"
107
+ str << " field = self.class.fields_by_name.fetch(#{name.to_s.dump})\n"
108
+ str << " if ::Protobug::UNSET == existing\n"
109
+ str << " existing = field.default\n"
110
+ str << " #{ivar} = existing\n"
111
+ str << " end\n"
112
+ str << " field.validate!(value, self)\n"
113
+ str << " existing << value\n"
114
+ str << "end\n"
77
115
  end
78
116
 
79
117
  def to_text(value)
@@ -108,12 +146,13 @@ module Protobug
108
146
  end
109
147
 
110
148
  def binary_decode(binary, message, registry, wire_type)
111
- if repeated? && wire_type == 2 && [0, 1, 5].include?(self.wire_type)
149
+ own_wire_type = self.wire_type
150
+ if repeated? && wire_type == 2 && PACKABLE_WIRE_TYPES.include?(own_wire_type)
112
151
  len = StringIO.new(BinaryEncoding.decode_length(binary))
113
152
  len.binmode
114
153
 
115
- message.send(adder, binary_decode_one(len, message, registry, self.wire_type)) until len.eof?
116
- elsif wire_type != self.wire_type
154
+ message.send(adder, binary_decode_one(len, message, registry, own_wire_type)) until len.eof?
155
+ elsif wire_type != own_wire_type
117
156
  raise DecodeError, "wrong wire type for #{self}: #{wire_type.inspect}"
118
157
  else
119
158
  message.send(adder || setter, binary_decode_one(binary, message, registry, wire_type))
@@ -226,7 +265,9 @@ module Protobug
226
265
  nil
227
266
  end
228
267
 
229
- def wire_type = 2
268
+ def wire_type
269
+ 2
270
+ end
230
271
  end
231
272
 
232
273
  class MapField < MessageField
@@ -250,9 +291,17 @@ module Protobug
250
291
  end
251
292
  end
252
293
 
253
- def repeated = true
254
- def default = {}
255
- def repeated? = true
294
+ def repeated
295
+ true
296
+ end
297
+
298
+ def default
299
+ {}
300
+ end
301
+
302
+ def repeated?
303
+ true
304
+ end
256
305
 
257
306
  def binary_encode(value, outbuf)
258
307
  value.each_with_object(@map_class.new) do |(k, v), entry|
@@ -295,24 +344,26 @@ module Protobug
295
344
  end
296
345
  end
297
346
 
298
- def type_lookup(_registry) = @map_class
299
-
300
- def define_adder(message)
301
- field = self
302
- message.define_method(adder) do |msg|
303
- existing = instance_variable_get(field.ivar)
304
- if UNSET == existing
305
- existing = field.default
306
- instance_variable_set(field.ivar, existing)
307
- end
347
+ def type_lookup(_registry)
348
+ @map_class
349
+ end
308
350
 
309
- existing[msg.key] = msg.value
310
- end
351
+ def adder_method_definition(str)
352
+ str << "def #{adder}(msg)\n"
353
+ str << " existing = #{ivar}\n"
354
+ str << " if ::Protobug::UNSET == existing\n"
355
+ str << " existing = {}\n"
356
+ str << " #{ivar} = existing\n"
357
+ str << " end\n"
358
+ str << " existing[msg.key] = msg.value\n"
359
+ str << "end\n"
311
360
  end
312
361
  end
313
362
 
314
363
  class BytesField < Field
315
- def self.type = :bytes
364
+ def self.type
365
+ :bytes
366
+ end
316
367
 
317
368
  def initialize(number, name, cardinality:, json_name: name, oneof: nil,
318
369
  proto3_optional: cardinality == :optional)
@@ -352,11 +403,15 @@ module Protobug
352
403
  "".b
353
404
  end
354
405
 
355
- def wire_type = 2
406
+ def wire_type
407
+ 2
408
+ end
356
409
  end
357
410
 
358
411
  class StringField < BytesField
359
- def self.type = :string
412
+ def self.type
413
+ :string
414
+ end
360
415
 
361
416
  def initialize(number, name, cardinality:, json_name: name, oneof: nil,
362
417
  proto3_optional: cardinality == :optional)
@@ -414,15 +469,12 @@ module Protobug
414
469
  when :varint
415
470
  length_mask = (2**bit_length) - 1
416
471
  negative = signed && value & (2**bit_length.pred) != 0
417
- # warn negative
418
- length_mask >> 1 if signed
419
472
  if negative
420
473
  value &= length_mask # remove sign bit
421
474
 
422
475
  # 2's complement
423
476
  value ^= length_mask
424
477
  value += 1
425
- # value &= length_mask
426
478
  -value
427
479
  else
428
480
  value & length_mask
@@ -439,7 +491,7 @@ module Protobug
439
491
  when :varint
440
492
  BinaryEncoding.encode_varint value, outbuf
441
493
  when :fixed
442
- [value].pack(binary_pack, buffer: outbuf)
494
+ BinaryEncoding.pack([value], binary_pack, buffer: outbuf)
443
495
  end
444
496
  end
445
497
 
@@ -452,8 +504,10 @@ module Protobug
452
504
  when /\A-?\d+\z/
453
505
  value = Integer(value)
454
506
  when Float
455
- value, remainder = value.divmod(1)
456
- raise DecodeError, "expected integer for #{inspect}, got #{value.inspect}" unless remainder.zero?
507
+ int, remainder = value.divmod(1)
508
+ raise DecodeError, "expected integer for #{inspect}, got #{value.inspect}" unless remainder == 0
509
+
510
+ value = int
457
511
  else
458
512
  raise DecodeError, "expected integer for #{inspect}, got #{value.inspect}"
459
513
  end
@@ -494,77 +548,199 @@ module Protobug
494
548
  # signed: true, false
495
549
  # EXCEPT: no unsigned zigzag
496
550
  class Int64Field < IntegerField
497
- def encoding = :varint
498
- def bit_length = 64
499
- def signed = true
500
- def wire_type = 0
551
+ def encoding
552
+ :varint
553
+ end
554
+
555
+ def bit_length
556
+ 64
557
+ end
558
+
559
+ def signed
560
+ true
561
+ end
562
+
563
+ def wire_type
564
+ 0
565
+ end
501
566
  end
502
567
 
503
568
  class UInt64Field < IntegerField
504
- def encoding = :varint
505
- def bit_length = 64
506
- def signed = false
507
- def wire_type = 0
569
+ def encoding
570
+ :varint
571
+ end
572
+
573
+ def bit_length
574
+ 64
575
+ end
576
+
577
+ def signed
578
+ false
579
+ end
580
+
581
+ def wire_type
582
+ 0
583
+ end
508
584
  end
509
585
 
510
586
  class SInt64Field < IntegerField
511
- def encoding = :zigzag
512
- def bit_length = 64
513
- def signed = true
514
- def wire_type = 0
587
+ def encoding
588
+ :zigzag
589
+ end
590
+
591
+ def bit_length
592
+ 64
593
+ end
594
+
595
+ def signed
596
+ true
597
+ end
598
+
599
+ def wire_type
600
+ 0
601
+ end
515
602
  end
516
603
 
517
604
  class Fixed64Field < IntegerField
518
- def encoding = :fixed
519
- def bit_length = 64
520
- def signed = false
521
- def wire_type = 1
522
- def binary_pack = "Q"
605
+ def encoding
606
+ :fixed
607
+ end
608
+
609
+ def bit_length
610
+ 64
611
+ end
612
+
613
+ def signed
614
+ false
615
+ end
616
+
617
+ def wire_type
618
+ 1
619
+ end
620
+
621
+ def binary_pack
622
+ "Q<"
623
+ end
523
624
  end
524
625
 
525
626
  class SFixed64Field < IntegerField
526
- def encoding = :fixed
527
- def bit_length = 64
528
- def signed = true
529
- def wire_type = 1
530
- def binary_pack = "q"
627
+ def encoding
628
+ :fixed
629
+ end
630
+
631
+ def bit_length
632
+ 64
633
+ end
634
+
635
+ def signed
636
+ true
637
+ end
638
+
639
+ def wire_type
640
+ 1
641
+ end
642
+
643
+ def binary_pack
644
+ "q<"
645
+ end
531
646
  end
532
647
 
533
648
  class Int32Field < IntegerField
534
- def encoding = :varint
535
- def bit_length = 32
536
- def signed = true
537
- def wire_type = 0
649
+ def encoding
650
+ :varint
651
+ end
652
+
653
+ def bit_length
654
+ 32
655
+ end
656
+
657
+ def signed
658
+ true
659
+ end
660
+
661
+ def wire_type
662
+ 0
663
+ end
538
664
  end
539
665
 
540
666
  class UInt32Field < IntegerField
541
- def encoding = :varint
542
- def bit_length = 32
543
- def signed = false
544
- def wire_type = 0
667
+ def encoding
668
+ :varint
669
+ end
670
+
671
+ def bit_length
672
+ 32
673
+ end
674
+
675
+ def signed
676
+ false
677
+ end
678
+
679
+ def wire_type
680
+ 0
681
+ end
545
682
  end
546
683
 
547
684
  class SInt32Field < IntegerField
548
- def encoding = :zigzag
549
- def bit_length = 32
550
- def signed = true
551
- def wire_type = 0
685
+ def encoding
686
+ :zigzag
687
+ end
688
+
689
+ def bit_length
690
+ 32
691
+ end
692
+
693
+ def signed
694
+ true
695
+ end
696
+
697
+ def wire_type
698
+ 0
699
+ end
552
700
  end
553
701
 
554
702
  class Fixed32Field < IntegerField
555
- def encoding = :fixed
556
- def bit_length = 32
557
- def signed = false
558
- def wire_type = 5
559
- def binary_pack = "V"
703
+ def encoding
704
+ :fixed
705
+ end
706
+
707
+ def bit_length
708
+ 32
709
+ end
710
+
711
+ def signed
712
+ false
713
+ end
714
+
715
+ def wire_type
716
+ 5
717
+ end
718
+
719
+ def binary_pack
720
+ "V"
721
+ end
560
722
  end
561
723
 
562
724
  class SFixed32Field < IntegerField
563
- def encoding = :fixed
564
- def bit_length = 32
565
- def signed = true
566
- def wire_type = 5
567
- def binary_pack = "l"
725
+ def encoding
726
+ :fixed
727
+ end
728
+
729
+ def bit_length
730
+ 32
731
+ end
732
+
733
+ def signed
734
+ true
735
+ end
736
+
737
+ def wire_type
738
+ 5
739
+ end
740
+
741
+ def binary_pack
742
+ "l<"
743
+ end
568
744
  end
569
745
 
570
746
  class BoolField < UInt64Field
@@ -592,7 +768,7 @@ module Protobug
592
768
  end
593
769
 
594
770
  def validate!(value, message)
595
- raise "expected boolean, got #{value.inspect}" unless [true, false].include?(value)
771
+ raise InvalidValueError.new(message, self, value, "expected boolean") unless [true, false].include?(value)
596
772
 
597
773
  super(value ? 1 : 0, message)
598
774
  end
@@ -669,9 +845,17 @@ module Protobug
669
845
  end
670
846
 
671
847
  class DoubleField < Field
672
- def type = :double
673
- def binary_pack = "E"
674
- def wire_type = 1
848
+ def type
849
+ :double
850
+ end
851
+
852
+ def binary_pack
853
+ "E"
854
+ end
855
+
856
+ def wire_type
857
+ 1
858
+ end
675
859
 
676
860
  def initialize(number, name, cardinality:, json_name: name, oneof: nil, packed: false,
677
861
  proto3_optional: cardinality == :optional)
@@ -680,7 +864,7 @@ module Protobug
680
864
  end
681
865
 
682
866
  def binary_encode_one(value, outbuf)
683
- [value].pack(binary_pack, buffer: outbuf)
867
+ BinaryEncoding.pack([value], binary_pack, buffer: outbuf)
684
868
  end
685
869
 
686
870
  def binary_decode_one(io, _message, _registry, wire_type)
@@ -700,7 +884,7 @@ module Protobug
700
884
  -Float::INFINITY
701
885
  when "NaN"
702
886
  Float::NAN
703
- when /\A-?\d+\z/
887
+ when /\A-?(?:\d+(?:\.\d+)?|\.\d+)(?:[eE][-+]?\d+)?\z/
704
888
  Float(value)
705
889
  when NilClass
706
890
  UNSET
@@ -731,9 +915,17 @@ module Protobug
731
915
  end
732
916
 
733
917
  class FloatField < DoubleField
734
- def type = :float
735
- def binary_pack = "e"
736
- def wire_type = 5
918
+ def type
919
+ :float
920
+ end
921
+
922
+ def binary_pack
923
+ "e"
924
+ end
925
+
926
+ def wire_type
927
+ 5
928
+ end
737
929
  end
738
930
 
739
931
  class GroupField < Field
@@ -11,6 +11,10 @@ module Protobug
11
11
  "<UNSET>"
12
12
  end
13
13
 
14
+ def UNSET.to_s
15
+ "<UNSET>"
16
+ end
17
+
14
18
  def UNSET.===(other)
15
19
  other.equal? UNSET
16
20
  end
@@ -120,14 +124,11 @@ module Protobug
120
124
 
121
125
  def decode(binary, registry:, object: new)
122
126
  binary.binmode
123
- loop do
124
- header = BinaryEncoding.decode_varint(binary)
125
- break if header.nil?
126
-
127
+ while (header = BinaryEncoding.decode_varint(binary))
127
128
  wire_type = header & 0b111
128
- number = (header ^ wire_type) >> 3
129
+ number = header >> 3
129
130
 
130
- unless number.positive?
131
+ unless number > 0
131
132
  raise DecodeError,
132
133
  "unexpected field number #{number} in #{full_name || fields_by_name.inspect}"
133
134
  end
@@ -156,8 +157,10 @@ module Protobug
156
157
  message.unknown_fields.each_with_object(buf) do |(number, wire_type, value), outbuf|
157
158
  BinaryEncoding.encode_varint((number << 3) | wire_type, outbuf)
158
159
  case wire_type
159
- when 0, 5
160
+ when 0
160
161
  BinaryEncoding.encode_varint(value, outbuf)
162
+ when 1, 5
163
+ outbuf << value
161
164
  when 2
162
165
  BinaryEncoding.encode_length(value, outbuf)
163
166
  else
@@ -167,6 +170,17 @@ module Protobug
167
170
  end
168
171
 
169
172
  def field(number, name, type:, **kwargs)
173
+ raise ArgumentError unless number.is_a? Integer
174
+
175
+ case name
176
+ when String
177
+ # OK
178
+ when Symbol
179
+ name = name.name
180
+ else
181
+ raise ArgumentError
182
+ end
183
+
170
184
  field =
171
185
  case type
172
186
  when :message
@@ -222,54 +236,26 @@ module Protobug
222
236
  fields_by_json_name[name] = field
223
237
  fields_by_json_name[field.json_name] = field
224
238
 
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?
239
+ class_eval(field.method_definitions, __FILE__, __LINE__)
255
240
 
256
241
  return unless field.oneof
257
242
 
258
- unless oneofs[field.oneof]
259
- oneofs[field.oneof] = ary = []
243
+ unless (oneof = oneofs[field.oneof])
244
+ oneofs[field.oneof] = oneof = []
260
245
  define_method(field.oneof) do
261
- ary.find { |f| send(f.haser) }&.name
246
+ oneof.find { |f| send(f.haser) }&.name
262
247
  end
263
248
  end
264
- oneofs[field.oneof] << field
249
+ oneof << field
265
250
  end
266
251
 
267
252
  module InstanceMethods
268
253
  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
254
+ return false unless other.instance_of?(self.class)
255
+
256
+ self.class.fields_by_name.all? do |name, _|
257
+ send(name) == other.send(name)
258
+ end
273
259
  end
274
260
  alias eql? ==
275
261
 
@@ -291,7 +277,7 @@ module Protobug
291
277
  pp.breakable
292
278
  fields_with_values.each_with_index do |(name, field), idx|
293
279
  pp.nest 2 do
294
- unless idx.zero?
280
+ unless idx == 0
295
281
  pp.text ","
296
282
  pp.breakable " "
297
283
  end
@@ -303,7 +289,11 @@ module Protobug
303
289
  end
304
290
 
305
291
  def hash
306
- self.class.fields_by_name.map { |name, _| send(name) }.hash
292
+ # Build a single array (map result is mutated in place via unshift) rather
293
+ # than allocating both the mapped array and a second spread literal.
294
+ values = self.class.fields_by_name.map { |name, _| send(name) }
295
+ values.unshift(self.class)
296
+ values.hash
307
297
  end
308
298
 
309
299
  def to_text
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Protobug
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/protobug.rb CHANGED
@@ -1,14 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "protobug/version"
4
-
5
3
  module Protobug
6
4
  class Error < StandardError; end
7
5
  # Your code goes here...
8
6
  end
9
7
 
8
+ require_relative "protobug/version"
10
9
  require_relative "protobug/base_descriptor"
11
10
  require_relative "protobug/enum"
12
11
  require_relative "protobug/message"
13
12
  require_relative "protobug/registry"
14
- require "date"
metadata CHANGED
@@ -1,16 +1,16 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: protobug
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Giddins
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-04-25 00:00:00.000000000 Z
11
+ date: 2026-06-19 00:00:00.000000000 Z
12
12
  dependencies: []
13
- description:
13
+ description:
14
14
  email:
15
15
  - segiddins@segiddins.me
16
16
  executables: []
@@ -38,7 +38,7 @@ metadata:
38
38
  homepage_uri: https://github.com/segiddins/protobug
39
39
  source_code_uri: https://github.com/segiddins/protobug
40
40
  rubygems_mfa_required: 'true'
41
- post_install_message:
41
+ post_install_message:
42
42
  rdoc_options: []
43
43
  require_paths:
44
44
  - lib
@@ -54,7 +54,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
54
54
  version: '0'
55
55
  requirements: []
56
56
  rubygems_version: 3.5.9
57
- signing_key:
57
+ signing_key:
58
58
  specification_version: 4
59
59
  summary: An embeddable protobuf compiler & runtime for Ruby
60
60
  test_files: []