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
data/README.md
ADDED
|
@@ -0,0 +1,647 @@
|
|
|
1
|
+
# protostar
|
|
2
|
+
|
|
3
|
+
(Slightly unhinged) Protocol buffers in pure Ruby. protostar compiles `.proto` files to Ruby, encodes and
|
|
4
|
+
decodes messages, reads them without decoding, writes them as JSON and as text, and can
|
|
5
|
+
stand in for Google's `google-protobuf` gem so that `grpc` and friends run without it.
|
|
6
|
+
|
|
7
|
+
No `protoc`, no C extension, no runtime dependencies. No native memory handles, GC problems, no uninspectable
|
|
8
|
+
blobs, no issues with `fork()`, Fibers or Ractors or Threads. Full introspection and control. Ruby 3.2 or newer.
|
|
9
|
+
|
|
10
|
+
## The north star
|
|
11
|
+
|
|
12
|
+
Why bother? Of course pure Ruby will be slower, you say... and that's correct.
|
|
13
|
+
|
|
14
|
+
But: a Ruby app that speaks protobuf usually drags in a native gem that
|
|
15
|
+
has to compile (or come prebuilt for your exact platform), plus a `protoc` binary of a
|
|
16
|
+
matching version that someone has to install on every machine that regenerates code. protoc
|
|
17
|
+
versions change. The DSL it outputs changes. The formatting changes. The proto messages are
|
|
18
|
+
handles to native pointers - they can't be marshaled, they can't be cached, they can't be
|
|
19
|
+
recreated without access to the native gem and the kitchen sink. They are, by any stretch
|
|
20
|
+
of the imagination, _as anti-Ruby spirited as things can be._
|
|
21
|
+
|
|
22
|
+
This is why. We sacrifice some speed (primarily in encoding and allocations) for the
|
|
23
|
+
luxury of having a readable, predictable, Ruby-spirited protobuf universum.
|
|
24
|
+
|
|
25
|
+
One that doesn't scream _"We despise you and your ridiculous slow toy language, but we made it
|
|
26
|
+
work because we were told to"_ every time you touch it in your `irb`.
|
|
27
|
+
|
|
28
|
+
protostar is one gem. It passes protobuf's own conformance suite, reproduces protoc's
|
|
29
|
+
descriptors byte for byte on protoc's own test files, and runs Google's Ruby tests through
|
|
30
|
+
its `google-protobuf` shim.
|
|
31
|
+
|
|
32
|
+
## Very experimental
|
|
33
|
+
|
|
34
|
+
This is young code. It passes protobuf's conformance suite and Google's own Ruby tests, but
|
|
35
|
+
it has not yet carried anyone's production traffic, the API will move before 1.0, and the
|
|
36
|
+
`google-protobuf` shim has a documented list of places where it differs from the C gem.
|
|
37
|
+
Try it, kick it, and tell me what breaks.
|
|
38
|
+
|
|
39
|
+
Ideas are as welcome as bug reports. The `llm/` directory is where this project thinks out
|
|
40
|
+
loud: `llm/todos/` holds feature briefs that are not scheduled yet (lazy readers, Marshal
|
|
41
|
+
round trips for Rails caching, a native codec, arenas, and more), `llm/instructions/` the
|
|
42
|
+
briefs that became workstreams, and `llm/outputs/` what each one reported back. If you have
|
|
43
|
+
a feature in mind, open a PR that adds a brief to `llm/todos/` in the same shape as the ones
|
|
44
|
+
there: what you want, why, what it would take. That is the fastest way to get it built.
|
|
45
|
+
|
|
46
|
+
## Installation
|
|
47
|
+
|
|
48
|
+
protostar is in prerelease, so name the version (or `gem install protostar --pre`):
|
|
49
|
+
|
|
50
|
+
```ruby
|
|
51
|
+
gem "protostar", "0.1.0.beta1"
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Compile a `.proto`
|
|
55
|
+
|
|
56
|
+
Take a schema:
|
|
57
|
+
|
|
58
|
+
```proto
|
|
59
|
+
// proto/acme/person.proto
|
|
60
|
+
syntax = "proto3";
|
|
61
|
+
|
|
62
|
+
package acme;
|
|
63
|
+
|
|
64
|
+
import "google/protobuf/timestamp.proto";
|
|
65
|
+
|
|
66
|
+
message Person {
|
|
67
|
+
string name = 1;
|
|
68
|
+
int32 id = 2;
|
|
69
|
+
repeated string emails = 3;
|
|
70
|
+
map<string, string> labels = 4;
|
|
71
|
+
Address address = 5;
|
|
72
|
+
google.protobuf.Timestamp born_at = 6;
|
|
73
|
+
oneof contact {
|
|
74
|
+
string phone = 7;
|
|
75
|
+
string pager = 8;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
message Address {
|
|
80
|
+
string street = 1;
|
|
81
|
+
string city = 2;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
service Directory {
|
|
85
|
+
rpc Find(Person) returns (Person);
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
and compile it:
|
|
90
|
+
|
|
91
|
+
```console
|
|
92
|
+
$ bundle exec protostar proto/acme/person.proto > person_pb.rb
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
With one file and no output flags the Ruby goes to stdout, and errors go to stderr.
|
|
96
|
+
The generated file is meant to be read, and committed:
|
|
97
|
+
|
|
98
|
+
```ruby
|
|
99
|
+
# frozen_string_literal: true
|
|
100
|
+
|
|
101
|
+
# Generated by protostar from person.proto. Do not edit.
|
|
102
|
+
|
|
103
|
+
require "protostar"
|
|
104
|
+
require "protostar/google/protobuf/timestamp_pb"
|
|
105
|
+
|
|
106
|
+
Protostar::DSL.file(
|
|
107
|
+
"person.proto",
|
|
108
|
+
package: "acme",
|
|
109
|
+
syntax: :proto3,
|
|
110
|
+
dependencies: ["google/protobuf/timestamp.proto"]
|
|
111
|
+
) do |f|
|
|
112
|
+
f.message "Person" do |m|
|
|
113
|
+
m.field :name, 1, :string
|
|
114
|
+
m.field :id, 2, :int32
|
|
115
|
+
m.field :emails, 3, :string, label: :repeated
|
|
116
|
+
m.map :labels, 4, :string, :string
|
|
117
|
+
m.field :address, 5, :message, ".acme.Address"
|
|
118
|
+
m.field :born_at, 6, :message, ".google.protobuf.Timestamp"
|
|
119
|
+
m.oneof :contact do |o|
|
|
120
|
+
o.field :phone, 7, :string
|
|
121
|
+
o.field :pager, 8, :string
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
f.message "Address" do |m|
|
|
125
|
+
m.field :street, 1, :string
|
|
126
|
+
m.field :city, 2, :string
|
|
127
|
+
end
|
|
128
|
+
f.service "Directory" do |s|
|
|
129
|
+
s.rpc :Find, ".acme.Person", ".acme.Person"
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
module Acme
|
|
134
|
+
Person = Protostar::DSL.message("acme.Person")
|
|
135
|
+
Address = Protostar::DSL.message("acme.Address")
|
|
136
|
+
Directory = Protostar::DSL.service("acme.Directory")
|
|
137
|
+
end
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Without `-I`, the directory the file is in is where imports are looked up, and the
|
|
141
|
+
file is named `person.proto`. Once protos import each other by paths like
|
|
142
|
+
`acme/person.proto`, compile them like protoc, with protoc's flags:
|
|
143
|
+
|
|
144
|
+
```console
|
|
145
|
+
$ bundle exec protostar compile -I proto --ruby_out=lib proto/acme/person.proto
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
That writes `lib/acme/person_pb.rb`, for the file named `acme/person.proto` under the
|
|
149
|
+
include path `proto`. `--ruby_out=DIR` also takes several files at once.
|
|
150
|
+
|
|
151
|
+
The well-known types (`google/protobuf/timestamp.proto` and the rest) are bundled, so
|
|
152
|
+
importing them needs no `-I`. Other outputs:
|
|
153
|
+
|
|
154
|
+
- `--grpc_out=DIR` writes gRPC service stubs (`*_services_pb.rb`)
|
|
155
|
+
- `--rbs_out=DIR` writes RBS signatures for the classes
|
|
156
|
+
- `--google_out=DIR` writes exactly what `protoc --ruby_out` writes, for the google-protobuf API
|
|
157
|
+
- `--descriptor_set_out=FILE` writes a `FileDescriptorSet`, with `--include_imports` and `--include_source_info`
|
|
158
|
+
|
|
159
|
+
`protostar --help` lists them all. Errors look like protoc's, file and line first.
|
|
160
|
+
|
|
161
|
+
### From Rake
|
|
162
|
+
|
|
163
|
+
Let Rake recompile a proto when it changes:
|
|
164
|
+
|
|
165
|
+
```ruby
|
|
166
|
+
rule "_pb.rb" => ->(target) { target.sub(/_pb\.rb\z/, ".proto") } do |t|
|
|
167
|
+
sh "protostar", "compile", "-I", ".", "--ruby_out=.", t.source
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
desc "Compile every .proto here into a _pb.rb next to it"
|
|
171
|
+
task protos: FileList["*.proto"].pathmap("%X_pb.rb")
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### As a protoc plugin
|
|
175
|
+
|
|
176
|
+
If a build already runs protoc, protostar plugs into it as `protoc-gen-protostar`:
|
|
177
|
+
|
|
178
|
+
```console
|
|
179
|
+
$ bundle exec protoc -I proto --protostar_out=lib proto/acme/person.proto
|
|
180
|
+
$ bundle exec protoc -I proto --protostar_out=ruby,grpc:lib proto/acme/person.proto
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
The parameter picks the outputs: `ruby` (the default), `google`, `rbs`, `grpc`.
|
|
184
|
+
|
|
185
|
+
### Without generated files
|
|
186
|
+
|
|
187
|
+
Compile at runtime if you would rather not commit generated code:
|
|
188
|
+
|
|
189
|
+
```ruby
|
|
190
|
+
files = Protostar::Compiler.new(include_paths: ["proto"]).compile(["acme/person.proto"])
|
|
191
|
+
registry = Protostar::Types::Registry.new
|
|
192
|
+
files.each { |f| registry.define(f) } # imports first, as compile returns them
|
|
193
|
+
person_class = registry.find("acme.Person") # => #<Class acme.Person>
|
|
194
|
+
person_class.new(name: "Ada") # => #<acme.Person name: "Ada">
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Classes defined this way are anonymous; they live in the registry, not in constants.
|
|
198
|
+
|
|
199
|
+
## Lint a `.proto`
|
|
200
|
+
|
|
201
|
+
`protostar lint` compiles files (or every `*.proto` under a directory) and explains
|
|
202
|
+
what is wrong the way `rustc` does, then checks what compiled against best-practice
|
|
203
|
+
rules, under `buf lint`'s names:
|
|
204
|
+
|
|
205
|
+
```console
|
|
206
|
+
$ bundle exec protostar lint proto/
|
|
207
|
+
warning[W0214]: `Person` is the response of `Find` and already the request of `Find`
|
|
208
|
+
--> proto/acme/person.proto:27:29
|
|
209
|
+
|
|
|
210
|
+
27 | rpc Find(Person) returns (Person);
|
|
211
|
+
| ------ ^^^^^^ used again
|
|
212
|
+
| |
|
|
213
|
+
| first used here
|
|
214
|
+
|
|
|
215
|
+
= help: give `Find` a `FindResponse` of its own
|
|
216
|
+
= note: rule RPC_REQUEST_RESPONSE_UNIQUE (default set)
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Compiler errors get the same treatment, with a code to look up (here with `id = 1`):
|
|
220
|
+
|
|
221
|
+
```console
|
|
222
|
+
error[L0301]: field number 1 is already used in `acme.Person`
|
|
223
|
+
--> proto/acme/person.proto:10:14
|
|
224
|
+
|
|
|
225
|
+
9 | string name = 1;
|
|
226
|
+
| - first used by `name`
|
|
227
|
+
10 | int32 id = 1;
|
|
228
|
+
| ^ reused here
|
|
229
|
+
|
|
|
230
|
+
= help: the next available field number is 2
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
`protostar lint --explain L0301` (or a rule, `--explain FIELD_NOT_REQUIRED`) says why
|
|
234
|
+
and how to fix it; `--list-rules` lists the rules. They come in three sets, each
|
|
235
|
+
including the one before:
|
|
236
|
+
|
|
237
|
+
- `minimal` (11 rules): the longevity ones, led by "`required` is forever"
|
|
238
|
+
(`FIELD_NOT_REQUIRED`), plus groups, enum zero values, unreserved gaps in field
|
|
239
|
+
numbers, imports and a missing `syntax` line
|
|
240
|
+
- `default` (28, what runs unless told otherwise): naming and package layout
|
|
241
|
+
- `strict` (37): comments on everything, version suffixes, field numbers 1 to 15, editions
|
|
242
|
+
|
|
243
|
+
Pick with `--rules default,documentation` (sets, categories or rule ids) and
|
|
244
|
+
`--except FIELD_NOT_REQUIRED`, or in a `.protostar.yml` next to the inputs or in the
|
|
245
|
+
current directory:
|
|
246
|
+
|
|
247
|
+
```yaml
|
|
248
|
+
lint:
|
|
249
|
+
rules: [default]
|
|
250
|
+
except: [PACKAGE_VERSION_SUFFIX]
|
|
251
|
+
ignore: ["vendor/**"]
|
|
252
|
+
allow_comment_directives: true
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
A comment switches a rule off for the next line, or for a stretch of the file:
|
|
256
|
+
|
|
257
|
+
```proto
|
|
258
|
+
// protostar-lint: disable-next-line FIELD_NOT_REQUIRED
|
|
259
|
+
required string id = 1;
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
`// protostar-lint: disable COMMENT_FIELD` ... `// protostar-lint: enable COMMENT_FIELD`
|
|
263
|
+
does the same for everything between. `--fix` applies the fixes that are mechanical
|
|
264
|
+
(renaming a field, oneof or enum value nothing else in the file mentions, adding the
|
|
265
|
+
missing `syntax` line) and prints the diff; `--diff` prints it without writing.
|
|
266
|
+
`--format json` prints one object per finding, `--format github` workflow annotations
|
|
267
|
+
for pull requests. The exit status is 0 when clean, 1 with warnings (unless
|
|
268
|
+
`--warnings-ok`) and 2 with errors.
|
|
269
|
+
|
|
270
|
+
`protostar compile --error_format=rich` prints compile errors this way too; without it,
|
|
271
|
+
`compile` prints protoc's one-liners, as before.
|
|
272
|
+
|
|
273
|
+
## Use the classes
|
|
274
|
+
|
|
275
|
+
```ruby
|
|
276
|
+
require "acme/person_pb"
|
|
277
|
+
|
|
278
|
+
person = Acme::Person.new(name: "Ada", id: 7, emails: ["ada@example.com"])
|
|
279
|
+
person.address = Acme::Address.new(city: "London")
|
|
280
|
+
person.labels["team"] = "engines"
|
|
281
|
+
person.born_at = Time.utc(1815, 12, 10)
|
|
282
|
+
person.phone = "+44 20 7946 0000"
|
|
283
|
+
|
|
284
|
+
person.name # => "Ada"
|
|
285
|
+
person.which_contact # => :phone
|
|
286
|
+
person
|
|
287
|
+
# => #<acme.Person name: "Ada", id: 7, emails: ["ada@example.com"], labels: {"team" => "engines"}, address: #<acme.Address city: "London">, born_at: #<google.protobuf.Timestamp seconds: -4861728000>, phone: "+44 20 7946 0000">
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
Fields are keyword arguments or a Hash, repeated fields are Arrays, maps are Hashes, enums
|
|
291
|
+
are Symbols. Values are checked when you assign them:
|
|
292
|
+
|
|
293
|
+
```ruby
|
|
294
|
+
person.id = "seven"
|
|
295
|
+
# raises Protostar::TypeError: acme.Person.id expects an Integer in int32 range, got "seven"
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
`Protostar::TypeError` is a Ruby `TypeError`. Fields with presence get `has_name?` and
|
|
299
|
+
`clear_name`; oneofs get `which_contact`; every field is also `person[:name]`.
|
|
300
|
+
|
|
301
|
+
### Binary
|
|
302
|
+
|
|
303
|
+
```ruby
|
|
304
|
+
bytes = person.encode # also Acme::Person.encode(person)
|
|
305
|
+
bytes.bytesize # => 82
|
|
306
|
+
Acme::Person.decode(bytes) == person # => true
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
Fields the schema does not know (sent by a newer version of it) are kept and written back
|
|
310
|
+
on `encode`; `unknown_fields` lists them. A broken input raises `Protostar::DecodeError`
|
|
311
|
+
with the offset:
|
|
312
|
+
|
|
313
|
+
```ruby
|
|
314
|
+
Acme::Person.decode("\x0a\x05ab")
|
|
315
|
+
# raises Protostar::DecodeError: length 5 runs past the end of the input at byte 2
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
For a stream of messages, `encode_delimited(io)` and `Klass.decode_delimited(io)`, which
|
|
319
|
+
returns nil at the end. `msg.encode_to(io)` writes to an IO without building the String.
|
|
320
|
+
|
|
321
|
+
### JSON
|
|
322
|
+
|
|
323
|
+
The proto3 JSON mapping, as google-protobuf writes it:
|
|
324
|
+
|
|
325
|
+
```ruby
|
|
326
|
+
person.to_json
|
|
327
|
+
# => "{\"name\":\"Ada\",\"id\":7,\"emails\":[\"ada@example.com\"],\"labels\":{\"team\":\"engines\"},\"address\":{\"city\":\"London\"},\"bornAt\":\"1815-12-10T00:00:00Z\",\"phone\":\"+44 20 7946 0000\"}"
|
|
328
|
+
Acme::Person.decode_json(person.to_json) == person # => true
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
`to_json` and `Klass.encode_json` take `preserve_proto_field_names:`, `emit_defaults:` and
|
|
332
|
+
`format_enums_as_integers:`; `decode_json` takes `ignore_unknown_fields:`.
|
|
333
|
+
`JSON.pretty_generate(message)` works:
|
|
334
|
+
|
|
335
|
+
```ruby
|
|
336
|
+
puts JSON.pretty_generate(Acme::Address.new(city: "Delft", street: "Oude Delft 1"))
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
```
|
|
340
|
+
{
|
|
341
|
+
"street": "Oude Delft 1",
|
|
342
|
+
"city": "Delft"
|
|
343
|
+
}
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
### Text format
|
|
347
|
+
|
|
348
|
+
protoc's text format, for logs and fixtures:
|
|
349
|
+
|
|
350
|
+
```ruby
|
|
351
|
+
puts person.to_text
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
```
|
|
355
|
+
name: "Ada"
|
|
356
|
+
id: 7
|
|
357
|
+
emails: "ada@example.com"
|
|
358
|
+
labels {
|
|
359
|
+
key: "team"
|
|
360
|
+
value: "engines"
|
|
361
|
+
}
|
|
362
|
+
address {
|
|
363
|
+
city: "London"
|
|
364
|
+
}
|
|
365
|
+
born_at {
|
|
366
|
+
seconds: -4861728000
|
|
367
|
+
}
|
|
368
|
+
phone: "+44 20 7946 0000"
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
```ruby
|
|
372
|
+
Protostar::Text.parse(Acme::Person, 'name: "Ada" address { city: "London" }')
|
|
373
|
+
# => #<acme.Person name: "Ada", address: #<acme.Address city: "London">>
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
### Views: read without decoding
|
|
377
|
+
|
|
378
|
+
A view reads fields straight from the bytes. It looks at the top level once, and decodes
|
|
379
|
+
a field only when you read it; nested messages are skipped until you open them. On a
|
|
380
|
+
1 MiB message, reading two fields through a view allocates 8 objects where `decode`
|
|
381
|
+
allocates 2,522.
|
|
382
|
+
|
|
383
|
+
```ruby
|
|
384
|
+
view = Acme::Person.view(bytes) # a String, or an IO::Buffer (a mapped file, say)
|
|
385
|
+
view.address.city # => "London"
|
|
386
|
+
view.emails.to_a # => ["ada@example.com"]
|
|
387
|
+
view.to_message # the decoded message, when you want all of it
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
Views are read-only, equal to the message they hold (`view == message`), and raise the
|
|
391
|
+
same errors as `decode`.
|
|
392
|
+
|
|
393
|
+
### Ractors
|
|
394
|
+
|
|
395
|
+
Classes and descriptors are frozen and shareable, so messages can be decoded, encoded and
|
|
396
|
+
read in any Ractor. Decode with `frozen: true` to get a message you can pass between
|
|
397
|
+
them:
|
|
398
|
+
|
|
399
|
+
```ruby
|
|
400
|
+
Ractor.shareable?(Acme::Person) # => true
|
|
401
|
+
frozen = Acme::Person.decode(bytes, frozen: true)
|
|
402
|
+
Ractor.shareable?(frozen) # => true
|
|
403
|
+
Ractor.new(frozen) { |m| m.name.upcase }.take # => "ADA" (Ractor#value on Ruby 4.0)
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
Load your `_pb.rb` files in the main Ractor before starting others. `require "protostar"`
|
|
407
|
+
loads the binary codec and autoloads the rest on first use, and on Ruby 3.2 and 3.3
|
|
408
|
+
nothing can be loaded from another Ractor, not even by `autoload`, so any part of
|
|
409
|
+
protostar a Ractor will use has to be touched in the main Ractor first. Encoding,
|
|
410
|
+
decoding and views need nothing beyond the classes. A first `to_json` or `decode_json`
|
|
411
|
+
loads `Protostar::JSON`, `to_text` loads `Protostar::Text`, `build` loads
|
|
412
|
+
`Protostar::Builder`, a `to_proto` or `to_h` on a descriptor loads `descriptor.proto`'s
|
|
413
|
+
classes, and `Protostar::Timestamp` and the other well-known types load theirs. Ruby 3.4
|
|
414
|
+
lifts this.
|
|
415
|
+
|
|
416
|
+
### Reflection
|
|
417
|
+
|
|
418
|
+
Each class answers `descriptor` (a `Protostar::Schema::Message`, with every field, option
|
|
419
|
+
and resolved feature of the schema) and `reflection`, for code that handles messages it
|
|
420
|
+
does not know in advance:
|
|
421
|
+
|
|
422
|
+
```ruby
|
|
423
|
+
Acme::Person.reflection.fields.map(&:name)
|
|
424
|
+
# => ["name", "id", "emails", "labels", "address", "born_at", "phone", "pager"]
|
|
425
|
+
Acme::Person.reflection.get(person, :name) # => "Ada"
|
|
426
|
+
Acme::Person.descriptor.field("labels").map? # => true
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
[docs/schema-api.md](docs/schema-api.md) describes the schema objects, options and
|
|
430
|
+
reflection.
|
|
431
|
+
|
|
432
|
+
## Write bytes as you go: `Protostar::Builder`
|
|
433
|
+
|
|
434
|
+
When a response is large, building message objects only to encode them is waste. The
|
|
435
|
+
Builder writes the wire bytes as the block runs; nested messages and packed fields are
|
|
436
|
+
the only things it holds back.
|
|
437
|
+
|
|
438
|
+
```ruby
|
|
439
|
+
bytes = Protostar::Builder.build(Acme::Person) do |pb|
|
|
440
|
+
pb.name "Team"
|
|
441
|
+
pb.labels "kind" => "group"
|
|
442
|
+
pb.address do
|
|
443
|
+
pb.city "London"
|
|
444
|
+
end
|
|
445
|
+
end
|
|
446
|
+
Acme::Person.decode(bytes)
|
|
447
|
+
# => #<acme.Person name: "Team", labels: {"kind" => "group"}, address: #<acme.Address city: "London">>
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
Inside a nested block the same `pb` writes the nested message. A repeated message field
|
|
451
|
+
takes a collection and a block per element (`pb.friends(users) { |u| pb.name u.name }`),
|
|
452
|
+
an Array of Hashes, or messages. `Builder.write(io, Klass)`
|
|
453
|
+
streams to an IO, `Klass.build { |pb| ... }` returns a message, and `pb.cache!(key) { }`
|
|
454
|
+
caches a fragment's bytes in a Rails-style cache store. A singular field written twice
|
|
455
|
+
raises unless you pass `overwrites: true`; then the later value wins when read, which
|
|
456
|
+
relies on the reader applying protobuf's merge rules (every conforming one does).
|
|
457
|
+
Building 1,000 nested elements this way takes a third (YJIT) to a half of the time and a fifth of the
|
|
458
|
+
objects of `new` and `encode` (see the [benchmarks](docs/benchmarks.md)).
|
|
459
|
+
|
|
460
|
+
`Builder.write` takes any sink: an IO or socket, a Rack or `ActionController::Live` stream,
|
|
461
|
+
an Array, a Proc. Top-level fields go out through a 64 KiB buffer (`buffer: 0` writes each
|
|
462
|
+
one as it completes), and only one nested message per level is held, until it closes. For a
|
|
463
|
+
nested message too large to hold, `chunk_nested_at: 64 * 1024` writes it in pieces that
|
|
464
|
+
every reader merges back into one; that relies on the default `overwrites: false`, the
|
|
465
|
+
promise that no field is written twice. `Builder.each(io, Klass, rows) { |pb, row| ... }`
|
|
466
|
+
writes a length-prefixed sequence for `decode_delimited`, and `Builder.each_chunk(Klass) {
|
|
467
|
+
|pb| ... }` is an Enumerator of chunks for a Rack body.
|
|
468
|
+
[docs/concepts/streaming.md](docs/concepts/streaming.md) measures what streams and what has
|
|
469
|
+
to be held.
|
|
470
|
+
|
|
471
|
+
## The DSL, by hand
|
|
472
|
+
|
|
473
|
+
The generated files are plain Ruby. You can write one yourself:
|
|
474
|
+
|
|
475
|
+
```ruby
|
|
476
|
+
Protostar::DSL.file("shop/item.proto", package: "shop", syntax: :proto3) do |f|
|
|
477
|
+
f.message "Item" do |m|
|
|
478
|
+
m.field :sku, 1, :string
|
|
479
|
+
m.field :price_cents, 2, :int64, proto3_optional: true
|
|
480
|
+
m.field :tags, 3, :string, label: :repeated
|
|
481
|
+
m.map :stock, 4, :string, :int32
|
|
482
|
+
end
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
module Shop
|
|
486
|
+
Item = Protostar::DSL.message("shop.Item")
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
item = Shop::Item.new(sku: "lamp", stock: {"ams" => 3})
|
|
490
|
+
item.has_price_cents? # => false
|
|
491
|
+
Shop::Item.decode(item.encode) # => #<shop.Item sku: "lamp", stock: {"ams" => 3}>
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
Options are keywords (`deprecated: true`, `packed: false`, `features: {...}`), custom
|
|
495
|
+
options go under `extensions: {"acme.my_option" => value}`.
|
|
496
|
+
|
|
497
|
+
## Well-known types
|
|
498
|
+
|
|
499
|
+
`Timestamp`, `Duration`, `Any`, `Struct`, `Value`, `ListValue`, `Empty`, `FieldMask` and
|
|
500
|
+
the wrappers have short names under `Protostar`, loaded on first use, and the helpers
|
|
501
|
+
google-protobuf has:
|
|
502
|
+
|
|
503
|
+
```ruby
|
|
504
|
+
Protostar::Timestamp.from_time(Time.utc(2024, 3, 1))
|
|
505
|
+
# => #<google.protobuf.Timestamp seconds: 1709251200>
|
|
506
|
+
Protostar::Duration.new(seconds: 90).to_f # => 90.0
|
|
507
|
+
any = Protostar::Any.pack(item)
|
|
508
|
+
any.type_url # => "type.googleapis.com/shop.Item"
|
|
509
|
+
any.unpack(Shop::Item) == item # => true
|
|
510
|
+
Protostar::Struct.from_hash({"a" => 1, "b" => [true, nil]}).to_h
|
|
511
|
+
# => {"a" => 1.0, "b" => [true, nil]}
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
A Timestamp field takes a `Time` (as above, `person.born_at = Time.utc(...)`), a Duration
|
|
515
|
+
field takes a number of seconds.
|
|
516
|
+
|
|
517
|
+
## gRPC
|
|
518
|
+
|
|
519
|
+
`--grpc_out` writes the stubs grpc-tools would, and they work with protostar's classes:
|
|
520
|
+
|
|
521
|
+
```console
|
|
522
|
+
$ bundle exec protostar compile -I proto --ruby_out=lib --grpc_out=lib proto/acme/person.proto
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
```ruby
|
|
526
|
+
require "acme/person_services_pb"
|
|
527
|
+
|
|
528
|
+
class Directory < Acme::Directory::Service
|
|
529
|
+
def find(person, _call)
|
|
530
|
+
Acme::Person.new(name: person.name, id: 42)
|
|
531
|
+
end
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
server = GRPC::RpcServer.new
|
|
535
|
+
port = server.add_http2_port("127.0.0.1:0", :this_port_is_insecure)
|
|
536
|
+
server.handle(Directory)
|
|
537
|
+
Thread.new { server.run }
|
|
538
|
+
server.wait_till_running
|
|
539
|
+
|
|
540
|
+
stub = Acme::Directory::Stub.new("127.0.0.1:#{port}", :this_channel_is_insecure)
|
|
541
|
+
stub.find(Acme::Person.new(name: "Ada")) # => #<acme.Person name: "Ada", id: 42>
|
|
542
|
+
defined?(Google::Protobuf) # => nil
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
The `grpc` gem depends on `google-protobuf`, so Bundler installs it, but with protostar's
|
|
546
|
+
classes it is never loaded. Install the shim as that gem (below) and the C extension is
|
|
547
|
+
gone from the bundle too.
|
|
548
|
+
|
|
549
|
+
## Replacing google-protobuf
|
|
550
|
+
|
|
551
|
+
protostar ships a shim that provides Google's `Google::Protobuf` API on protostar, so
|
|
552
|
+
`grpc`, `googleapis-common-protos-types` and the `_pb.rb` files protoc generated run
|
|
553
|
+
unchanged. It is off until you ask for it. Require it before anything requires
|
|
554
|
+
`google/protobuf`:
|
|
555
|
+
|
|
556
|
+
```ruby
|
|
557
|
+
require "protostar/google_protobuf_shim"
|
|
558
|
+
require "grpc"
|
|
559
|
+
Google::Protobuf::IMPLEMENTATION # => :PROTOSTAR
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
From then on every `require "google/protobuf"` in the process loads the shim, even with
|
|
563
|
+
Google's gem installed alongside (grpc's dependency makes Bundler install it). If Google's
|
|
564
|
+
gem was loaded first, the require raises `LoadError`.
|
|
565
|
+
|
|
566
|
+
To keep Google's gem out of the bundle entirely, use the shim as a gem named
|
|
567
|
+
`google-protobuf`, from git. Every gem that depends on `google-protobuf` then gets it
|
|
568
|
+
instead of Google's:
|
|
569
|
+
|
|
570
|
+
```ruby
|
|
571
|
+
gem "protostar", github: "julik/protostar"
|
|
572
|
+
gem "google-protobuf", github: "julik/protostar", glob: "shims/google-protobuf/*.gemspec"
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
Its [README](shims/google-protobuf/README.md) has the other ways to install it, what it
|
|
576
|
+
covers, and how to check it is the one loaded. [docs/migration.md](docs/migration.md) walks
|
|
577
|
+
through moving an app or a gem from google-protobuf and protoc, either way.
|
|
578
|
+
|
|
579
|
+
## What is not supported
|
|
580
|
+
|
|
581
|
+
- **Extensions as fields.** Extension values are kept, byte for byte, as unknown fields,
|
|
582
|
+
and the text format, JSON and the shim's `FieldDescriptor#get`/`#set` read and write them
|
|
583
|
+
by name. There is no `msg.extension(...)` on protostar's own classes. This is also the one
|
|
584
|
+
conformance test protostar fails: UTF-8 checking of a string extension.
|
|
585
|
+
- **The legacy DSL.** `_pb.rb` files from protoc before 3.21 (`add_file`, `add_message`)
|
|
586
|
+
do not load, natively or through the shim. Regenerate them.
|
|
587
|
+
- **JSPB**, protobuf's JavaScript array format.
|
|
588
|
+
- The C++ and Java options (`cc_enable_arenas`, `java_package`, ...) are kept in the
|
|
589
|
+
descriptors and otherwise ignored, as protoc's Ruby generator ignores them.
|
|
590
|
+
|
|
591
|
+
## Performance
|
|
592
|
+
|
|
593
|
+
Pure Ruby is slower than C, and there is no hiding it. On the [benchmarks](docs/benchmarks.md)
|
|
594
|
+
(Ruby 3.4, Apple M1 Pro), decoding a populated 413-byte message takes 42 µs, 30 µs with
|
|
595
|
+
YJIT, where google-protobuf's C extension takes 3 µs; encoding it, 30 µs and 12 µs against
|
|
596
|
+
0.9 µs. A 50-byte message decodes in 2.5 µs and encodes in 3.6 µs with YJIT. Where
|
|
597
|
+
protostar does well: with YJIT, a view reads two fields of a 1 MiB message in 53 µs, five
|
|
598
|
+
times faster than the C gem decodes it; the Builder writes large responses without
|
|
599
|
+
building objects; and nothing has to compile at install time. Defining classes costs
|
|
600
|
+
about 1.1 ms per message class when a `_pb.rb` is loaded.
|
|
601
|
+
|
|
602
|
+
If protobuf encoding is your hot path, measure with your own messages
|
|
603
|
+
(`rake bench` shows how) before you switch.
|
|
604
|
+
|
|
605
|
+
## Contributing
|
|
606
|
+
|
|
607
|
+
```console
|
|
608
|
+
$ bundle install
|
|
609
|
+
$ bundle exec rake # the tests and standard
|
|
610
|
+
$ bundle exec rake shim:test # the shim's suite, with its own bundle
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
The tests check protostar against protoc's own output, not against what we think protoc
|
|
614
|
+
does:
|
|
615
|
+
|
|
616
|
+
- `test/corpus/protobuf/` is a slice of the protobuf repository at v36.2 (see `PIN`), BSD
|
|
617
|
+
licensed. `test/corpus/goldens/` holds, for each of its 107 `.proto` files, the
|
|
618
|
+
`FileDescriptorSet` protoc 36.2 wrote (`.binpb`, with source info) and the same as JSON.
|
|
619
|
+
The compiler must reproduce them exactly. Never edit either by hand: `rake corpus:fetch`
|
|
620
|
+
and `rake corpus:goldens` regenerate them (the latter needs protoc, which
|
|
621
|
+
`rake protoc:install` downloads into `~/.cache/protostar`). The tests themselves never
|
|
622
|
+
need protoc.
|
|
623
|
+
- `BUNDLE_WITH=oracle bundle install` and `PROTOSTAR_ORACLE=1 bundle exec rake test`
|
|
624
|
+
cross-check the codec and JSON against the C-extension gem. Without it those tests skip.
|
|
625
|
+
- `rake conformance:build` builds protobuf's conformance runner (CMake and a C++ compiler);
|
|
626
|
+
`rake conformance:run` runs it.
|
|
627
|
+
- `rake test:warnings` fails on any Ruby warning from `lib/`; `rake rbs:validate` checks
|
|
628
|
+
`sig/`.
|
|
629
|
+
- Generated files are committed and checked: `rake bootstrap:check`, `rake examples:check`,
|
|
630
|
+
`rake conformance:check`. After changing an emitter, run the matching `:regenerate`.
|
|
631
|
+
- `test/release/` builds both gems, installs them into an empty GEM_HOME and uses them
|
|
632
|
+
from a subprocess with no bundle.
|
|
633
|
+
- `rake test` runs the test files in a forked worker per core (`test/parallel.rb`);
|
|
634
|
+
`PARALLEL=0`, or `TEST=` naming one file, runs them in one process. Tests make temp
|
|
635
|
+
dirs with `tmpdir`, ports with `free_port`, and keep generated classes out of the
|
|
636
|
+
process-wide registry; `test/parallel/isolation_test.rb` checks that they do.
|
|
637
|
+
|
|
638
|
+
[docs/design.md](docs/design.md) is the map of the code; [docs/compiler-internals.md](docs/compiler-internals.md)
|
|
639
|
+
follows the compiler stage by stage. [examples/](examples/README.md) has four runnable
|
|
640
|
+
playbooks.
|
|
641
|
+
|
|
642
|
+
## Licence
|
|
643
|
+
|
|
644
|
+
MIT (`LICENSE.txt`). The Google `.proto` files bundled under
|
|
645
|
+
`lib/protostar/compiler/include/`, the code generated from them, and the vendored protobuf
|
|
646
|
+
sources under `test/corpus/` are Google's, under the BSD 3-Clause licence;
|
|
647
|
+
[THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md) lists them and carries that licence.
|