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
@@ -0,0 +1,503 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Protostar
4
+ class Compiler
5
+ module Emitters
6
+ # Writes protostar's own `_pb.rb` files: a {Protostar::DSL} block
7
+ # describing the file, then constants named as protoc's Ruby generator
8
+ # names them.
9
+ class Ruby
10
+ # source_code_info path components (descriptor.proto field numbers)
11
+ FILE_MESSAGE = 4
12
+ FILE_ENUM = 5
13
+ FILE_SERVICE = 6
14
+ FILE_EXTENSION = 7
15
+ MESSAGE_FIELD = 2
16
+ MESSAGE_NESTED = 3
17
+ MESSAGE_ENUM = 4
18
+ MESSAGE_EXTENSION = 6
19
+ MESSAGE_ONEOF = 8
20
+ ENUM_VALUE = 2
21
+ SERVICE_METHOD = 2
22
+
23
+ # @param namespaces [Hash{String => String}] Ruby module paths ("A::B") by file name, overriding
24
+ # the package-derived modules; the bootstrap puts google.protobuf under Protostar::Google::Protobuf
25
+ # @param bootstrap_requires [Boolean] require imports of {BOOTSTRAP} files as
26
+ # "protostar/google/protobuf/..._pb" rather than by their own names
27
+ def initialize(namespaces: {}, bootstrap_requires: true)
28
+ @namespaces = namespaces
29
+ @bootstrap_requires = bootstrap_requires
30
+ end
31
+
32
+ # @param files [Array<Schema::File>]
33
+ # @return [Hash{String => String}] output path to source
34
+ def emit(files)
35
+ files.to_h { |f| [Naming.require_name(f.name) + ".rb", file(f)] }
36
+ end
37
+
38
+ # @param file [Schema::File]
39
+ # @return [String] the generated source
40
+ def file(file)
41
+ Naming.check_ruby_package(file)
42
+ @file = file
43
+ @source = file.source_info
44
+ @out = +""
45
+ @out << "# frozen_string_literal: true\n\n"
46
+ @out << "# Generated by protostar from #{file.name}. Do not edit.\n\n"
47
+ @out << "require \"protostar\"\n"
48
+ file.dependencies.each { |dep| @out << "require #{Literal.string(require_path(dep))}\n" }
49
+ @out << "\n"
50
+ emit_file(file)
51
+ emit_constants(file)
52
+ @out
53
+ ensure
54
+ @file = @source = @reachable = nil
55
+ end
56
+
57
+ # Only the {Protostar::DSL.file} statement, without requires and constants
58
+ # @param file [Schema::File]
59
+ # @return [String]
60
+ def definition(file)
61
+ @file = file
62
+ @source = file.source_info
63
+ @out = +""
64
+ emit_file(file)
65
+ @out
66
+ ensure
67
+ @file = @source = @reachable = nil
68
+ end
69
+
70
+ # @param name [String] an imported file name
71
+ # @return [String] what a generated file requires for it
72
+ def require_path(name)
73
+ if @bootstrap_requires && BOOTSTRAP.include?(name)
74
+ "protostar/#{Naming.require_name(name)}"
75
+ else
76
+ Naming.require_name(name)
77
+ end
78
+ end
79
+
80
+ private
81
+
82
+ def emit_file(file)
83
+ args = [Literal.string(file.name)]
84
+ args << "package: #{Literal.string(file.package)}" unless file.package.empty?
85
+ if file.edition
86
+ args << "syntax: :editions" unless file.proto_syntax == "editions"
87
+ args << "edition: #{file.edition.inspect}"
88
+ elsif file.proto_syntax
89
+ args << "syntax: #{file.proto_syntax.to_sym.inspect}"
90
+ end
91
+ deps = file.dependencies
92
+ args << ["dependencies: ", deps] unless deps.empty?
93
+ [[:public_dependencies, file.public_dependencies], [:weak_dependencies, file.weak_dependencies]].each do |key, list|
94
+ args << ["#{key}: ", list.map { |i| deps[i] }] unless list.empty?
95
+ end
96
+ args << ["option_dependencies: ", file.option_dependencies] unless file.option_dependencies.empty?
97
+ args.concat(option_args(file.options))
98
+ body = +""
99
+ file.messages.each_with_index { |m, i| message(body, 1, "f", m, [FILE_MESSAGE, i]) }
100
+ file.enums.each_with_index { |e, i| enum(body, 1, "f", e, [FILE_ENUM, i]) }
101
+ extensions(body, 1, "f", file.extensions, [FILE_EXTENSION])
102
+ file.services.each_with_index { |s, i| service(body, 1, "f", s, [FILE_SERVICE, i]) }
103
+ if body.empty?
104
+ @out << call("Protostar::DSL.file", args, 0) << "\n"
105
+ else
106
+ @out << call("Protostar::DSL.file", args, 0) << " do |f|\n" << body << "end\n"
107
+ end
108
+ end
109
+
110
+ def message(out, depth, var, desc, path)
111
+ comment(out, depth, path)
112
+ args = [Literal.string(desc.name)]
113
+ args << "visibility: #{desc.visibility.inspect}" if desc.visibility
114
+ args.concat(option_args(desc.options))
115
+ head = call("#{var}.message", args, depth)
116
+ body = +""
117
+ message_body(body, depth + 1, desc, path)
118
+ out << ind(depth) << head << (body.empty? ? "\n" : " do |m|\n#{body}#{ind(depth)}end\n")
119
+ end
120
+
121
+ def message_body(out, depth, desc, path)
122
+ fields = desc.fields
123
+ nested = desc.nested_messages
124
+ oneofs = desc.oneofs
125
+ owned = owned_nested(desc, fields)
126
+ canonical = canonical_oneofs?(fields, oneofs)
127
+ unless canonical
128
+ oneofs.each do |o|
129
+ out << ind(depth) << call("m.oneof", [o.name.to_sym.inspect] + option_args(o.options), depth) << "\n"
130
+ end
131
+ end
132
+ items = field_items(fields, canonical)
133
+ emitted = {}
134
+ flush = lambda do |limit|
135
+ nested.each_index do |j|
136
+ next if j >= limit || emitted[j] || owned.value?(j)
137
+ emitted[j] = true
138
+ message(out, depth, "m", nested[j], path + [MESSAGE_NESTED, j])
139
+ end
140
+ end
141
+ items.each do |oneof_index, indexes|
142
+ owned_here = indexes.filter_map { |i| owned[i] }
143
+ flush.call(owned_here.max) unless owned_here.empty?
144
+ if oneof_index
145
+ comment(out, depth, path + [MESSAGE_ONEOF, oneof_index])
146
+ o = oneofs[oneof_index]
147
+ out << ind(depth) << call("m.oneof", [o.name.to_sym.inspect] + option_args(o.options), depth)
148
+ out << " do |o|\n"
149
+ indexes.each { |i| field(out, depth + 1, "o", desc, fields[i], owned[i], path + [MESSAGE_FIELD, i], false) }
150
+ out << ind(depth) << "end\n"
151
+ else
152
+ indexes.each { |i| field(out, depth, "m", desc, fields[i], owned[i], path + [MESSAGE_FIELD, i], !canonical) }
153
+ end
154
+ owned_here.each { |j| emitted[j] = true }
155
+ end
156
+ flush.call(nested.size)
157
+ desc.nested_enums.each_with_index { |e, i| enum(out, depth, "m", e, path + [MESSAGE_ENUM, i]) }
158
+ extensions(out, depth, "m", desc.extensions, path + [MESSAGE_EXTENSION])
159
+ desc.extension_range_descriptors.each do |r|
160
+ out << ind(depth) << call("m.extensions", [range(r.start, r.end)] + option_args(r.options), depth) << "\n"
161
+ end
162
+ reserved = desc.reserved_ranges.map { |r| range(r.begin, r.end) }
163
+ reserved.concat(desc.reserved_names.map { |n| Literal.string(n) })
164
+ out << ind(depth) << call("m.reserved", reserved, depth) << "\n" unless reserved.empty?
165
+ end
166
+
167
+ # Fields grouped for emission: [oneof index or nil, [field indexes]]
168
+ def field_items(fields, canonical)
169
+ return fields.each_index.map { |i| [nil, [i]] } unless canonical
170
+ items = []
171
+ fields.each_with_index do |f, i|
172
+ o = f.oneof&.index
173
+ if o && !f.proto3_optional?
174
+ if items.last && items.last[0] == o
175
+ items.last[1] << i
176
+ else
177
+ items << [o, [i]]
178
+ end
179
+ else
180
+ items << [nil, [i]]
181
+ end
182
+ end
183
+ items
184
+ end
185
+
186
+ # Whether the oneofs are laid out as the DSL's block form and synthetic
187
+ # oneofs reproduce them: real oneofs in order of their contiguous
188
+ # members, then one synthetic oneof per proto3 optional field, named
189
+ # as protoc's parser names them
190
+ def canonical_oneofs?(fields, oneofs)
191
+ return true if oneofs.empty?
192
+ real = []
193
+ synthetic = []
194
+ fields.each_with_index do |f, i|
195
+ next unless (o = f.oneof&.index)
196
+ if f.proto3_optional?
197
+ synthetic << [o, f]
198
+ else
199
+ return false if real.include?(o) && real.last != o
200
+ return false if real.include?(o) && fields[i - 1].oneof&.index != o
201
+ real << o unless real.include?(o)
202
+ end
203
+ end
204
+ return false unless real == (0...real.size).to_a
205
+ return false unless synthetic.map(&:first) == (real.size...oneofs.size).to_a
206
+ taken = fields.map(&:name) + oneofs.first(real.size).map(&:name)
207
+ names = Schema::Draft.synthetic_oneof_names(synthetic.map { |_, f| f.name }, taken)
208
+ synthetic.zip(names).all? do |(o, _), name|
209
+ oneofs[o].name == name && !oneofs[o].options.declared?
210
+ end
211
+ end
212
+
213
+ # Nested types written through `map` or `group`: field index to nested index
214
+ def owned_nested(desc, fields)
215
+ owned = {}
216
+ fields.each_with_index do |f, i|
217
+ nested = f.message_type
218
+ next unless nested && nested.containing_message.equal?(desc) && nested.file.equal?(desc.file)
219
+ owned[i] = nested.index if map_sugar(f, nested) || group_sugar?(f, nested)
220
+ end
221
+ # the sugar appends its nested type when called, so the order must agree
222
+ return {} unless owned.values == owned.values.sort
223
+ owned
224
+ end
225
+
226
+ # [key type, value type, value type name] when `m.map` makes exactly this entry
227
+ def map_sugar(f, entry)
228
+ return nil unless f.declared_label == :repeated && entry.name == Schema::Draft.map_entry_name(f.name)
229
+ return nil if f.raw_default || f.oneof || f.proto3_optional?
230
+ return nil unless only?(entry.options, map_entry: true)
231
+ return nil unless entry.nested_messages.empty? && entry.nested_enums.empty? && entry.extensions.empty? &&
232
+ entry.extension_range_descriptors.empty? && entry.oneofs.empty? && entry.reserved_ranges.empty? &&
233
+ entry.reserved_names.empty? && entry.visibility.nil?
234
+ key, value = entry.fields
235
+ return nil unless entry.fields.size == 2 && entry_field?(key, "key", 1) && entry_field?(value, "value", 2)
236
+ return nil if key.type_name
237
+ features = f.options.declared? ? f.options[:features] : nil
238
+ [key, value].each do |kv|
239
+ if features
240
+ return nil unless only?(kv.options, features: features)
241
+ elsif kv.options.declared?
242
+ return nil
243
+ end
244
+ end
245
+ [key.declared_type, value.declared_type, value.type_name && ".#{value.type_name}"]
246
+ end
247
+
248
+ def entry_field?(field, name, number)
249
+ field.name == name && field.number == number && field.declared_label == :optional &&
250
+ field.json_name == name && field.raw_default.nil? && field.oneof.nil? && !field.proto3_optional?
251
+ end
252
+
253
+ # Options declared with exactly these fields, no custom or unknown ones
254
+ def only?(options, **fields)
255
+ options.declared? && options.fields == fields && options.extensions.empty? && options.unknown_fields.empty?
256
+ end
257
+
258
+ def group_sugar?(f, nested)
259
+ f.declared_type == :group && f.name == Schema::Draft.group_field_name(nested.name) &&
260
+ !f.proto3_optional? && f.raw_default.nil?
261
+ end
262
+
263
+ def field(out, depth, var, desc, f, owned, path, explicit_oneof)
264
+ comment(out, depth, path)
265
+ nested = owned && desc.nested_messages[owned]
266
+ if nested && (sugar = map_sugar(f, nested))
267
+ key_type, value_type, value_type_name = sugar
268
+ args = [f.name.to_sym.inspect, f.number.to_s, key_type.inspect, value_type.inspect]
269
+ args << Literal.string(value_type_name) if value_type_name
270
+ args << "json_name: #{Literal.string(f.json_name)}" if custom_json_name?(f)
271
+ args.concat(option_args(f.options))
272
+ out << ind(depth) << call("#{var}.map", args, depth) << "\n"
273
+ return
274
+ end
275
+ if nested
276
+ args = [Literal.string(nested.name), f.number.to_s]
277
+ args << "label: #{f.declared_label.inspect}" unless f.declared_label == :optional
278
+ args << "json_name: #{Literal.string(f.json_name)}" if custom_json_name?(f)
279
+ args << "oneof: #{f.oneof.name.to_sym.inspect}" if f.oneof && explicit_oneof
280
+ args.concat(option_args(f.options))
281
+ args << ["message_options: ", dsl_hash(nested.options)] if nested.options.declared?
282
+ body = +""
283
+ message_body(body, depth + 1, nested, path_for_nested(owned, path))
284
+ head = call("#{var}.group", args, depth)
285
+ out << ind(depth) << head << (body.empty? ? "\n" : " do |m|\n#{body}#{ind(depth)}end\n")
286
+ return
287
+ end
288
+ args = field_args(f)
289
+ args << "proto3_optional: true" if f.proto3_optional?
290
+ args << "oneof: #{f.oneof.name.to_sym.inspect}" if f.oneof && explicit_oneof
291
+ args.concat(option_args(f.options))
292
+ out << ind(depth) << call("#{var}.field", args, depth) << "\n"
293
+ end
294
+
295
+ def path_for_nested(index, field_path)
296
+ field_path[0...-2] + [MESSAGE_NESTED, index]
297
+ end
298
+
299
+ def field_args(f)
300
+ args = [f.name.to_sym.inspect, f.number.to_s, f.declared_type.inspect]
301
+ args << Literal.string(".#{f.type_name}") if f.type_name
302
+ args << "label: #{f.declared_label.inspect}" unless f.declared_label == :optional
303
+ args << "default: #{Literal.string(f.raw_default)}" if f.raw_default
304
+ args << "json_name: #{Literal.string(f.json_name)}" if custom_json_name?(f)
305
+ args
306
+ end
307
+
308
+ def custom_json_name?(f)
309
+ f.json_name != Schema.json_name(f.name)
310
+ end
311
+
312
+ def extensions(out, depth, var, list, path)
313
+ i = 0
314
+ while i < list.size
315
+ extendee = list[i].extendee
316
+ out << ind(depth) << "#{var}.extend #{Literal.string(".#{extendee.full_name}")} do |e|\n"
317
+ while i < list.size && list[i].extendee.equal?(extendee)
318
+ f = list[i]
319
+ comment(out, depth + 1, path + [i])
320
+ args = field_args(f)
321
+ args << "proto3_optional: true" if f.proto3_optional?
322
+ args.concat(option_args(f.options))
323
+ out << ind(depth + 1) << call("e.field", args, depth + 1) << "\n"
324
+ i += 1
325
+ end
326
+ out << ind(depth) << "end\n"
327
+ end
328
+ end
329
+
330
+ def enum(out, depth, var, desc, path)
331
+ comment(out, depth, path)
332
+ args = [Literal.string(desc.name)]
333
+ args << "visibility: #{desc.visibility.inspect}" if desc.visibility
334
+ args.concat(option_args(desc.options))
335
+ out << ind(depth) << call("#{var}.enum", args, depth) << " do |e|\n"
336
+ desc.values.each_with_index do |v, i|
337
+ comment(out, depth + 1, path + [ENUM_VALUE, i])
338
+ args = [v.name.to_sym.inspect, v.number.to_s] + option_args(v.options)
339
+ out << ind(depth + 1) << call("e.value", args, depth + 1) << "\n"
340
+ end
341
+ reserved = desc.reserved_ranges.map { |r| range(r.begin, r.end) }
342
+ reserved.concat(desc.reserved_names.map { |n| Literal.string(n) })
343
+ out << ind(depth + 1) << call("e.reserved", reserved, depth + 1) << "\n" unless reserved.empty?
344
+ out << ind(depth) << "end\n"
345
+ end
346
+
347
+ def service(out, depth, var, desc, path)
348
+ comment(out, depth, path)
349
+ args = [Literal.string(desc.name)] + option_args(desc.options)
350
+ head = call("#{var}.service", args, depth)
351
+ if desc.rpcs.empty?
352
+ out << ind(depth) << head << "\n"
353
+ return
354
+ end
355
+ out << ind(depth) << head << " do |s|\n"
356
+ desc.rpcs.each_with_index do |m, i|
357
+ comment(out, depth + 1, path + [SERVICE_METHOD, i])
358
+ name = /\A[A-Za-z_][A-Za-z0-9_]*\z/.match?(m.name) ? m.name.to_sym.inspect : Literal.string(m.name)
359
+ args = [name, Literal.string(".#{m.input_type.full_name}"), Literal.string(".#{m.output_type.full_name}")]
360
+ args << "client_streaming: true" if m.client_streaming?
361
+ args << "server_streaming: true" if m.server_streaming?
362
+ args.concat(option_args(m.options))
363
+ out << ind(depth + 1) << call("s.rpc", args, depth + 1) << "\n"
364
+ end
365
+ out << ind(depth) << "end\n"
366
+ end
367
+
368
+ # Options as keywords, custom ones under `extensions:`; verbatim
369
+ # under `options:` when that cannot be said with keywords. Each is a
370
+ # `[prefix, value]` pair, rendered by {#call}.
371
+ def option_args(options)
372
+ return [] unless options.declared?
373
+ h = dsl_hash(options)
374
+ return [["options: ", h]] if h.empty? || h.key?(:options)
375
+ h.map { |key, value| ["#{key}: ", value] }
376
+ end
377
+
378
+ # An options message (or a message inside one) as the DSL spells it:
379
+ # fields by name, `extensions` by full name, `unknown_fields` as wire
380
+ # bytes; a field named like either of those two takes a String key. An
381
+ # extension the file reaches only through `import option` is written
382
+ # as unknown bytes, as protoc's own generator embeds it: the generated
383
+ # file does not load that import, so it may not be there to name the
384
+ # option.
385
+ def dsl_hash(value)
386
+ h = {}
387
+ value.fields.each { |k, v| h[RESERVED_KEYS.include?(k) ? k.to_s : k] = dsl_value(v) }
388
+ named, unnamed = value.extensions.partition { |f, _| reachable?(f) }
389
+ h[:extensions] = named.to_h { |f, v| [f.full_name, dsl_value(v)] } unless named.empty?
390
+ unknown = value.unknown_fields
391
+ unnamed.each { |f, v| unknown += Schema::MessageValue.new({}, {f => v}).encode(f.extendee) }
392
+ h[:unknown_fields] = unknown.b unless unknown.empty?
393
+ h
394
+ end
395
+
396
+ RESERVED_KEYS = %i[extensions unknown_fields].freeze
397
+
398
+ # Whether an extension is defined by the file or one it imports, so
399
+ # that loading the generated file can find it
400
+ def reachable?(extension)
401
+ @reachable ||= begin
402
+ names = {}
403
+ queue = [@file]
404
+ until queue.empty?
405
+ f = queue.shift
406
+ next if names[f.name]
407
+ names[f.name] = true
408
+ queue.concat(f.dependency_files)
409
+ end
410
+ names
411
+ end
412
+ @reachable.key?(extension.file.name)
413
+ end
414
+
415
+ def dsl_value(value)
416
+ case value
417
+ when Schema::MessageValue then dsl_hash(value)
418
+ when Array then value.map { |v| dsl_value(v) }
419
+ else value
420
+ end
421
+ end
422
+
423
+ # A call on one line when it fits. Otherwise the top-level call puts
424
+ # one argument per line inside parentheses, and calls in blocks keep
425
+ # their positional arguments on the first line and continue with one
426
+ # keyword per line. Arguments are source Strings or `[prefix, value]` pairs.
427
+ def call(receiver, args, depth)
428
+ flat = args.map { |a| a.is_a?(Array) ? a[0] + Literal.flat(a[1]) : a }
429
+ one = if args.empty?
430
+ receiver
431
+ elsif depth.positive?
432
+ "#{receiver} #{flat.join(", ")}"
433
+ else
434
+ "#{receiver}(#{flat.join(", ")})"
435
+ end
436
+ return one if depth * 2 + one.size <= Literal::WIDTH
437
+ inner = (depth + 1) * 2
438
+ render = lambda do |a|
439
+ a.is_a?(Array) ? a[0] + Literal.call(a[1], inner, inner + a[0].size) : a
440
+ end
441
+ if depth.zero?
442
+ return "#{receiver}(\n#{args.map { |a| " " * inner + render.call(a) }.join(",\n")}\n)"
443
+ end
444
+ positional, keywords = args.partition { |a| a.is_a?(String) && !KEYWORD.match?(a) }
445
+ return one if keywords.empty?
446
+ head = positional.empty? ? receiver : "#{receiver} #{positional.join(", ")}"
447
+ lines = keywords.map { |a| " " * inner + render.call(a) }
448
+ "#{head},\n#{lines.join(",\n")}"
449
+ end
450
+
451
+ KEYWORD = /\A[a-z_][a-z0-9_]*: /
452
+
453
+ def range(first, last)
454
+ (first == last) ? first.to_s : "#{first}..#{last}"
455
+ end
456
+
457
+ def comment(out, depth, path)
458
+ return unless @source
459
+ location = @source.location(path)
460
+ text = location&.leading_comments
461
+ return if text.nil? || text.empty?
462
+ lines = text.split("\n", -1)
463
+ lines.pop if lines.last == ""
464
+ lines.each do |line|
465
+ line = line.rstrip
466
+ out << ind(depth) << (line.empty? ? "#" : "##{line}") << "\n"
467
+ end
468
+ end
469
+
470
+ def ind(depth)
471
+ " " * depth
472
+ end
473
+
474
+ def emit_constants(file)
475
+ names = Naming.module_path(file, @namespaces[file.name])
476
+ lines = []
477
+ file.messages.each { |m| message_constants(lines, "", m) }
478
+ file.enums.each { |e| lines << enum_constant("", e) }
479
+ file.services.each do |s|
480
+ lines << "#{Naming.rubify_constant(s.name)} = Protostar::DSL.service(#{Literal.string(s.full_name)})"
481
+ end
482
+ return if lines.empty?
483
+ @out << "\n"
484
+ names.each_with_index { |n, i| @out << ind(i) << "module #{n}\n" }
485
+ lines.each { |l| @out << ind(names.size) << l << "\n" }
486
+ names.size.downto(1) { |i| @out << ind(i - 1) << "end\n" }
487
+ end
488
+
489
+ def message_constants(lines, prefix, message)
490
+ return if message.map_entry?
491
+ const = prefix + Naming.rubify_constant(message.name)
492
+ lines << "#{const} = Protostar::DSL.message(#{Literal.string(message.full_name)})"
493
+ message.nested_messages.each { |m| message_constants(lines, "#{const}::", m) }
494
+ message.nested_enums.each { |e| lines << enum_constant("#{const}::", e) }
495
+ end
496
+
497
+ def enum_constant(prefix, enum)
498
+ "#{prefix}#{Naming.rubify_constant(enum.name)} = Protostar::DSL.enum(#{Literal.string(enum.full_name)})"
499
+ end
500
+ end
501
+ end
502
+ end
503
+ end