protostar 0.1.0.beta1
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 +60 -0
- data/LICENSE.txt +21 -0
- data/README.md +647 -0
- data/THIRD_PARTY_NOTICES.md +61 -0
- data/exe/protoc-gen-protostar +7 -0
- data/exe/protostar +7 -0
- data/lib/protostar/builder/generator.rb +71 -0
- data/lib/protostar/builder/sink.rb +35 -0
- data/lib/protostar/builder.rb +965 -0
- data/lib/protostar/cli.rb +267 -0
- data/lib/protostar/codec/decode_generator.rb +199 -0
- data/lib/protostar/codec/decoder.rb +296 -0
- data/lib/protostar/codec/encode_generator.rb +224 -0
- data/lib/protostar/codec/encoder.rb +121 -0
- data/lib/protostar/codec/sizer.rb +91 -0
- data/lib/protostar/codec.rb +15 -0
- data/lib/protostar/compiler/ast.rb +193 -0
- data/lib/protostar/compiler/emitters/descriptor_set.rb +57 -0
- data/lib/protostar/compiler/emitters/google_ruby.rb +103 -0
- data/lib/protostar/compiler/emitters/grpc.rb +175 -0
- data/lib/protostar/compiler/emitters/protostar_rbs.rb +252 -0
- data/lib/protostar/compiler/emitters/rbs.rb +394 -0
- data/lib/protostar/compiler/emitters/ruby.rb +503 -0
- data/lib/protostar/compiler/emitters.rb +268 -0
- data/lib/protostar/compiler/include/LICENSE +32 -0
- data/lib/protostar/compiler/include/google/protobuf/any.proto +106 -0
- data/lib/protostar/compiler/include/google/protobuf/api.proto +229 -0
- data/lib/protostar/compiler/include/google/protobuf/compiler/plugin.proto +180 -0
- data/lib/protostar/compiler/include/google/protobuf/cpp_features.proto +87 -0
- data/lib/protostar/compiler/include/google/protobuf/descriptor.proto +1532 -0
- data/lib/protostar/compiler/include/google/protobuf/duration.proto +115 -0
- data/lib/protostar/compiler/include/google/protobuf/empty.proto +51 -0
- data/lib/protostar/compiler/include/google/protobuf/field_mask.proto +243 -0
- data/lib/protostar/compiler/include/google/protobuf/java_features.proto +132 -0
- data/lib/protostar/compiler/include/google/protobuf/source_context.proto +48 -0
- data/lib/protostar/compiler/include/google/protobuf/struct.proto +111 -0
- data/lib/protostar/compiler/include/google/protobuf/timestamp.proto +145 -0
- data/lib/protostar/compiler/include/google/protobuf/type.proto +217 -0
- data/lib/protostar/compiler/include/google/protobuf/wrappers.proto +157 -0
- data/lib/protostar/compiler/lexer.rb +659 -0
- data/lib/protostar/compiler/linker.rb +894 -0
- data/lib/protostar/compiler/option_interpreter.rb +363 -0
- data/lib/protostar/compiler/parser.rb +1696 -0
- data/lib/protostar/compiler/retention.rb +104 -0
- data/lib/protostar/compiler/run.rb +184 -0
- data/lib/protostar/compiler/source_info.rb +161 -0
- data/lib/protostar/compiler/symbol_table.rb +223 -0
- data/lib/protostar/compiler/text_format.rb +340 -0
- data/lib/protostar/compiler/validator.rb +944 -0
- data/lib/protostar/compiler.rb +304 -0
- data/lib/protostar/diagnostics/codes.rb +1307 -0
- data/lib/protostar/diagnostics/formats.rb +64 -0
- data/lib/protostar/diagnostics/renderer.rb +240 -0
- data/lib/protostar/diagnostics/source.rb +153 -0
- data/lib/protostar/diagnostics.rb +117 -0
- data/lib/protostar/dsl.rb +367 -0
- data/lib/protostar/enum.rb +70 -0
- data/lib/protostar/errors.rb +23 -0
- data/lib/protostar/google/protobuf/any_pb.rb +90 -0
- data/lib/protostar/google/protobuf/api_pb.rb +199 -0
- data/lib/protostar/google/protobuf/compiler/plugin_pb.rb +168 -0
- data/lib/protostar/google/protobuf/descriptor_pb.rb +1239 -0
- data/lib/protostar/google/protobuf/duration_pb.rb +99 -0
- data/lib/protostar/google/protobuf/empty_pb.rb +36 -0
- data/lib/protostar/google/protobuf/field_mask_pb.rb +228 -0
- data/lib/protostar/google/protobuf/source_context_pb.rb +33 -0
- data/lib/protostar/google/protobuf/struct_pb.rb +91 -0
- data/lib/protostar/google/protobuf/timestamp_pb.rb +129 -0
- data/lib/protostar/google/protobuf/type_pb.rb +202 -0
- data/lib/protostar/google/protobuf/wrappers_pb.rb +125 -0
- data/lib/protostar/google_protobuf_shim.rb +24 -0
- data/lib/protostar/json/encoder.rb +340 -0
- data/lib/protostar/json/errors.rb +34 -0
- data/lib/protostar/json/parser.rb +437 -0
- data/lib/protostar/json/path.rb +39 -0
- data/lib/protostar/json.rb +95 -0
- data/lib/protostar/lint/command.rb +197 -0
- data/lib/protostar/lint/config.rb +98 -0
- data/lib/protostar/lint/context.rb +126 -0
- data/lib/protostar/lint/directives.rb +56 -0
- data/lib/protostar/lint/fixer.rb +165 -0
- data/lib/protostar/lint/naming.rb +54 -0
- data/lib/protostar/lint/rule.rb +120 -0
- data/lib/protostar/lint/rules/comment_enum.rb +31 -0
- data/lib/protostar/lint/rules/comment_enum_value.rb +32 -0
- data/lib/protostar/lint/rules/comment_field.rb +33 -0
- data/lib/protostar/lint/rules/comment_message.rb +31 -0
- data/lib/protostar/lint/rules/comment_rpc.rb +31 -0
- data/lib/protostar/lint/rules/comment_service.rb +31 -0
- data/lib/protostar/lint/rules/edition_recommended.rb +31 -0
- data/lib/protostar/lint/rules/enum_first_value_zero.rb +34 -0
- data/lib/protostar/lint/rules/enum_pascal_case.rb +25 -0
- data/lib/protostar/lint/rules/enum_value_prefix.rb +38 -0
- data/lib/protostar/lint/rules/enum_value_upper_snake_case.rb +28 -0
- data/lib/protostar/lint/rules/enum_zero_value_suffix.rb +41 -0
- data/lib/protostar/lint/rules/field_lower_snake_case.rb +34 -0
- data/lib/protostar/lint/rules/field_not_required.rb +39 -0
- data/lib/protostar/lint/rules/field_number_reuse_risk.rb +45 -0
- data/lib/protostar/lint/rules/field_numbers_hot_range.rb +44 -0
- data/lib/protostar/lint/rules/file_lower_snake_case.rb +27 -0
- data/lib/protostar/lint/rules/import_no_public.rb +29 -0
- data/lib/protostar/lint/rules/import_no_weak.rb +28 -0
- data/lib/protostar/lint/rules/import_used.rb +33 -0
- data/lib/protostar/lint/rules/message_pascal_case.rb +27 -0
- data/lib/protostar/lint/rules/no_deprecated_features.rb +50 -0
- data/lib/protostar/lint/rules/no_groups.rb +34 -0
- data/lib/protostar/lint/rules/oneof_lower_snake_case.rb +27 -0
- data/lib/protostar/lint/rules/package_defined.rb +28 -0
- data/lib/protostar/lint/rules/package_directory_match.rb +33 -0
- data/lib/protostar/lint/rules/package_lower_snake_case.rb +28 -0
- data/lib/protostar/lint/rules/package_same_directory.rb +39 -0
- data/lib/protostar/lint/rules/package_version_suffix.rb +31 -0
- data/lib/protostar/lint/rules/reserved_names_with_numbers.rb +44 -0
- data/lib/protostar/lint/rules/rpc_pascal_case.rb +25 -0
- data/lib/protostar/lint/rules/rpc_request_response_unique.rb +40 -0
- data/lib/protostar/lint/rules/rpc_request_standard_name.rb +32 -0
- data/lib/protostar/lint/rules/rpc_response_standard_name.rb +31 -0
- data/lib/protostar/lint/rules/service_pascal_case.rb +25 -0
- data/lib/protostar/lint/rules/service_suffix.rb +28 -0
- data/lib/protostar/lint/rules/syntax_specified.rb +35 -0
- data/lib/protostar/lint/runner.rb +135 -0
- data/lib/protostar/lint.rb +122 -0
- data/lib/protostar/message/accessors.rb +86 -0
- data/lib/protostar/message/coercion.rb +171 -0
- data/lib/protostar/message/field_info.rb +145 -0
- data/lib/protostar/message/reflection.rb +149 -0
- data/lib/protostar/message.rb +572 -0
- data/lib/protostar/plugin.rb +94 -0
- data/lib/protostar/schema/default_value.rb +113 -0
- data/lib/protostar/schema/descriptor.rb +41 -0
- data/lib/protostar/schema/draft/copy.rb +88 -0
- data/lib/protostar/schema/draft/resolution.rb +177 -0
- data/lib/protostar/schema/draft.rb +779 -0
- data/lib/protostar/schema/enum.rb +106 -0
- data/lib/protostar/schema/extension_range.rb +37 -0
- data/lib/protostar/schema/features.rb +153 -0
- data/lib/protostar/schema/field.rb +201 -0
- data/lib/protostar/schema/file.rb +217 -0
- data/lib/protostar/schema/freezer.rb +61 -0
- data/lib/protostar/schema/message.rb +104 -0
- data/lib/protostar/schema/oneof.rb +38 -0
- data/lib/protostar/schema/options.rb +367 -0
- data/lib/protostar/schema/pool.rb +179 -0
- data/lib/protostar/schema/proto.rb +576 -0
- data/lib/protostar/schema/resolver.rb +118 -0
- data/lib/protostar/schema/service.rb +89 -0
- data/lib/protostar/schema/source_info.rb +53 -0
- data/lib/protostar/schema.rb +65 -0
- data/lib/protostar/short_names/any.rb +7 -0
- data/lib/protostar/short_names/duration.rb +7 -0
- data/lib/protostar/short_names/empty.rb +7 -0
- data/lib/protostar/short_names/field_mask.rb +7 -0
- data/lib/protostar/short_names/struct.rb +9 -0
- data/lib/protostar/short_names/timestamp.rb +7 -0
- data/lib/protostar/short_names/wrappers.rb +15 -0
- data/lib/protostar/text/extensions.rb +114 -0
- data/lib/protostar/text/parser.rb +439 -0
- data/lib/protostar/text/printer.rb +304 -0
- data/lib/protostar/text/tokenizer.rb +116 -0
- data/lib/protostar/text.rb +67 -0
- data/lib/protostar/types.rb +152 -0
- data/lib/protostar/unknown_field.rb +58 -0
- data/lib/protostar/version.rb +5 -0
- data/lib/protostar/view/generator.rb +298 -0
- data/lib/protostar/view/map.rb +194 -0
- data/lib/protostar/view/repeated.rb +143 -0
- data/lib/protostar/view/scan.rb +183 -0
- data/lib/protostar/view.rb +283 -0
- data/lib/protostar/well_known/any.rb +52 -0
- data/lib/protostar/well_known/duration.rb +53 -0
- data/lib/protostar/well_known/list_value.rb +63 -0
- data/lib/protostar/well_known/struct.rb +55 -0
- data/lib/protostar/well_known/timestamp.rb +64 -0
- data/lib/protostar/well_known/value.rb +60 -0
- data/lib/protostar/well_known.rb +38 -0
- data/lib/protostar/wire/io_source.rb +40 -0
- data/lib/protostar/wire/reader.rb +254 -0
- data/lib/protostar/wire/writer.rb +201 -0
- data/lib/protostar/wire.rb +96 -0
- data/lib/protostar.rb +95 -0
- data/shims/google-protobuf/lib/google/protobuf/any_pb.rb +17 -0
- data/shims/google-protobuf/lib/google/protobuf/api_pb.rb +22 -0
- data/shims/google-protobuf/lib/google/protobuf/compiler/plugin_pb.rb +25 -0
- data/shims/google-protobuf/lib/google/protobuf/descriptor_pb.rb +72 -0
- data/shims/google-protobuf/lib/google/protobuf/descriptor_pool.rb +164 -0
- data/shims/google-protobuf/lib/google/protobuf/descriptors.rb +553 -0
- data/shims/google-protobuf/lib/google/protobuf/duration_pb.rb +17 -0
- data/shims/google-protobuf/lib/google/protobuf/empty_pb.rb +17 -0
- data/shims/google-protobuf/lib/google/protobuf/field_mask_pb.rb +17 -0
- data/shims/google-protobuf/lib/google/protobuf/internal/convert.rb +180 -0
- data/shims/google-protobuf/lib/google/protobuf/internal/extensions.rb +154 -0
- data/shims/google-protobuf/lib/google/protobuf/internal/json.rb +104 -0
- data/shims/google-protobuf/lib/google/protobuf/internal/well_known.rb +140 -0
- data/shims/google-protobuf/lib/google/protobuf/map.rb +216 -0
- data/shims/google-protobuf/lib/google/protobuf/message.rb +544 -0
- data/shims/google-protobuf/lib/google/protobuf/message_exts.rb +41 -0
- data/shims/google-protobuf/lib/google/protobuf/plugin_pb.rb +4 -0
- data/shims/google-protobuf/lib/google/protobuf/repeated_field.rb +366 -0
- data/shims/google-protobuf/lib/google/protobuf/source_context_pb.rb +17 -0
- data/shims/google-protobuf/lib/google/protobuf/struct_pb.rb +20 -0
- data/shims/google-protobuf/lib/google/protobuf/timestamp_pb.rb +17 -0
- data/shims/google-protobuf/lib/google/protobuf/type_pb.rb +27 -0
- data/shims/google-protobuf/lib/google/protobuf/well_known_types.rb +37 -0
- data/shims/google-protobuf/lib/google/protobuf/wrappers_pb.rb +25 -0
- data/shims/google-protobuf/lib/google/protobuf.rb +107 -0
- data/shims/google-protobuf/lib/google/protobuf_ffi.rb +5 -0
- data/shims/google-protobuf/lib/google/protobuf_native.rb +5 -0
- data/sig/protostar/google/protobuf/any_pb.rbs +77 -0
- data/sig/protostar/google/protobuf/api_pb.rbs +242 -0
- data/sig/protostar/google/protobuf/compiler/plugin_pb.rbs +264 -0
- data/sig/protostar/google/protobuf/descriptor_pb.rbs +2220 -0
- data/sig/protostar/google/protobuf/duration_pb.rbs +82 -0
- data/sig/protostar/google/protobuf/empty_pb.rbs +18 -0
- data/sig/protostar/google/protobuf/field_mask_pb.rbs +170 -0
- data/sig/protostar/google/protobuf/source_context_pb.rbs +24 -0
- data/sig/protostar/google/protobuf/struct_pb.rbs +135 -0
- data/sig/protostar/google/protobuf/timestamp_pb.rbs +102 -0
- data/sig/protostar/google/protobuf/type_pb.rbs +304 -0
- data/sig/protostar/google/protobuf/wrappers_pb.rbs +169 -0
- data/sig/protostar/lint.rbs +216 -0
- data/sig/protostar.rbs +326 -0
- metadata +366 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Protostar
|
|
4
|
+
module WellKnown
|
|
5
|
+
# Mixed into google.protobuf.Timestamp
|
|
6
|
+
module Timestamp
|
|
7
|
+
def self.included(base)
|
|
8
|
+
base.extend(ClassMethods)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# @param value [Object]
|
|
12
|
+
# @return [Array(Integer, Integer), nil] seconds and nanos of a Time, a
|
|
13
|
+
# DateTime or a Date; nil for anything else
|
|
14
|
+
def self.parts(value)
|
|
15
|
+
time = if value.is_a?(::Time) then value
|
|
16
|
+
elsif defined?(::Date) && value.is_a?(::Date) then value.to_time
|
|
17
|
+
else return nil
|
|
18
|
+
end
|
|
19
|
+
[time.to_i, time.nsec]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
module ClassMethods
|
|
23
|
+
# @param time [Time]
|
|
24
|
+
# @return [Message] a new Timestamp at the same instant, to the nanosecond
|
|
25
|
+
def from_time(time)
|
|
26
|
+
new.from_time(time)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# What a Timestamp field makes of a value that is neither a Timestamp
|
|
30
|
+
# nor a Hash: a Time, a DateTime or a Date becomes a Timestamp
|
|
31
|
+
# @return [Message, nil]
|
|
32
|
+
# @api private
|
|
33
|
+
def __coerce(value)
|
|
34
|
+
seconds, nanos = Timestamp.parts(value)
|
|
35
|
+
new(seconds: seconds, nanos: nanos) if seconds
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# @param time [Time]
|
|
40
|
+
# @return [self]
|
|
41
|
+
def from_time(time)
|
|
42
|
+
self.seconds = time.to_i
|
|
43
|
+
self.nanos = time.nsec
|
|
44
|
+
self
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# @return [Time] in the local zone, as `Time.at` gives it
|
|
48
|
+
def to_time
|
|
49
|
+
::Time.at(seconds, nanos, :nanosecond)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# @return [Integer] seconds since the epoch
|
|
53
|
+
def to_i
|
|
54
|
+
seconds
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# @return [Rational] seconds since the epoch, exact; google-protobuf
|
|
58
|
+
# returns a Rational here too
|
|
59
|
+
def to_f
|
|
60
|
+
seconds + nanos.quo(1_000_000_000)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Protostar
|
|
4
|
+
module WellKnown
|
|
5
|
+
# Mixed into google.protobuf.Value
|
|
6
|
+
module Value
|
|
7
|
+
def self.included(base)
|
|
8
|
+
base.extend(ClassMethods)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
module ClassMethods
|
|
12
|
+
# @param value [Object] nil, Numeric, String, true, false, Hash,
|
|
13
|
+
# Array, a Struct or a ListValue
|
|
14
|
+
# @return [Message] a new Value
|
|
15
|
+
# @raise [UnexpectedStructType]
|
|
16
|
+
def from_ruby(value)
|
|
17
|
+
new.from_ruby(value)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# @param recursive [Boolean] whether to convert a Struct or ListValue to
|
|
22
|
+
# a Hash or Array as well
|
|
23
|
+
# @return [Object]
|
|
24
|
+
# @raise [UnexpectedStructType] when no kind is set
|
|
25
|
+
def to_ruby(recursive = false)
|
|
26
|
+
case which_kind
|
|
27
|
+
when :struct_value then recursive ? struct_value.to_h : struct_value
|
|
28
|
+
when :list_value then recursive ? list_value.to_a : list_value
|
|
29
|
+
when :null_value then nil
|
|
30
|
+
when :number_value then number_value
|
|
31
|
+
when :string_value then string_value
|
|
32
|
+
when :bool_value then bool_value
|
|
33
|
+
else raise UnexpectedStructType
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Sets the kind matching a Ruby value
|
|
38
|
+
# @return [self]
|
|
39
|
+
# @raise [UnexpectedStructType]
|
|
40
|
+
def from_ruby(value)
|
|
41
|
+
fields = self.class.__by_name
|
|
42
|
+
struct_class = fields.fetch("struct_value").message_class
|
|
43
|
+
list_class = fields.fetch("list_value").message_class
|
|
44
|
+
case value
|
|
45
|
+
when NilClass then self.null_value = :NULL_VALUE
|
|
46
|
+
when Numeric then self.number_value = value.to_f
|
|
47
|
+
when String then self.string_value = value
|
|
48
|
+
when TrueClass then self.bool_value = true
|
|
49
|
+
when FalseClass then self.bool_value = false
|
|
50
|
+
when struct_class then self.struct_value = value
|
|
51
|
+
when Hash then self.struct_value = struct_class.from_hash(value)
|
|
52
|
+
when list_class then self.list_value = value
|
|
53
|
+
when Array then self.list_value = list_class.from_a(value)
|
|
54
|
+
else raise UnexpectedStructType
|
|
55
|
+
end
|
|
56
|
+
self
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Protostar
|
|
4
|
+
# Ruby helpers for the well-known types, with the names google-protobuf's
|
|
5
|
+
# `well_known_types.rb` gives them. {Types.define} includes the matching
|
|
6
|
+
# mixin into a class whose full name is in {MIXINS}, so the helpers are
|
|
7
|
+
# there however the class came to be defined.
|
|
8
|
+
#
|
|
9
|
+
# any = Google::Protobuf::Any.pack(person)
|
|
10
|
+
# any.unpack(Person) # => person
|
|
11
|
+
# Google::Protobuf::Timestamp.from_time(Time.now).to_time
|
|
12
|
+
# Google::Protobuf::Struct.from_hash("a" => [1, nil]).to_h
|
|
13
|
+
module WellKnown
|
|
14
|
+
# Raised by Struct, Value and ListValue for a Ruby value they cannot hold
|
|
15
|
+
class UnexpectedStructType < Protostar::Error; end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
require_relative "well_known/any"
|
|
20
|
+
require_relative "well_known/timestamp"
|
|
21
|
+
require_relative "well_known/duration"
|
|
22
|
+
require_relative "well_known/struct"
|
|
23
|
+
require_relative "well_known/value"
|
|
24
|
+
require_relative "well_known/list_value"
|
|
25
|
+
|
|
26
|
+
module Protostar
|
|
27
|
+
module WellKnown
|
|
28
|
+
# Full name to the mixin {Types.define} includes into that class
|
|
29
|
+
MIXINS = {
|
|
30
|
+
"google.protobuf.Any" => Any,
|
|
31
|
+
"google.protobuf.Timestamp" => Timestamp,
|
|
32
|
+
"google.protobuf.Duration" => Duration,
|
|
33
|
+
"google.protobuf.Struct" => Struct,
|
|
34
|
+
"google.protobuf.Value" => Value,
|
|
35
|
+
"google.protobuf.ListValue" => ListValue
|
|
36
|
+
}.freeze
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Protostar
|
|
4
|
+
module Wire
|
|
5
|
+
# Presents an IO::Buffer with the String methods {Reader} uses, so that
|
|
6
|
+
# a buffer is read in place: numbers with `get_value`, and only string
|
|
7
|
+
# and bytes values copied out, with `get_string`.
|
|
8
|
+
class IOSource
|
|
9
|
+
# unpack1 directives the codec uses, as IO::Buffer data types
|
|
10
|
+
TYPES = {"V" => :u32, "Q<" => :u64, "l<" => :s32, "q<" => :s64, "e" => :f32, "E" => :f64}.freeze
|
|
11
|
+
|
|
12
|
+
# @return [IO::Buffer]
|
|
13
|
+
attr_reader :buffer
|
|
14
|
+
# @return [Integer]
|
|
15
|
+
attr_reader :bytesize
|
|
16
|
+
|
|
17
|
+
# @param buffer [IO::Buffer] must not change while it is read
|
|
18
|
+
def initialize(buffer)
|
|
19
|
+
@buffer = buffer
|
|
20
|
+
@bytesize = buffer.size
|
|
21
|
+
freeze
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @return [Integer]
|
|
25
|
+
def getbyte(offset)
|
|
26
|
+
@buffer.get_value(:U8, offset)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @return [String] a new binary String
|
|
30
|
+
def byteslice(offset, length)
|
|
31
|
+
@buffer.get_string(offset, length)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @param format [String] one of the directives in {TYPES}
|
|
35
|
+
def unpack1(format, offset:)
|
|
36
|
+
@buffer.get_value(TYPES.fetch(format), offset)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Protostar
|
|
4
|
+
module Wire
|
|
5
|
+
# Reads wire-format values from a String without copying it or allocating
|
|
6
|
+
# per byte. The String is never mutated, and its encoding does not matter:
|
|
7
|
+
# every read works on bytes. An IO::Buffer is read in place through an
|
|
8
|
+
# {IOSource}, and so is any source answering `getbyte`, `byteslice`,
|
|
9
|
+
# `unpack1(format, offset:)` and `bytesize` the way String does.
|
|
10
|
+
#
|
|
11
|
+
# A reader has a position and a limit. Reads never go past the limit, so a
|
|
12
|
+
# nested message is read by narrowing the limit to its end and restoring it
|
|
13
|
+
# afterwards ({#push_limit}, {#pop_limit}).
|
|
14
|
+
#
|
|
15
|
+
# r = Protostar::Wire::Reader.new(bytes)
|
|
16
|
+
# until r.eof?
|
|
17
|
+
# number, wire_type = r.read_tag
|
|
18
|
+
# ...
|
|
19
|
+
# end
|
|
20
|
+
class Reader
|
|
21
|
+
# What a source other than a String must answer
|
|
22
|
+
SOURCE_METHODS = %i[getbyte byteslice unpack1 bytesize].freeze
|
|
23
|
+
|
|
24
|
+
# @return [String, IOSource, Object] the underlying bytes
|
|
25
|
+
attr_reader :buffer
|
|
26
|
+
# @return [Integer] the current byte offset
|
|
27
|
+
attr_accessor :pos
|
|
28
|
+
# @return [Integer] the offset reads must not pass
|
|
29
|
+
attr_reader :limit
|
|
30
|
+
|
|
31
|
+
# @param buffer [String, IO::Buffer, #getbyte] an IO::Buffer is wrapped in an {IOSource}, not copied
|
|
32
|
+
# @param pos [Integer] where to start
|
|
33
|
+
# @param limit [Integer, nil] where to stop, the end of the buffer by default
|
|
34
|
+
def initialize(buffer, pos = 0, limit = nil)
|
|
35
|
+
buffer = IOSource.new(buffer) if defined?(IO::Buffer) && buffer.is_a?(IO::Buffer)
|
|
36
|
+
unless buffer.is_a?(String) || SOURCE_METHODS.all? { |m| buffer.respond_to?(m) }
|
|
37
|
+
raise ::TypeError, "expected a String, an IO::Buffer or a source answering #{SOURCE_METHODS.join(", ")}, got #{buffer.class}"
|
|
38
|
+
end
|
|
39
|
+
@buffer = buffer
|
|
40
|
+
@pos = pos
|
|
41
|
+
@limit = limit || buffer.bytesize
|
|
42
|
+
raise ArgumentError, "limit #{@limit} is past the end of the buffer" if @limit > buffer.bytesize
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# @return [Boolean] whether the position has reached the limit
|
|
46
|
+
def eof?
|
|
47
|
+
@pos >= @limit
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Narrows the limit to `length` bytes from the current position
|
|
51
|
+
# @return [Integer] the previous limit, to hand to {#pop_limit}
|
|
52
|
+
# @raise [DecodeError] when that runs past the current limit
|
|
53
|
+
def push_limit(length)
|
|
54
|
+
new_limit = @pos + length
|
|
55
|
+
raise DecodeError.new("length #{length} runs past the end of the input", offset: @pos) if new_limit > @limit
|
|
56
|
+
old = @limit
|
|
57
|
+
@limit = new_limit
|
|
58
|
+
old
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Restores a limit returned by {#push_limit}
|
|
62
|
+
def pop_limit(old_limit)
|
|
63
|
+
@limit = old_limit
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# @return [Integer] an unsigned varint of up to 64 bits. Longer encodings
|
|
67
|
+
# are truncated to 64 bits, as protoc does.
|
|
68
|
+
# @raise [DecodeError] when truncated or longer than ten bytes
|
|
69
|
+
def read_varint
|
|
70
|
+
pos = @pos
|
|
71
|
+
raise DecodeError.new("truncated varint", offset: pos) if pos >= @limit
|
|
72
|
+
byte = @buffer.getbyte(pos)
|
|
73
|
+
if byte < 0x80
|
|
74
|
+
@pos = pos + 1
|
|
75
|
+
return byte
|
|
76
|
+
end
|
|
77
|
+
read_varint_slow(pos)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# @return [Integer] a tag's varint, unchecked
|
|
81
|
+
# @raise [DecodeError] when truncated or longer than five bytes, which
|
|
82
|
+
# protoc refuses even when the value would fit
|
|
83
|
+
def read_tag_varint
|
|
84
|
+
at = @pos
|
|
85
|
+
tag = read_varint
|
|
86
|
+
raise DecodeError.new("tag longer than 5 bytes", offset: at) if @pos - at > 5
|
|
87
|
+
tag
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# @return [Array(Integer, Integer)] field number and wire type
|
|
91
|
+
# @raise [DecodeError] for field number 0, an out of range tag or an invalid wire type
|
|
92
|
+
def read_tag
|
|
93
|
+
at = @pos
|
|
94
|
+
tag = read_tag_varint
|
|
95
|
+
Reader.check_tag(tag, at)
|
|
96
|
+
[tag >> 3, tag & 7]
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# @return [Integer] a signed 32-bit integer from a varint (int32 and enum)
|
|
100
|
+
def read_int32
|
|
101
|
+
v = read_varint & MASK32
|
|
102
|
+
(v >= 0x8000_0000) ? v - 0x1_0000_0000 : v
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# @return [Integer] a signed 64-bit integer from a varint
|
|
106
|
+
def read_int64
|
|
107
|
+
v = read_varint
|
|
108
|
+
(v >= 0x8000_0000_0000_0000) ? v - 0x1_0000_0000_0000_0000 : v
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# @return [Integer] an unsigned 32-bit integer from a varint
|
|
112
|
+
def read_uint32
|
|
113
|
+
read_varint & MASK32
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# @return [Integer] a zigzag-decoded signed 32-bit integer (sint32)
|
|
117
|
+
def read_sint32
|
|
118
|
+
v = read_varint & MASK32
|
|
119
|
+
(v >> 1) ^ -(v & 1)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# @return [Integer] a zigzag-decoded signed 64-bit integer (sint64)
|
|
123
|
+
def read_sint64
|
|
124
|
+
v = read_varint
|
|
125
|
+
(v >> 1) ^ -(v & 1)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# @return [Integer] an unsigned 32-bit little-endian integer
|
|
129
|
+
def read_fixed32
|
|
130
|
+
read_fixed(4, "V")
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# @return [Integer] an unsigned 64-bit little-endian integer
|
|
134
|
+
def read_fixed64
|
|
135
|
+
read_fixed(8, "Q<")
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# @return [Integer]
|
|
139
|
+
def read_sfixed32
|
|
140
|
+
read_fixed(4, "l<")
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# @return [Integer]
|
|
144
|
+
def read_sfixed64
|
|
145
|
+
read_fixed(8, "q<")
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# @return [Float]
|
|
149
|
+
def read_float
|
|
150
|
+
read_fixed(4, "e")
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# @return [Float]
|
|
154
|
+
def read_double
|
|
155
|
+
read_fixed(8, "E")
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Reads a varint length and that many bytes
|
|
159
|
+
# @return [String] a new binary String
|
|
160
|
+
def read_bytes
|
|
161
|
+
len = read_varint
|
|
162
|
+
read_raw(len)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# @return [String] the next `length` bytes as a new binary String
|
|
166
|
+
def read_raw(length)
|
|
167
|
+
pos = @pos
|
|
168
|
+
raise DecodeError.new("length #{length} runs past the end of the input", offset: pos) if pos + length > @limit
|
|
169
|
+
@pos = pos + length
|
|
170
|
+
s = @buffer.byteslice(pos, length)
|
|
171
|
+
s.force_encoding(BINARY)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Skips the value of a field whose tag was just read
|
|
175
|
+
# @param number [Integer] the field number, needed to match the end of a group
|
|
176
|
+
# @param wire_type [Integer]
|
|
177
|
+
# @param depth [Integer] groups left before {DecodeError} for nesting too deep
|
|
178
|
+
def skip_field(number, wire_type, depth = 100)
|
|
179
|
+
case wire_type
|
|
180
|
+
when VARINT then read_varint
|
|
181
|
+
when FIXED64 then skip(8)
|
|
182
|
+
when LENGTH_DELIMITED then skip(read_varint)
|
|
183
|
+
when FIXED32 then skip(4)
|
|
184
|
+
when START_GROUP then skip_group(number, depth)
|
|
185
|
+
when END_GROUP then raise DecodeError.new("unmatched end of group #{number}", offset: @pos)
|
|
186
|
+
else raise DecodeError.new("invalid wire type #{wire_type}", offset: @pos)
|
|
187
|
+
end
|
|
188
|
+
nil
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# @raise [DecodeError] unless `length` bytes remain before the limit
|
|
192
|
+
def skip(length)
|
|
193
|
+
raise DecodeError.new("length #{length} runs past the end of the input", offset: @pos) if @pos + length > @limit
|
|
194
|
+
@pos += length
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Skips the contents of a group up to and including its end tag
|
|
198
|
+
def skip_group(number, depth = 100)
|
|
199
|
+
raise DecodeError.new("groups nested too deep", offset: @pos) if depth <= 0
|
|
200
|
+
loop do
|
|
201
|
+
raise DecodeError.new("missing end of group #{number}", offset: @pos) if @pos >= @limit
|
|
202
|
+
at = @pos
|
|
203
|
+
tag = read_tag_varint
|
|
204
|
+
Reader.check_tag(tag, at)
|
|
205
|
+
n = tag >> 3
|
|
206
|
+
wt = tag & 7
|
|
207
|
+
if wt == END_GROUP
|
|
208
|
+
return if n == number
|
|
209
|
+
raise DecodeError.new("end of group #{n} does not match group #{number}", offset: at)
|
|
210
|
+
end
|
|
211
|
+
skip_field(n, wt, depth - 1)
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# @api private
|
|
216
|
+
def self.check_tag(tag, at)
|
|
217
|
+
raise DecodeError.new("tag out of range", offset: at) if tag > MASK32
|
|
218
|
+
raise DecodeError.new("field number 0", offset: at) if tag < 8
|
|
219
|
+
wt = tag & 7
|
|
220
|
+
raise DecodeError.new("invalid wire type #{wt}", offset: at) if wt > 5
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
private
|
|
224
|
+
|
|
225
|
+
def read_fixed(size, format)
|
|
226
|
+
pos = @pos
|
|
227
|
+
raise DecodeError.new("truncated #{size * 8}-bit value", offset: pos) if pos + size > @limit
|
|
228
|
+
@pos = pos + size
|
|
229
|
+
@buffer.unpack1(format, offset: pos)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def read_varint_slow(start)
|
|
233
|
+
buf = @buffer
|
|
234
|
+
limit = @limit
|
|
235
|
+
pos = start
|
|
236
|
+
result = 0
|
|
237
|
+
shift = 0
|
|
238
|
+
while shift < 70
|
|
239
|
+
raise DecodeError.new("truncated varint", offset: start) if pos >= limit
|
|
240
|
+
byte = buf.getbyte(pos)
|
|
241
|
+
pos += 1
|
|
242
|
+
result |= (byte & 0x7F) << shift
|
|
243
|
+
if byte < 0x80
|
|
244
|
+
@pos = pos
|
|
245
|
+
# masking with a Bignum allocates, and only a tenth byte can overflow 64 bits
|
|
246
|
+
return (shift == 63) ? result & MASK64 : result
|
|
247
|
+
end
|
|
248
|
+
shift += 7
|
|
249
|
+
end
|
|
250
|
+
raise DecodeError.new("varint longer than 10 bytes", offset: start)
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
end
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Protostar
|
|
4
|
+
module Wire
|
|
5
|
+
# Writes wire-format values into a binary String, or through a small
|
|
6
|
+
# buffer into a sink: an IO, or anything that takes bytes.
|
|
7
|
+
#
|
|
8
|
+
# w = Protostar::Wire::Writer.new
|
|
9
|
+
# w.write_tag(1, Protostar::Wire::VARINT).write_varint(150)
|
|
10
|
+
# w.string # => "\x08\x96\x01"
|
|
11
|
+
#
|
|
12
|
+
# Protostar::Wire::Writer.new(io).write_bytes("hi").flush
|
|
13
|
+
class Writer
|
|
14
|
+
# Bytes buffered before a sink is written to, by default
|
|
15
|
+
FLUSH_AT = 64 * 1024
|
|
16
|
+
|
|
17
|
+
# @return [String] the bytes written so far (for a sink, those not yet flushed)
|
|
18
|
+
attr_reader :string
|
|
19
|
+
# @return [Object, nil] the sink, or nil for a String target
|
|
20
|
+
attr_reader :io
|
|
21
|
+
# @return [Integer] bytes buffered before the sink is written to
|
|
22
|
+
attr_reader :flush_at
|
|
23
|
+
|
|
24
|
+
# @param target [String, #write, #<<, #call, nil] a String to append to
|
|
25
|
+
# (it is switched to binary encoding), a sink, or nil for a new String.
|
|
26
|
+
# A sink is anything answering `write` (an IO, a socket, a Rack or
|
|
27
|
+
# ActionController::Live stream), `<<` (an Array, an Enumerator::Yielder)
|
|
28
|
+
# or `call` (a Proc). An IO or StringIO is given the buffer itself and
|
|
29
|
+
# anything else a copy, since a sink may keep what it is given.
|
|
30
|
+
# @param flush_at [Integer] bytes to collect before writing to the sink;
|
|
31
|
+
# 0 writes on every {#flush_if_full}
|
|
32
|
+
def initialize(target = nil, flush_at: FLUSH_AT)
|
|
33
|
+
@flush_at = flush_at
|
|
34
|
+
if target.nil?
|
|
35
|
+
@string = String.new(encoding: BINARY)
|
|
36
|
+
@io = nil
|
|
37
|
+
elsif target.is_a?(String)
|
|
38
|
+
@string = target.force_encoding(BINARY)
|
|
39
|
+
@io = nil
|
|
40
|
+
else
|
|
41
|
+
@string = String.new(capacity: (flush_at > 0) ? flush_at : 256, encoding: BINARY)
|
|
42
|
+
@io = target
|
|
43
|
+
@push = Writer.push_method(target)
|
|
44
|
+
@copy = !(target.is_a?(IO) || (defined?(::StringIO) && target.is_a?(::StringIO)))
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# @return [Symbol] how bytes are handed to `sink`: :write, :<< or :call
|
|
49
|
+
# @raise [ArgumentError] when it takes none of them
|
|
50
|
+
def self.push_method(sink)
|
|
51
|
+
# Proc#<< composes, so a callable is called before anything else is tried
|
|
52
|
+
if sink.is_a?(Proc) || sink.is_a?(Method) then :call
|
|
53
|
+
elsif sink.respond_to?(:write) then :write
|
|
54
|
+
elsif sink.respond_to?(:<<) then :<<
|
|
55
|
+
elsif sink.respond_to?(:call) then :call
|
|
56
|
+
else
|
|
57
|
+
raise ArgumentError, "#{sink.class} is not a sink: it answers none of write, << and call"
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# @param value [Integer] negative values are written as ten-byte two's complement
|
|
62
|
+
# @return [self]
|
|
63
|
+
def write_varint(value)
|
|
64
|
+
buf = @string
|
|
65
|
+
if value < 0x80 && value >= 0
|
|
66
|
+
buf << value
|
|
67
|
+
return self
|
|
68
|
+
end
|
|
69
|
+
value &= MASK64 if value < 0
|
|
70
|
+
while value >= 0x80
|
|
71
|
+
buf << ((value & 0x7F) | 0x80)
|
|
72
|
+
value >>= 7
|
|
73
|
+
end
|
|
74
|
+
buf << value
|
|
75
|
+
self
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# @return [self]
|
|
79
|
+
def write_tag(number, wire_type)
|
|
80
|
+
write_varint((number << 3) | wire_type)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# @param value [Integer] a signed integer, written zigzag-encoded (sint32, sint64)
|
|
84
|
+
# @return [self]
|
|
85
|
+
def write_zigzag(value)
|
|
86
|
+
write_varint((value << 1) ^ (value >> 63))
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# @return [self]
|
|
90
|
+
def write_fixed32(value)
|
|
91
|
+
[value].pack("V", buffer: @string)
|
|
92
|
+
self
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# @return [self]
|
|
96
|
+
def write_fixed64(value)
|
|
97
|
+
[value].pack("Q<", buffer: @string)
|
|
98
|
+
self
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# @return [self]
|
|
102
|
+
def write_sfixed32(value)
|
|
103
|
+
[value].pack("l<", buffer: @string)
|
|
104
|
+
self
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# @return [self]
|
|
108
|
+
def write_sfixed64(value)
|
|
109
|
+
[value].pack("q<", buffer: @string)
|
|
110
|
+
self
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# @return [self]
|
|
114
|
+
def write_float(value)
|
|
115
|
+
if value.is_a?(Float) && value.nan?
|
|
116
|
+
@string << Wire.float_nan(value)
|
|
117
|
+
else
|
|
118
|
+
[value].pack("e", buffer: @string)
|
|
119
|
+
end
|
|
120
|
+
self
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# @return [self]
|
|
124
|
+
def write_double(value)
|
|
125
|
+
[value].pack("E", buffer: @string)
|
|
126
|
+
self
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Writes a varint length followed by the bytes of `value`, whatever its encoding
|
|
130
|
+
# @return [self]
|
|
131
|
+
def write_bytes(value)
|
|
132
|
+
write_varint(value.bytesize)
|
|
133
|
+
write_raw(value)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
if String.method_defined?(:append_as_bytes)
|
|
137
|
+
# Appends the bytes of `value` as they are, whatever its encoding
|
|
138
|
+
# @return [self]
|
|
139
|
+
def write_raw(value)
|
|
140
|
+
@string.append_as_bytes(value)
|
|
141
|
+
flush_if_full
|
|
142
|
+
end
|
|
143
|
+
else
|
|
144
|
+
# Appends the bytes of `value` as they are, whatever its encoding
|
|
145
|
+
# @return [self]
|
|
146
|
+
def write_raw(value)
|
|
147
|
+
# `<<` would raise or re-tag the buffer for a non-ASCII UTF-8 value
|
|
148
|
+
[value].pack("a*", buffer: @string)
|
|
149
|
+
flush_if_full
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Writes the buffer to the sink once it holds {#flush_at} bytes
|
|
154
|
+
# @return [self]
|
|
155
|
+
def flush_if_full
|
|
156
|
+
flush if @io && @string.bytesize >= @flush_at
|
|
157
|
+
self
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Writes whatever is buffered to the sink. A no-op for a String target.
|
|
161
|
+
# @return [self]
|
|
162
|
+
def flush
|
|
163
|
+
if @io && !@string.empty?
|
|
164
|
+
deliver(@copy ? @string.dup : @string)
|
|
165
|
+
@string.clear
|
|
166
|
+
end
|
|
167
|
+
self
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Writes the buffer and then `bytes` to the sink, without copying
|
|
171
|
+
# `bytes` into the buffer first. For a String target, appends them.
|
|
172
|
+
# @return [self]
|
|
173
|
+
def write_through(bytes)
|
|
174
|
+
return write_raw(bytes) unless @io
|
|
175
|
+
if @copy
|
|
176
|
+
flush
|
|
177
|
+
deliver(bytes.b)
|
|
178
|
+
else
|
|
179
|
+
# IO#write and StringIO#write take several Strings in one call
|
|
180
|
+
deliver_all(@string, bytes)
|
|
181
|
+
@string.clear
|
|
182
|
+
end
|
|
183
|
+
self
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
private
|
|
187
|
+
|
|
188
|
+
def deliver_all(*chunks)
|
|
189
|
+
@io.write(*chunks)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def deliver(bytes)
|
|
193
|
+
case @push
|
|
194
|
+
when :write then @io.write(bytes)
|
|
195
|
+
when :<< then @io << bytes
|
|
196
|
+
else @io.call(bytes)
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|