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,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Protostar
|
|
4
|
+
module Lint
|
|
5
|
+
module Rules
|
|
6
|
+
# RPC_RESPONSE_STANDARD_NAME: An rpc whose response is not `<Rpc>Response`
|
|
7
|
+
class RpcResponseStandardName < Rule
|
|
8
|
+
REASON = "A response message of its own per RPC can grow fields without touching any other RPC."
|
|
9
|
+
EXPLANATION = <<~TEXT
|
|
10
|
+
Returning a shared type such as `User` means a field added for one RPC shows up in all of them.
|
|
11
|
+
|
|
12
|
+
rpc GetUser(GetUserRequest) returns (User);
|
|
13
|
+
|
|
14
|
+
Fix: `returns (GetUserResponse)`, with a `User user = 1;` field in it.
|
|
15
|
+
TEXT
|
|
16
|
+
|
|
17
|
+
def check
|
|
18
|
+
context.each(:service) do |service_node, service|
|
|
19
|
+
service.rpcs.each_with_index do |rpc, i|
|
|
20
|
+
base = Naming.pascal(rpc.name)
|
|
21
|
+
wanted = ["#{base}Response", "#{service.name}#{base}Response"]
|
|
22
|
+
next if wanted.include?(rpc.output_type.name)
|
|
23
|
+
report(service_node.rpcs[i], :output_type, "response of `#{rpc.name}` is `#{rpc.output_type.name}`, not `#{wanted[0]}`",
|
|
24
|
+
label: "not `#{wanted[0]}`", help: "return a `#{wanted[0]}` message of its own")
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Protostar
|
|
4
|
+
module Lint
|
|
5
|
+
module Rules
|
|
6
|
+
# SERVICE_PASCAL_CASE: A service name that is not PascalCase
|
|
7
|
+
class ServicePascalCase < Rule
|
|
8
|
+
REASON = "Services become classes and interfaces in generated code; PascalCase is what generators expect."
|
|
9
|
+
EXPLANATION = <<~TEXT
|
|
10
|
+
service user_api {}
|
|
11
|
+
|
|
12
|
+
Fix: `service UserAPIService {}`.
|
|
13
|
+
TEXT
|
|
14
|
+
|
|
15
|
+
def check
|
|
16
|
+
context.each(:service) do |node, service|
|
|
17
|
+
next if Naming.pascal?(service.name)
|
|
18
|
+
report(node, :name, "service `#{service.name}` is not PascalCase", label: "not PascalCase",
|
|
19
|
+
help: "name it `#{Naming.pascal(service.name)}`")
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Protostar
|
|
4
|
+
module Lint
|
|
5
|
+
module Rules
|
|
6
|
+
# SERVICE_SUFFIX: A service whose name does not end in `Service`
|
|
7
|
+
class ServiceSuffix < Rule
|
|
8
|
+
REASON = "A `Service` suffix keeps the generated client and server names from colliding with message names."
|
|
9
|
+
EXPLANATION = <<~TEXT
|
|
10
|
+
Generators name stubs and base classes after the service; `service User` next to `message User` makes
|
|
11
|
+
those collide or read ambiguously.
|
|
12
|
+
|
|
13
|
+
service Users { rpc Get(GetRequest) returns (GetResponse); }
|
|
14
|
+
|
|
15
|
+
Fix: `service UsersService`.
|
|
16
|
+
TEXT
|
|
17
|
+
|
|
18
|
+
def check
|
|
19
|
+
context.each(:service) do |node, service|
|
|
20
|
+
next if service.name.end_with?("Service")
|
|
21
|
+
report(node, :name, "service `#{service.name}` does not end in `Service`", label: "no `Service` suffix",
|
|
22
|
+
help: "name it `#{service.name}Service`")
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Protostar
|
|
4
|
+
module Lint
|
|
5
|
+
module Rules
|
|
6
|
+
# SYNTAX_SPECIFIED: A file without a `syntax` or `edition` line
|
|
7
|
+
class SyntaxSpecified < Rule
|
|
8
|
+
REASON = "Without `syntax` or `edition` a file is proto2, which is rarely what its author meant."
|
|
9
|
+
EXPLANATION = <<~TEXT
|
|
10
|
+
protoc reads a file with neither line as proto2, silently. Fields then need labels, enums are closed, and
|
|
11
|
+
defaults behave differently from proto3; a reader of the file cannot tell which was intended.
|
|
12
|
+
|
|
13
|
+
message User {
|
|
14
|
+
optional string name = 1;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
Fix: start the file with `edition = "2023";` (or `syntax = "proto3";`). `--fix` adds
|
|
18
|
+
`syntax = "proto2";`, which spells out what the file already is.
|
|
19
|
+
TEXT
|
|
20
|
+
|
|
21
|
+
def check
|
|
22
|
+
return if context.ast.syntax || context.ast.edition
|
|
23
|
+
text = context.text
|
|
24
|
+
fix = unless text.start_with?("\xEF\xBB\xBF".b) || text.b.start_with?("\xEF\xBB\xBF".b)
|
|
25
|
+
Diagnostics::Fix.new("add `syntax = \"proto2\";`, which is what the file already is",
|
|
26
|
+
[Diagnostics::Edit.new(1, 1, 1, 1, "syntax = \"proto2\";\n")])
|
|
27
|
+
end
|
|
28
|
+
span = Diagnostics::Span.new(1, 1, label: "proto2 by default")
|
|
29
|
+
add("file `#{context.name}` has no `syntax` or `edition` line", [span],
|
|
30
|
+
help: "start the file with `edition = \"2023\";` or `syntax = \"proto3\";`", fix: fix)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Protostar
|
|
4
|
+
module Lint
|
|
5
|
+
# Compiles each file on its own, so one that fails does not stop the
|
|
6
|
+
# others, sharing one pool so imports are built once; then runs the rules
|
|
7
|
+
# over every file that compiled.
|
|
8
|
+
#
|
|
9
|
+
# The compiler's diagnostics come first, in the order it reports them
|
|
10
|
+
# (protoc's): errors from any file, warnings for the files asked about.
|
|
11
|
+
# Then each file's lint findings, in file order and by position within a
|
|
12
|
+
# file. Comment directives switch off warnings, never errors.
|
|
13
|
+
class Runner
|
|
14
|
+
# What a run found
|
|
15
|
+
class Result
|
|
16
|
+
# @return [Array<Compiler::Diagnostic>]
|
|
17
|
+
attr_reader :diagnostics
|
|
18
|
+
# @return [Array<Context>] the files that compiled
|
|
19
|
+
attr_reader :contexts
|
|
20
|
+
# @return [Array<String>] every file asked about
|
|
21
|
+
attr_reader :names
|
|
22
|
+
|
|
23
|
+
def initialize(diagnostics, contexts, names)
|
|
24
|
+
@diagnostics = diagnostics.freeze
|
|
25
|
+
@contexts = contexts.freeze
|
|
26
|
+
@names = names.freeze
|
|
27
|
+
freeze
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def errors?
|
|
31
|
+
@diagnostics.any?(&:error?)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def warnings?
|
|
35
|
+
@diagnostics.any?(&:warning?)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# @return [Compiler]
|
|
40
|
+
attr_reader :compiler
|
|
41
|
+
|
|
42
|
+
# @param include_paths [Array<String>] where file names and imports are resolved
|
|
43
|
+
# @param rules [Array<String>] rule ids to run, from {Lint.select}
|
|
44
|
+
# @param except [Array<String>] rule ids switched off by name; the compiler
|
|
45
|
+
# warnings they would report are not passed through either
|
|
46
|
+
# @param directives [Boolean] honour `// protostar-lint:` comments
|
|
47
|
+
def initialize(include_paths:, rules:, except: [], directives: true)
|
|
48
|
+
@compiler = Compiler.new(include_paths: include_paths)
|
|
49
|
+
@rules = rules.map { |id| Lint.rule_class(id) }
|
|
50
|
+
@claimed = (rules + except).flat_map { |id| Lint.rule_class(id)::CLAIMS }.uniq
|
|
51
|
+
@directives = directives
|
|
52
|
+
@texts = {}
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# @param names [Array<String>] file names relative to an include path
|
|
56
|
+
# @param sources [Hash{String => String}] file contents by name, before the include paths
|
|
57
|
+
# @return [Result]
|
|
58
|
+
def run(names, sources: {})
|
|
59
|
+
@sources = sources
|
|
60
|
+
reported = []
|
|
61
|
+
seen = {}
|
|
62
|
+
linkers = {}
|
|
63
|
+
names.each do |name|
|
|
64
|
+
run = Compiler::Run.new(@compiler, [name], true, sources: sources, all_direct: true)
|
|
65
|
+
if linkers[name]
|
|
66
|
+
# built as an import of an earlier file: only the checks protoc
|
|
67
|
+
# runs on the files it is given are left
|
|
68
|
+
Compiler::Validator.cli_checks([linkers[name]], run)
|
|
69
|
+
else
|
|
70
|
+
run.call
|
|
71
|
+
end
|
|
72
|
+
run.diagnostics.each do |d|
|
|
73
|
+
next if seen[d]
|
|
74
|
+
seen[d] = true
|
|
75
|
+
reported << d
|
|
76
|
+
end
|
|
77
|
+
names.each { |n| linkers[n] ||= run.linker(n) }
|
|
78
|
+
end
|
|
79
|
+
failed = reported.select(&:error?).map(&:file).uniq
|
|
80
|
+
contexts = []
|
|
81
|
+
names.each do |name|
|
|
82
|
+
next if linkers[name].nil? || failed.include?(name)
|
|
83
|
+
warnings = reported.select { |d| d.warning? && d.file == name }
|
|
84
|
+
contexts << Context.new(name: name, path: shown_path(name), disk_path: disk_path(name),
|
|
85
|
+
linker: linkers[name], text: text(name), warnings: warnings, all: contexts)
|
|
86
|
+
end
|
|
87
|
+
diagnostics = reported.select do |d|
|
|
88
|
+
d.error? || (names.include?(d.file) && !@claimed.include?(d.code) && !suppressed?(d))
|
|
89
|
+
end
|
|
90
|
+
contexts.each { |context| diagnostics.concat(lint(context)) }
|
|
91
|
+
Result.new(diagnostics, contexts, names)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# @param name [String]
|
|
95
|
+
# @return [String, nil] the file's text, from the sources given or from disk
|
|
96
|
+
def text(name)
|
|
97
|
+
return @texts[name] if @texts.key?(name)
|
|
98
|
+
@texts[name] = if @sources&.key?(name)
|
|
99
|
+
@sources[name]
|
|
100
|
+
elsif (path = @compiler.resolve(name))
|
|
101
|
+
::File.binread(path)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
private
|
|
106
|
+
|
|
107
|
+
def lint(context)
|
|
108
|
+
found = @rules.flat_map do |rule_class|
|
|
109
|
+
rule = rule_class.new(context)
|
|
110
|
+
rule.check
|
|
111
|
+
rule.diagnostics
|
|
112
|
+
end
|
|
113
|
+
found.reject! { |d| suppressed?(d) }
|
|
114
|
+
found.each_with_index.sort_by { |d, i| [d.line || 0, d.column || 0, i] }.map(&:first)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def suppressed?(diagnostic)
|
|
118
|
+
return false unless @directives && diagnostic.warning?
|
|
119
|
+
@parsed ||= {}
|
|
120
|
+
directives = @parsed[diagnostic.file] ||= (text = text(diagnostic.file)) && Directives.new(text)
|
|
121
|
+
directives ? directives.suppressed?(diagnostic) : false
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def shown_path(name)
|
|
125
|
+
return nil if @sources.key?(name)
|
|
126
|
+
@compiler.disk_path(name)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def disk_path(name)
|
|
130
|
+
return nil if @sources.key?(name)
|
|
131
|
+
@compiler.resolve(name)
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Protostar
|
|
4
|
+
# `protostar lint`: compiles `.proto` files, reports the compiler's errors
|
|
5
|
+
# with {Diagnostics::Renderer}, and checks the files that compiled against
|
|
6
|
+
# best-practice rules, the kind `buf lint` checks, under the same names.
|
|
7
|
+
#
|
|
8
|
+
# runner = Protostar::Lint::Runner.new(include_paths: ["proto"], rules: Protostar::Lint.select(["default"]))
|
|
9
|
+
# result = runner.run(["acme/person.proto"])
|
|
10
|
+
# result.diagnostics.each { |d| puts d }
|
|
11
|
+
#
|
|
12
|
+
# Each rule is one class under {Lint::Rules}, in a file named after its id,
|
|
13
|
+
# and one line in {RULES}.
|
|
14
|
+
module Lint
|
|
15
|
+
autoload :Rule, ::File.expand_path("lint/rule", __dir__)
|
|
16
|
+
autoload :Context, ::File.expand_path("lint/context", __dir__)
|
|
17
|
+
autoload :Naming, ::File.expand_path("lint/naming", __dir__)
|
|
18
|
+
autoload :Config, ::File.expand_path("lint/config", __dir__)
|
|
19
|
+
autoload :Directives, ::File.expand_path("lint/directives", __dir__)
|
|
20
|
+
autoload :Runner, ::File.expand_path("lint/runner", __dir__)
|
|
21
|
+
autoload :Fixer, ::File.expand_path("lint/fixer", __dir__)
|
|
22
|
+
autoload :Command, ::File.expand_path("lint/command", __dir__)
|
|
23
|
+
|
|
24
|
+
# Raised for a configuration or command-line mistake
|
|
25
|
+
class Error < Protostar::Error; end
|
|
26
|
+
|
|
27
|
+
# The sets a rule can be in, each including the one before
|
|
28
|
+
TIERS = %i[minimal default strict].freeze
|
|
29
|
+
|
|
30
|
+
# Every rule: id => [code, the smallest set it is in, its category]
|
|
31
|
+
RULES = {
|
|
32
|
+
"FIELD_NOT_REQUIRED" => ["W0101", :minimal, :longevity],
|
|
33
|
+
"NO_GROUPS" => ["W0102", :minimal, :longevity],
|
|
34
|
+
"ENUM_ZERO_VALUE_SUFFIX" => ["W0103", :minimal, :longevity],
|
|
35
|
+
"ENUM_FIRST_VALUE_ZERO" => ["W0104", :minimal, :longevity],
|
|
36
|
+
"FIELD_NUMBER_REUSE_RISK" => ["W0105", :minimal, :longevity],
|
|
37
|
+
"RESERVED_NAMES_WITH_NUMBERS" => ["W0106", :minimal, :longevity],
|
|
38
|
+
"PACKAGE_DEFINED" => ["W0107", :minimal, :longevity],
|
|
39
|
+
"IMPORT_NO_PUBLIC" => ["W0108", :minimal, :longevity],
|
|
40
|
+
"IMPORT_NO_WEAK" => ["W0109", :minimal, :longevity],
|
|
41
|
+
"IMPORT_USED" => ["W0110", :minimal, :longevity],
|
|
42
|
+
"SYNTAX_SPECIFIED" => ["W0111", :minimal, :longevity],
|
|
43
|
+
"FILE_LOWER_SNAKE_CASE" => ["W0201", :default, :naming],
|
|
44
|
+
"PACKAGE_LOWER_SNAKE_CASE" => ["W0202", :default, :naming],
|
|
45
|
+
"MESSAGE_PASCAL_CASE" => ["W0203", :default, :naming],
|
|
46
|
+
"FIELD_LOWER_SNAKE_CASE" => ["W0204", :default, :naming],
|
|
47
|
+
"ONEOF_LOWER_SNAKE_CASE" => ["W0205", :default, :naming],
|
|
48
|
+
"ENUM_PASCAL_CASE" => ["W0206", :default, :naming],
|
|
49
|
+
# before UPPER_SNAKE_CASE: when both rename a value, the prefixed name fixes both
|
|
50
|
+
"ENUM_VALUE_PREFIX" => ["W0208", :default, :naming],
|
|
51
|
+
"ENUM_VALUE_UPPER_SNAKE_CASE" => ["W0207", :default, :naming],
|
|
52
|
+
"SERVICE_PASCAL_CASE" => ["W0209", :default, :naming],
|
|
53
|
+
"SERVICE_SUFFIX" => ["W0210", :default, :naming],
|
|
54
|
+
"RPC_PASCAL_CASE" => ["W0211", :default, :naming],
|
|
55
|
+
"RPC_REQUEST_STANDARD_NAME" => ["W0212", :default, :naming],
|
|
56
|
+
"RPC_RESPONSE_STANDARD_NAME" => ["W0213", :default, :naming],
|
|
57
|
+
"RPC_REQUEST_RESPONSE_UNIQUE" => ["W0214", :default, :naming],
|
|
58
|
+
"PACKAGE_DIRECTORY_MATCH" => ["W0301", :default, :structure],
|
|
59
|
+
"PACKAGE_SAME_DIRECTORY" => ["W0302", :default, :structure],
|
|
60
|
+
"NO_DEPRECATED_FEATURES" => ["W0303", :default, :structure],
|
|
61
|
+
"PACKAGE_VERSION_SUFFIX" => ["W0304", :strict, :structure],
|
|
62
|
+
"FIELD_NUMBERS_HOT_RANGE" => ["W0305", :strict, :structure],
|
|
63
|
+
"COMMENT_MESSAGE" => ["W0401", :strict, :documentation],
|
|
64
|
+
"COMMENT_FIELD" => ["W0402", :strict, :documentation],
|
|
65
|
+
"COMMENT_ENUM" => ["W0403", :strict, :documentation],
|
|
66
|
+
"COMMENT_ENUM_VALUE" => ["W0404", :strict, :documentation],
|
|
67
|
+
"COMMENT_SERVICE" => ["W0405", :strict, :documentation],
|
|
68
|
+
"COMMENT_RPC" => ["W0406", :strict, :documentation],
|
|
69
|
+
"EDITION_RECOMMENDED" => ["W0501", :strict, :editions]
|
|
70
|
+
}.freeze
|
|
71
|
+
|
|
72
|
+
# The rule classes, each loading its file on first use
|
|
73
|
+
module Rules
|
|
74
|
+
RULES.each_key do |id|
|
|
75
|
+
autoload(id.split("_").map(&:capitalize).join.to_sym, ::File.expand_path("lint/rules/#{id.downcase}", __dir__))
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# @return [Array<Symbol>] the category names a rule list can use
|
|
80
|
+
def self.categories
|
|
81
|
+
RULES.values.map(&:last).uniq
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# @param id [String] a rule id
|
|
85
|
+
# @return [Class] its {Rule} subclass
|
|
86
|
+
def self.rule_class(id)
|
|
87
|
+
Rules.const_get(id.split("_").map(&:capitalize).join, false)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# @param id_or_code [String] "FIELD_NOT_REQUIRED" or "W0101"
|
|
91
|
+
# @return [String, nil] the rule id
|
|
92
|
+
def self.rule_id(id_or_code)
|
|
93
|
+
key = id_or_code.upcase
|
|
94
|
+
return key if RULES.key?(key)
|
|
95
|
+
RULES.find { |_, (code, _, _)| code == key }&.first
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# The rules a list of set names, category names and rule ids selects, in
|
|
99
|
+
# {RULES} order
|
|
100
|
+
# @param names [Array<String>] e.g. ["default", "documentation", "PACKAGE_VERSION_SUFFIX"]
|
|
101
|
+
# @param except [Array<String>] rule ids or codes to leave out
|
|
102
|
+
# @return [Array<String>] rule ids
|
|
103
|
+
# @raise [Error] for a name that is none of these
|
|
104
|
+
def self.select(names, except: [])
|
|
105
|
+
chosen = names.flat_map do |name|
|
|
106
|
+
key = name.to_s.strip
|
|
107
|
+
if TIERS.include?(key.downcase.to_sym)
|
|
108
|
+
upto = TIERS.index(key.downcase.to_sym)
|
|
109
|
+
RULES.select { |_, (_, tier, _)| TIERS.index(tier) <= upto }.keys
|
|
110
|
+
elsif categories.include?(key.downcase.to_sym)
|
|
111
|
+
RULES.select { |_, (_, _, category)| category == key.downcase.to_sym }.keys
|
|
112
|
+
elsif (id = rule_id(key))
|
|
113
|
+
[id]
|
|
114
|
+
else
|
|
115
|
+
raise Error, "Unknown rule or set: #{key} (sets: #{TIERS.join(", ")}; categories: #{categories.join(", ")})"
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
skipped = except.map { |name| rule_id(name.to_s.strip) || raise(Error, "Unknown rule: #{name}") }
|
|
119
|
+
RULES.keys & (chosen - skipped)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Protostar
|
|
4
|
+
class Message
|
|
5
|
+
# Writes the field accessors of a message class as Ruby source and evals
|
|
6
|
+
# it into the class. Methods made from source, unlike `define_method`
|
|
7
|
+
# blocks, can be called from any Ractor, and they read their slot by a
|
|
8
|
+
# literal index.
|
|
9
|
+
# @api private
|
|
10
|
+
module Accessors
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
# Methods every message has; a field whose accessor would replace one
|
|
14
|
+
# is reachable only through `[]` and `[]=`
|
|
15
|
+
def reserved
|
|
16
|
+
(Message.public_instance_methods + %i[initialize]).to_h { |m| [m, true] }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# @param klass [Class]
|
|
20
|
+
# @param fields [Array<FieldInfo>]
|
|
21
|
+
# @param oneofs [Array<Array(Symbol, Array<Integer>)>] real oneofs: name and member slots
|
|
22
|
+
# @param reserved [Hash{Symbol => true}] from {reserved}, computed once for many classes
|
|
23
|
+
def install(klass, fields, oneofs, reserved = self.reserved)
|
|
24
|
+
taken = reserved.dup
|
|
25
|
+
src = +""
|
|
26
|
+
fields.each do |info|
|
|
27
|
+
name = info.name
|
|
28
|
+
i = info.index
|
|
29
|
+
reader = name.to_sym
|
|
30
|
+
writer = :"#{name}="
|
|
31
|
+
unless taken[reader]
|
|
32
|
+
taken[reader] = true
|
|
33
|
+
src << reader_source(name, info)
|
|
34
|
+
end
|
|
35
|
+
unless taken[writer]
|
|
36
|
+
taken[writer] = true
|
|
37
|
+
src << "def #{name}=(v); __set(#{i}, v); end\n"
|
|
38
|
+
end
|
|
39
|
+
next unless info.presence
|
|
40
|
+
has = :"has_#{name}?"
|
|
41
|
+
clear = :"clear_#{name}"
|
|
42
|
+
unless taken[has]
|
|
43
|
+
taken[has] = true
|
|
44
|
+
src << "def has_#{name}?; !@values[#{i}].nil?; end\n"
|
|
45
|
+
end
|
|
46
|
+
unless taken[clear]
|
|
47
|
+
taken[clear] = true
|
|
48
|
+
src << "def clear_#{name}; __clear(#{i}); end\n"
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
oneofs.each do |oneof_name, slots|
|
|
52
|
+
which = :"which_#{oneof_name}"
|
|
53
|
+
next if taken[which]
|
|
54
|
+
taken[which] = true
|
|
55
|
+
src << "def which_#{oneof_name}\n"
|
|
56
|
+
slots.each do |slot|
|
|
57
|
+
src << " return :#{fields[slot].name} unless @values[#{slot}].nil?\n"
|
|
58
|
+
end
|
|
59
|
+
src << " nil\nend\n"
|
|
60
|
+
end
|
|
61
|
+
klass.class_eval(src, "(protostar #{klass.descriptor.full_name})", 1)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def reader_source(name, info)
|
|
65
|
+
i = info.index
|
|
66
|
+
if info.repeated || info.map
|
|
67
|
+
"def #{name}; @values[#{i}] || __container(#{i}); end\n"
|
|
68
|
+
elsif info.message? || !info.presence
|
|
69
|
+
"def #{name}; @values[#{i}]; end\n"
|
|
70
|
+
else
|
|
71
|
+
"def #{name}; v = @values[#{i}]; v.nil? ? #{default_literal(info)} : v; end\n"
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# The default as a literal when it has an exact one, otherwise a lookup
|
|
76
|
+
def default_literal(info)
|
|
77
|
+
d = info.default
|
|
78
|
+
case d
|
|
79
|
+
when Integer, true, false, Symbol then d.inspect
|
|
80
|
+
when Float then d.finite? ? d.inspect : "self.class.__fields[#{info.index}].default"
|
|
81
|
+
else "self.class.__fields[#{info.index}].default"
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Protostar
|
|
4
|
+
class Message
|
|
5
|
+
# Checks and converts values assigned to fields. Everything stored in a
|
|
6
|
+
# message has been through here (or through the decoder, which produces
|
|
7
|
+
# the same shapes): Integers in range, Floats, true/false, frozen UTF-8 or
|
|
8
|
+
# binary Strings, enum Symbols (or Integers for numbers without a name),
|
|
9
|
+
# messages of the field's class.
|
|
10
|
+
# @api private
|
|
11
|
+
module Coercion
|
|
12
|
+
RANGES = {
|
|
13
|
+
FieldInfo::INT32 => -0x8000_0000..0x7FFF_FFFF,
|
|
14
|
+
FieldInfo::SINT32 => -0x8000_0000..0x7FFF_FFFF,
|
|
15
|
+
FieldInfo::SFIXED32 => -0x8000_0000..0x7FFF_FFFF,
|
|
16
|
+
FieldInfo::UINT32 => 0..0xFFFF_FFFF,
|
|
17
|
+
FieldInfo::FIXED32 => 0..0xFFFF_FFFF,
|
|
18
|
+
FieldInfo::INT64 => -0x8000_0000_0000_0000..0x7FFF_FFFF_FFFF_FFFF,
|
|
19
|
+
FieldInfo::SINT64 => -0x8000_0000_0000_0000..0x7FFF_FFFF_FFFF_FFFF,
|
|
20
|
+
FieldInfo::SFIXED64 => -0x8000_0000_0000_0000..0x7FFF_FFFF_FFFF_FFFF,
|
|
21
|
+
FieldInfo::UINT64 => 0..0xFFFF_FFFF_FFFF_FFFF,
|
|
22
|
+
FieldInfo::FIXED64 => 0..0xFFFF_FFFF_FFFF_FFFF
|
|
23
|
+
}.freeze
|
|
24
|
+
|
|
25
|
+
ENUM_RANGE = RANGES.fetch(FieldInfo::INT32)
|
|
26
|
+
FLOAT_MAX = 3.4028234663852886e+38
|
|
27
|
+
# Halfway between FLT_MAX and 2**128: doubles below it round to FLT_MAX
|
|
28
|
+
FLOAT_ROUNDS_TO_MAX = 3.4028235677973366e+38
|
|
29
|
+
UTF_8 = Encoding::UTF_8
|
|
30
|
+
BINARY = Encoding::BINARY
|
|
31
|
+
|
|
32
|
+
module_function
|
|
33
|
+
|
|
34
|
+
# @param info [FieldInfo]
|
|
35
|
+
# @return [Object] what gets stored for a non-nil value assigned to the field
|
|
36
|
+
# @raise [Protostar::TypeError]
|
|
37
|
+
def field(info, value)
|
|
38
|
+
if info.map
|
|
39
|
+
raise_type(info, value, "a Hash") unless value.is_a?(Hash)
|
|
40
|
+
key = info.key
|
|
41
|
+
val = info.value
|
|
42
|
+
out = {}
|
|
43
|
+
value.each { |k, v| out[scalar(key, k)] = single(val, v) }
|
|
44
|
+
out
|
|
45
|
+
elsif info.repeated
|
|
46
|
+
raise_type(info, value, "an Array") unless value.is_a?(Array)
|
|
47
|
+
value.map { |v| single(info, v) }
|
|
48
|
+
else
|
|
49
|
+
single(info, value)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# One value of a singular field, an element of a repeated field, or a map value
|
|
54
|
+
def single(info, value)
|
|
55
|
+
if info.message?
|
|
56
|
+
klass = info.message_class
|
|
57
|
+
return value if value.instance_of?(klass)
|
|
58
|
+
return klass.new(value) if value.is_a?(Hash)
|
|
59
|
+
begin
|
|
60
|
+
coerced = klass.__coerce(value)
|
|
61
|
+
rescue RangeError
|
|
62
|
+
raise_type(info, value, "a finite value in #{klass.descriptor.full_name}'s range")
|
|
63
|
+
end
|
|
64
|
+
return coerced if coerced
|
|
65
|
+
raise_type(info, value, "a #{klass.descriptor.full_name} or a Hash")
|
|
66
|
+
end
|
|
67
|
+
scalar(info, value)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def scalar(info, value)
|
|
71
|
+
case (kind = info.kind)
|
|
72
|
+
when FieldInfo::DOUBLE
|
|
73
|
+
raise_type(info, value, "a Numeric") unless value.is_a?(Float) || value.is_a?(Integer)
|
|
74
|
+
value.to_f
|
|
75
|
+
when FieldInfo::FLOAT
|
|
76
|
+
raise_type(info, value, "a Numeric") unless value.is_a?(Float) || value.is_a?(Integer)
|
|
77
|
+
# stored at single precision so that reads match what decoding gives back
|
|
78
|
+
float32(value.to_f)
|
|
79
|
+
when FieldInfo::BOOL
|
|
80
|
+
raise_type(info, value, "true or false") unless value == true || value == false
|
|
81
|
+
value
|
|
82
|
+
when FieldInfo::STRING
|
|
83
|
+
string(info, value)
|
|
84
|
+
when FieldInfo::BYTES
|
|
85
|
+
raise_type(info, value, "a String") unless value.is_a?(String)
|
|
86
|
+
(value.frozen? && value.encoding == BINARY) ? value : value.b.freeze
|
|
87
|
+
when FieldInfo::ENUM
|
|
88
|
+
enum(info, value)
|
|
89
|
+
else
|
|
90
|
+
range = RANGES.fetch(kind)
|
|
91
|
+
unless value.is_a?(Integer) && range.cover?(value)
|
|
92
|
+
raise_type(info, value, "an Integer in #{info.descriptor.type} range")
|
|
93
|
+
end
|
|
94
|
+
value
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# The nearest single-precision value, as C's double-to-float conversion
|
|
99
|
+
# gives it. `pack("e")` alone turns everything above FLT_MAX into
|
|
100
|
+
# Infinity, even values that round down to FLT_MAX, and every NaN into
|
|
101
|
+
# the same one.
|
|
102
|
+
def float32(f)
|
|
103
|
+
a = f.abs
|
|
104
|
+
if a > FLOAT_MAX && a < FLOAT_ROUNDS_TO_MAX
|
|
105
|
+
(f > 0) ? FLOAT_MAX : -FLOAT_MAX
|
|
106
|
+
elsif f.nan?
|
|
107
|
+
Wire.float_nan(f).unpack1("e")
|
|
108
|
+
else
|
|
109
|
+
[f].pack("e").unpack1("e")
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def string(info, value)
|
|
114
|
+
raise_type(info, value, "a String") unless value.is_a?(String)
|
|
115
|
+
enc = value.encoding
|
|
116
|
+
if enc == UTF_8
|
|
117
|
+
raise_type(info, value, "valid UTF-8") unless value.valid_encoding?
|
|
118
|
+
value.frozen? ? value : value.dup.freeze
|
|
119
|
+
elsif enc == BINARY || enc == Encoding::US_ASCII
|
|
120
|
+
s = value.dup.force_encoding(UTF_8)
|
|
121
|
+
raise_type(info, value, "valid UTF-8") unless s.valid_encoding?
|
|
122
|
+
s.freeze
|
|
123
|
+
else
|
|
124
|
+
begin
|
|
125
|
+
value.encode(UTF_8).freeze
|
|
126
|
+
rescue EncodingError
|
|
127
|
+
raise_type(info, value, "a String convertible to UTF-8")
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def enum(info, value)
|
|
133
|
+
if value.is_a?(Symbol)
|
|
134
|
+
number = info.enum_numbers[value]
|
|
135
|
+
raise_type(info, value, "a value of #{info.enum_module.descriptor.full_name}") unless number
|
|
136
|
+
info.enum_names.fetch(number)
|
|
137
|
+
elsif value.is_a?(Integer)
|
|
138
|
+
name = info.enum_names[value]
|
|
139
|
+
return name if name
|
|
140
|
+
if info.closed_enum || !ENUM_RANGE.cover?(value)
|
|
141
|
+
raise_type(info, value, "a value of #{info.enum_module.descriptor.full_name}")
|
|
142
|
+
end
|
|
143
|
+
value
|
|
144
|
+
else
|
|
145
|
+
raise_type(info, value, "a Symbol or Integer")
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Whether a value found in a repeated field or map is one the encoder can
|
|
150
|
+
# write. Those containers are plain Arrays and Hashes that callers may
|
|
151
|
+
# have filled directly, past the writers' checks.
|
|
152
|
+
def storable?(info, value)
|
|
153
|
+
case (kind = info.kind)
|
|
154
|
+
when FieldInfo::DOUBLE, FieldInfo::FLOAT then value.is_a?(Float) || value.is_a?(Integer)
|
|
155
|
+
when FieldInfo::BOOL then value == true || value == false
|
|
156
|
+
when FieldInfo::STRING, FieldInfo::BYTES then value.is_a?(String)
|
|
157
|
+
when FieldInfo::ENUM
|
|
158
|
+
value.is_a?(Symbol) ? info.enum_numbers.key?(value) : (value.is_a?(Integer) && ENUM_RANGE.cover?(value))
|
|
159
|
+
when FieldInfo::MESSAGE, FieldInfo::GROUP then value.instance_of?(info.message_class)
|
|
160
|
+
else value.is_a?(Integer) && RANGES.fetch(kind).cover?(value)
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def raise_type(info, value, expected)
|
|
165
|
+
shown = value.inspect
|
|
166
|
+
shown = "#{shown[0, 60]}..." if shown.length > 64
|
|
167
|
+
raise Protostar::TypeError, "#{info.descriptor.full_name} expects #{expected}, got #{shown}"
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|