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.
Files changed (223) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +60 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +647 -0
  5. data/THIRD_PARTY_NOTICES.md +61 -0
  6. data/exe/protoc-gen-protostar +7 -0
  7. data/exe/protostar +7 -0
  8. data/lib/protostar/builder/generator.rb +71 -0
  9. data/lib/protostar/builder/sink.rb +35 -0
  10. data/lib/protostar/builder.rb +965 -0
  11. data/lib/protostar/cli.rb +267 -0
  12. data/lib/protostar/codec/decode_generator.rb +199 -0
  13. data/lib/protostar/codec/decoder.rb +296 -0
  14. data/lib/protostar/codec/encode_generator.rb +224 -0
  15. data/lib/protostar/codec/encoder.rb +121 -0
  16. data/lib/protostar/codec/sizer.rb +91 -0
  17. data/lib/protostar/codec.rb +15 -0
  18. data/lib/protostar/compiler/ast.rb +193 -0
  19. data/lib/protostar/compiler/emitters/descriptor_set.rb +57 -0
  20. data/lib/protostar/compiler/emitters/google_ruby.rb +103 -0
  21. data/lib/protostar/compiler/emitters/grpc.rb +175 -0
  22. data/lib/protostar/compiler/emitters/protostar_rbs.rb +252 -0
  23. data/lib/protostar/compiler/emitters/rbs.rb +394 -0
  24. data/lib/protostar/compiler/emitters/ruby.rb +503 -0
  25. data/lib/protostar/compiler/emitters.rb +268 -0
  26. data/lib/protostar/compiler/include/LICENSE +32 -0
  27. data/lib/protostar/compiler/include/google/protobuf/any.proto +106 -0
  28. data/lib/protostar/compiler/include/google/protobuf/api.proto +229 -0
  29. data/lib/protostar/compiler/include/google/protobuf/compiler/plugin.proto +180 -0
  30. data/lib/protostar/compiler/include/google/protobuf/cpp_features.proto +87 -0
  31. data/lib/protostar/compiler/include/google/protobuf/descriptor.proto +1532 -0
  32. data/lib/protostar/compiler/include/google/protobuf/duration.proto +115 -0
  33. data/lib/protostar/compiler/include/google/protobuf/empty.proto +51 -0
  34. data/lib/protostar/compiler/include/google/protobuf/field_mask.proto +243 -0
  35. data/lib/protostar/compiler/include/google/protobuf/java_features.proto +132 -0
  36. data/lib/protostar/compiler/include/google/protobuf/source_context.proto +48 -0
  37. data/lib/protostar/compiler/include/google/protobuf/struct.proto +111 -0
  38. data/lib/protostar/compiler/include/google/protobuf/timestamp.proto +145 -0
  39. data/lib/protostar/compiler/include/google/protobuf/type.proto +217 -0
  40. data/lib/protostar/compiler/include/google/protobuf/wrappers.proto +157 -0
  41. data/lib/protostar/compiler/lexer.rb +659 -0
  42. data/lib/protostar/compiler/linker.rb +894 -0
  43. data/lib/protostar/compiler/option_interpreter.rb +363 -0
  44. data/lib/protostar/compiler/parser.rb +1696 -0
  45. data/lib/protostar/compiler/retention.rb +104 -0
  46. data/lib/protostar/compiler/run.rb +184 -0
  47. data/lib/protostar/compiler/source_info.rb +161 -0
  48. data/lib/protostar/compiler/symbol_table.rb +223 -0
  49. data/lib/protostar/compiler/text_format.rb +340 -0
  50. data/lib/protostar/compiler/validator.rb +944 -0
  51. data/lib/protostar/compiler.rb +304 -0
  52. data/lib/protostar/diagnostics/codes.rb +1307 -0
  53. data/lib/protostar/diagnostics/formats.rb +64 -0
  54. data/lib/protostar/diagnostics/renderer.rb +240 -0
  55. data/lib/protostar/diagnostics/source.rb +153 -0
  56. data/lib/protostar/diagnostics.rb +117 -0
  57. data/lib/protostar/dsl.rb +367 -0
  58. data/lib/protostar/enum.rb +70 -0
  59. data/lib/protostar/errors.rb +23 -0
  60. data/lib/protostar/google/protobuf/any_pb.rb +90 -0
  61. data/lib/protostar/google/protobuf/api_pb.rb +199 -0
  62. data/lib/protostar/google/protobuf/compiler/plugin_pb.rb +168 -0
  63. data/lib/protostar/google/protobuf/descriptor_pb.rb +1239 -0
  64. data/lib/protostar/google/protobuf/duration_pb.rb +99 -0
  65. data/lib/protostar/google/protobuf/empty_pb.rb +36 -0
  66. data/lib/protostar/google/protobuf/field_mask_pb.rb +228 -0
  67. data/lib/protostar/google/protobuf/source_context_pb.rb +33 -0
  68. data/lib/protostar/google/protobuf/struct_pb.rb +91 -0
  69. data/lib/protostar/google/protobuf/timestamp_pb.rb +129 -0
  70. data/lib/protostar/google/protobuf/type_pb.rb +202 -0
  71. data/lib/protostar/google/protobuf/wrappers_pb.rb +125 -0
  72. data/lib/protostar/google_protobuf_shim.rb +24 -0
  73. data/lib/protostar/json/encoder.rb +340 -0
  74. data/lib/protostar/json/errors.rb +34 -0
  75. data/lib/protostar/json/parser.rb +437 -0
  76. data/lib/protostar/json/path.rb +39 -0
  77. data/lib/protostar/json.rb +95 -0
  78. data/lib/protostar/lint/command.rb +197 -0
  79. data/lib/protostar/lint/config.rb +98 -0
  80. data/lib/protostar/lint/context.rb +126 -0
  81. data/lib/protostar/lint/directives.rb +56 -0
  82. data/lib/protostar/lint/fixer.rb +165 -0
  83. data/lib/protostar/lint/naming.rb +54 -0
  84. data/lib/protostar/lint/rule.rb +120 -0
  85. data/lib/protostar/lint/rules/comment_enum.rb +31 -0
  86. data/lib/protostar/lint/rules/comment_enum_value.rb +32 -0
  87. data/lib/protostar/lint/rules/comment_field.rb +33 -0
  88. data/lib/protostar/lint/rules/comment_message.rb +31 -0
  89. data/lib/protostar/lint/rules/comment_rpc.rb +31 -0
  90. data/lib/protostar/lint/rules/comment_service.rb +31 -0
  91. data/lib/protostar/lint/rules/edition_recommended.rb +31 -0
  92. data/lib/protostar/lint/rules/enum_first_value_zero.rb +34 -0
  93. data/lib/protostar/lint/rules/enum_pascal_case.rb +25 -0
  94. data/lib/protostar/lint/rules/enum_value_prefix.rb +38 -0
  95. data/lib/protostar/lint/rules/enum_value_upper_snake_case.rb +28 -0
  96. data/lib/protostar/lint/rules/enum_zero_value_suffix.rb +41 -0
  97. data/lib/protostar/lint/rules/field_lower_snake_case.rb +34 -0
  98. data/lib/protostar/lint/rules/field_not_required.rb +39 -0
  99. data/lib/protostar/lint/rules/field_number_reuse_risk.rb +45 -0
  100. data/lib/protostar/lint/rules/field_numbers_hot_range.rb +44 -0
  101. data/lib/protostar/lint/rules/file_lower_snake_case.rb +27 -0
  102. data/lib/protostar/lint/rules/import_no_public.rb +29 -0
  103. data/lib/protostar/lint/rules/import_no_weak.rb +28 -0
  104. data/lib/protostar/lint/rules/import_used.rb +33 -0
  105. data/lib/protostar/lint/rules/message_pascal_case.rb +27 -0
  106. data/lib/protostar/lint/rules/no_deprecated_features.rb +50 -0
  107. data/lib/protostar/lint/rules/no_groups.rb +34 -0
  108. data/lib/protostar/lint/rules/oneof_lower_snake_case.rb +27 -0
  109. data/lib/protostar/lint/rules/package_defined.rb +28 -0
  110. data/lib/protostar/lint/rules/package_directory_match.rb +33 -0
  111. data/lib/protostar/lint/rules/package_lower_snake_case.rb +28 -0
  112. data/lib/protostar/lint/rules/package_same_directory.rb +39 -0
  113. data/lib/protostar/lint/rules/package_version_suffix.rb +31 -0
  114. data/lib/protostar/lint/rules/reserved_names_with_numbers.rb +44 -0
  115. data/lib/protostar/lint/rules/rpc_pascal_case.rb +25 -0
  116. data/lib/protostar/lint/rules/rpc_request_response_unique.rb +40 -0
  117. data/lib/protostar/lint/rules/rpc_request_standard_name.rb +32 -0
  118. data/lib/protostar/lint/rules/rpc_response_standard_name.rb +31 -0
  119. data/lib/protostar/lint/rules/service_pascal_case.rb +25 -0
  120. data/lib/protostar/lint/rules/service_suffix.rb +28 -0
  121. data/lib/protostar/lint/rules/syntax_specified.rb +35 -0
  122. data/lib/protostar/lint/runner.rb +135 -0
  123. data/lib/protostar/lint.rb +122 -0
  124. data/lib/protostar/message/accessors.rb +86 -0
  125. data/lib/protostar/message/coercion.rb +171 -0
  126. data/lib/protostar/message/field_info.rb +145 -0
  127. data/lib/protostar/message/reflection.rb +149 -0
  128. data/lib/protostar/message.rb +572 -0
  129. data/lib/protostar/plugin.rb +94 -0
  130. data/lib/protostar/schema/default_value.rb +113 -0
  131. data/lib/protostar/schema/descriptor.rb +41 -0
  132. data/lib/protostar/schema/draft/copy.rb +88 -0
  133. data/lib/protostar/schema/draft/resolution.rb +177 -0
  134. data/lib/protostar/schema/draft.rb +779 -0
  135. data/lib/protostar/schema/enum.rb +106 -0
  136. data/lib/protostar/schema/extension_range.rb +37 -0
  137. data/lib/protostar/schema/features.rb +153 -0
  138. data/lib/protostar/schema/field.rb +201 -0
  139. data/lib/protostar/schema/file.rb +217 -0
  140. data/lib/protostar/schema/freezer.rb +61 -0
  141. data/lib/protostar/schema/message.rb +104 -0
  142. data/lib/protostar/schema/oneof.rb +38 -0
  143. data/lib/protostar/schema/options.rb +367 -0
  144. data/lib/protostar/schema/pool.rb +179 -0
  145. data/lib/protostar/schema/proto.rb +576 -0
  146. data/lib/protostar/schema/resolver.rb +118 -0
  147. data/lib/protostar/schema/service.rb +89 -0
  148. data/lib/protostar/schema/source_info.rb +53 -0
  149. data/lib/protostar/schema.rb +65 -0
  150. data/lib/protostar/short_names/any.rb +7 -0
  151. data/lib/protostar/short_names/duration.rb +7 -0
  152. data/lib/protostar/short_names/empty.rb +7 -0
  153. data/lib/protostar/short_names/field_mask.rb +7 -0
  154. data/lib/protostar/short_names/struct.rb +9 -0
  155. data/lib/protostar/short_names/timestamp.rb +7 -0
  156. data/lib/protostar/short_names/wrappers.rb +15 -0
  157. data/lib/protostar/text/extensions.rb +114 -0
  158. data/lib/protostar/text/parser.rb +439 -0
  159. data/lib/protostar/text/printer.rb +304 -0
  160. data/lib/protostar/text/tokenizer.rb +116 -0
  161. data/lib/protostar/text.rb +67 -0
  162. data/lib/protostar/types.rb +152 -0
  163. data/lib/protostar/unknown_field.rb +58 -0
  164. data/lib/protostar/version.rb +5 -0
  165. data/lib/protostar/view/generator.rb +298 -0
  166. data/lib/protostar/view/map.rb +194 -0
  167. data/lib/protostar/view/repeated.rb +143 -0
  168. data/lib/protostar/view/scan.rb +183 -0
  169. data/lib/protostar/view.rb +283 -0
  170. data/lib/protostar/well_known/any.rb +52 -0
  171. data/lib/protostar/well_known/duration.rb +53 -0
  172. data/lib/protostar/well_known/list_value.rb +63 -0
  173. data/lib/protostar/well_known/struct.rb +55 -0
  174. data/lib/protostar/well_known/timestamp.rb +64 -0
  175. data/lib/protostar/well_known/value.rb +60 -0
  176. data/lib/protostar/well_known.rb +38 -0
  177. data/lib/protostar/wire/io_source.rb +40 -0
  178. data/lib/protostar/wire/reader.rb +254 -0
  179. data/lib/protostar/wire/writer.rb +201 -0
  180. data/lib/protostar/wire.rb +96 -0
  181. data/lib/protostar.rb +95 -0
  182. data/shims/google-protobuf/lib/google/protobuf/any_pb.rb +17 -0
  183. data/shims/google-protobuf/lib/google/protobuf/api_pb.rb +22 -0
  184. data/shims/google-protobuf/lib/google/protobuf/compiler/plugin_pb.rb +25 -0
  185. data/shims/google-protobuf/lib/google/protobuf/descriptor_pb.rb +72 -0
  186. data/shims/google-protobuf/lib/google/protobuf/descriptor_pool.rb +164 -0
  187. data/shims/google-protobuf/lib/google/protobuf/descriptors.rb +553 -0
  188. data/shims/google-protobuf/lib/google/protobuf/duration_pb.rb +17 -0
  189. data/shims/google-protobuf/lib/google/protobuf/empty_pb.rb +17 -0
  190. data/shims/google-protobuf/lib/google/protobuf/field_mask_pb.rb +17 -0
  191. data/shims/google-protobuf/lib/google/protobuf/internal/convert.rb +180 -0
  192. data/shims/google-protobuf/lib/google/protobuf/internal/extensions.rb +154 -0
  193. data/shims/google-protobuf/lib/google/protobuf/internal/json.rb +104 -0
  194. data/shims/google-protobuf/lib/google/protobuf/internal/well_known.rb +140 -0
  195. data/shims/google-protobuf/lib/google/protobuf/map.rb +216 -0
  196. data/shims/google-protobuf/lib/google/protobuf/message.rb +544 -0
  197. data/shims/google-protobuf/lib/google/protobuf/message_exts.rb +41 -0
  198. data/shims/google-protobuf/lib/google/protobuf/plugin_pb.rb +4 -0
  199. data/shims/google-protobuf/lib/google/protobuf/repeated_field.rb +366 -0
  200. data/shims/google-protobuf/lib/google/protobuf/source_context_pb.rb +17 -0
  201. data/shims/google-protobuf/lib/google/protobuf/struct_pb.rb +20 -0
  202. data/shims/google-protobuf/lib/google/protobuf/timestamp_pb.rb +17 -0
  203. data/shims/google-protobuf/lib/google/protobuf/type_pb.rb +27 -0
  204. data/shims/google-protobuf/lib/google/protobuf/well_known_types.rb +37 -0
  205. data/shims/google-protobuf/lib/google/protobuf/wrappers_pb.rb +25 -0
  206. data/shims/google-protobuf/lib/google/protobuf.rb +107 -0
  207. data/shims/google-protobuf/lib/google/protobuf_ffi.rb +5 -0
  208. data/shims/google-protobuf/lib/google/protobuf_native.rb +5 -0
  209. data/sig/protostar/google/protobuf/any_pb.rbs +77 -0
  210. data/sig/protostar/google/protobuf/api_pb.rbs +242 -0
  211. data/sig/protostar/google/protobuf/compiler/plugin_pb.rbs +264 -0
  212. data/sig/protostar/google/protobuf/descriptor_pb.rbs +2220 -0
  213. data/sig/protostar/google/protobuf/duration_pb.rbs +82 -0
  214. data/sig/protostar/google/protobuf/empty_pb.rbs +18 -0
  215. data/sig/protostar/google/protobuf/field_mask_pb.rbs +170 -0
  216. data/sig/protostar/google/protobuf/source_context_pb.rbs +24 -0
  217. data/sig/protostar/google/protobuf/struct_pb.rbs +135 -0
  218. data/sig/protostar/google/protobuf/timestamp_pb.rbs +102 -0
  219. data/sig/protostar/google/protobuf/type_pb.rbs +304 -0
  220. data/sig/protostar/google/protobuf/wrappers_pb.rbs +169 -0
  221. data/sig/protostar/lint.rbs +216 -0
  222. data/sig/protostar.rbs +326 -0
  223. 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.