protobug-compiler 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 6c3c9d32d7ba7357c9bda140e4b67634871c4ea151c0a35684f9a6dd9030f53a
4
+ data.tar.gz: 58b29936acaffa8906497d83488d4f9bf2a46d3ff4418f6680905c688ed84209
5
+ SHA512:
6
+ metadata.gz: 33ad398f7e589f6cfb6b1a67473dd28d5c81d3bf5714b7f4cbc31a0fb2524dfa953babbea5853c2772b5dcb2bce9849fee35ccb089b4743f205cdb7455932af4
7
+ data.tar.gz: 034d83ffaa55fb291437704742fe62185584e18c451b2346fbacb5f830105afe10297226f063b2ac8e7a25d9d8788fb2f4c9837622a528211933bf9c390971a8
data/CHANGELOG.md ADDED
@@ -0,0 +1,7 @@
1
+ # protobug-compiler Changelog
2
+
3
+ ## [Unreleased]
4
+
5
+ ## [0.1.0] - 2024-03-06
6
+
7
+ - Initial release
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2024 Samuel Giddins
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "protobug_compiler_protos"
5
+ require "protobug/compiler"
6
+
7
+ compiler_registry = Protobug::Registry.new do |registry|
8
+ Google::Protobuf::Compiler.register_plugin_protos(registry)
9
+ end
10
+
11
+ request = Google::Protobuf::Compiler::CodeGeneratorRequest.decode($stdin, registry: compiler_registry)
12
+ SimpleCov.command_name "protoc-gen-protobug:#{Digest::SHA256.hexdigest(request.to_json)}" if defined?(SimpleCov)
13
+
14
+ response = Protobug::Compiler.compile!(request)
15
+
16
+ $stdout.print response.class.encode(response)
@@ -0,0 +1,260 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "delegate"
4
+ require "forwardable"
5
+ require "prettier_print"
6
+
7
+ module Protobug
8
+ class Compiler
9
+ module Builder
10
+ def self.build_file
11
+ builder = File.new
12
+ if block_given?
13
+ yield builder
14
+ builder.render
15
+ else
16
+ builder
17
+ end
18
+ end
19
+
20
+ class Statement
21
+ attr_reader :items
22
+
23
+ def initialize
24
+ @items = []
25
+ end
26
+
27
+ def comment(text)
28
+ append Comment.new(text)
29
+ end
30
+
31
+ def empty
32
+ append Token.new(type: :empty, content: "")
33
+ end
34
+
35
+ def identifier(name)
36
+ append Token.new(type: :identifier, content: name)
37
+ end
38
+
39
+ def literal(content)
40
+ append Token.new(type: :literal, content: content)
41
+ end
42
+
43
+ def block(&blk)
44
+ append Group.new(
45
+ name: "block", items: [], close: "end", multi: true, indent: 2
46
+ ).tap(&blk)
47
+ end
48
+
49
+ def dot(name)
50
+ append(Token.new(type: :delimiter, content: ".")).identifier(name)
51
+ end
52
+
53
+ def op(operator)
54
+ append Token.new(type: :operator, content: operator)
55
+ end
56
+
57
+ def render(q) # rubocop:disable Naming/MethodParameterName
58
+ first = true
59
+ prev = nil
60
+ q.group do
61
+ items.each do |item|
62
+ next if item.empty?
63
+
64
+ if !first && !item.compact? && !prev&.compact?
65
+ if (item.is_a?(Token) && item.type == :operator) || (prev.is_a?(Token) && prev.type == :operator) ||
66
+ item.is_a?(Comment)
67
+ q.text " "
68
+ elsif !(item.is_a?(Group) && item.name == :call) && !(item.is_a?(Group) && item.multi)
69
+ q.fill_breakable " "
70
+ end
71
+ end
72
+ first = false unless item.compact? || prev&.compact?
73
+ prev = item
74
+
75
+ item.render(q)
76
+ end
77
+ end
78
+ end
79
+
80
+ def append(item)
81
+ items << item
82
+ self
83
+ end
84
+
85
+ def empty?
86
+ items.none? { |item| !item.empty? }
87
+ end
88
+
89
+ def compact? = false
90
+ end
91
+
92
+ Group = Struct.new(:name, :items, :open, :close, :separator, :multi, :indent,
93
+ keyword_init: true) do
94
+ include Builder
95
+
96
+ def comment(text)
97
+ append Statement.new.comment(text)
98
+ end
99
+
100
+ def empty
101
+ append Statement.new.empty
102
+ end
103
+
104
+ def block(&blk)
105
+ append Statement.new.block(&blk)
106
+ end
107
+
108
+ def identifier(name)
109
+ append Statement.new.identifier(name)
110
+ end
111
+
112
+ def literal(content)
113
+ append Statement.new.literal(content)
114
+ end
115
+
116
+ def op(operator)
117
+ append Statement.new.op(operator)
118
+ end
119
+
120
+ def render(q) # rubocop:disable Naming/MethodParameterName
121
+ if name == "block" && empty?
122
+ q.breakable "; "
123
+ q.text "end"
124
+ end
125
+
126
+ q.group do
127
+ q.text open if open
128
+ q.nest(indent || 0) do
129
+ write_items(q)
130
+ end
131
+ return unless close
132
+
133
+ multi ? q.breakable_force : q.breakable_empty
134
+ q.text close
135
+ end
136
+ end
137
+
138
+ def append(item)
139
+ items << item
140
+ item
141
+ end
142
+
143
+ def empty?
144
+ return false if name != :call && (open || close)
145
+
146
+ items.none? { |item| !item.empty? }
147
+ end
148
+
149
+ def compact? = false
150
+
151
+ private
152
+
153
+ def write_items(q) # rubocop:disable Naming/MethodParameterName
154
+ multi ? q.breakable_force : q.breakable_empty
155
+ q.seplist(items.reject(&:empty?), lambda {
156
+ if separator == "\n" || multi
157
+ q.breakable_force
158
+ elsif separator
159
+ q.text separator.rstrip
160
+ if separator.end_with?(" ")
161
+ q.breakable
162
+ else
163
+ q.breakable_empty
164
+ end
165
+ end
166
+ }, :each) do |item|
167
+ item.render(q)
168
+ end
169
+ end
170
+ end
171
+
172
+ class File < DelegateClass(Group)
173
+ def initialize
174
+ @headers = []
175
+ super(Group.new(name: "file", items: [], multi: true))
176
+ end
177
+
178
+ def header_comment(text)
179
+ @headers << text
180
+ end
181
+
182
+ def render
183
+ q = PrettierPrint.new(+"")
184
+ @headers.each_with_index do |header, idx|
185
+ q.breakable_force if idx.positive?
186
+ Comment.new(header).render(q)
187
+ q.breakable_force
188
+ end
189
+ super(q)
190
+ q.trim
191
+ q.breakable_force
192
+ q.flush
193
+ q.output
194
+ end
195
+ end
196
+
197
+ Comment = Struct.new(:comment) do
198
+ def render(q) # rubocop:disable Naming/MethodParameterName
199
+ if comment.start_with?("#")
200
+ q.text(comment)
201
+ else
202
+ prefixed = comment.start_with?(" ")
203
+
204
+ q.group do
205
+ q.break_parent
206
+ q.seplist(
207
+ comment.chomp.gsub(/^/m, prefixed ? "#" : "# ").tap do |s|
208
+ s.gsub!(/^#\s*$/m, "#")
209
+ end.each_line(chomp: true),
210
+ -> { q.breakable_empty }
211
+ ) do |line|
212
+ q.text line
213
+ end
214
+ end
215
+ end
216
+ end
217
+
218
+ def empty? = false
219
+ def compact? = false
220
+ end
221
+
222
+ Token = Struct.new(:type, :content,
223
+ keyword_init: true) do
224
+ def render(q) # rubocop:disable Naming/MethodParameterName
225
+ case type
226
+ when :empty, :keyword, :identifier, :delimiter, :operator
227
+ q.text content
228
+ when :literal
229
+ case content
230
+ when Integer
231
+ int_part = Integer(content)
232
+ formatted_int = int_part.abs.to_s.reverse.gsub(/...(?=.)/, '\&_').reverse
233
+ formatted_int.insert(0, "-") if int_part.negative?
234
+ q.text formatted_int
235
+ else
236
+ q.text content.inspect
237
+ end
238
+ else
239
+ raise "Unknown token type: #{type}"
240
+ end
241
+ end
242
+
243
+ def empty? = false
244
+
245
+ COMPACT_OPERATORS = %w[** .. ...].freeze # rubocop:disable Lint/ConstantDefinitionInBlock
246
+
247
+ def compact?
248
+ case type
249
+ when :operator
250
+ COMPACT_OPERATORS.include?(content)
251
+ when :delimiter
252
+ content == "."
253
+ end
254
+ end
255
+ end
256
+ end
257
+ end
258
+ end
259
+
260
+ require_relative "builder_gen"
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Protobug
4
+ class Compiler
5
+ module Builder
6
+ class Statement
7
+ def _class
8
+ append Token.new(type: :keyword, content: "class")
9
+ end
10
+
11
+ def _def
12
+ append Token.new(type: :keyword, content: "def")
13
+ end
14
+
15
+ def _module
16
+ append Token.new(type: :keyword, content: "module")
17
+ end
18
+
19
+ def parens(item, &blk)
20
+ append Group.new(
21
+ name: :parens,
22
+ items: [item],
23
+ open: "(",
24
+ close: ")",
25
+ indent: 2
26
+ ).tap(&blk)
27
+ end
28
+
29
+ def list(*items, &blk)
30
+ append Group.new(
31
+ name: :list,
32
+ items: items,
33
+ separator: ",",
34
+ indent: 2
35
+ ).tap(&blk)
36
+ end
37
+
38
+ def index(*items, &blk)
39
+ append Group.new(
40
+ name: :index,
41
+ items: items,
42
+ open: "[",
43
+ close: "]",
44
+ separator: ","
45
+ ).tap(&blk)
46
+ end
47
+
48
+ def call(*args, &blk)
49
+ append Group.new(
50
+ name: :call,
51
+ items: args,
52
+ open: "(",
53
+ close: ")",
54
+ separator: ", ",
55
+ indent: 2
56
+ ).tap(&blk)
57
+ end
58
+ end
59
+
60
+ class Group
61
+ def _class
62
+ append Statement.new._class
63
+ end
64
+
65
+ def _def
66
+ append Statement.new._def
67
+ end
68
+
69
+ def _module
70
+ append Statement.new._module
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,500 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "protobug_compiler_protos"
4
+
5
+ require_relative "compiler/builder"
6
+
7
+ module Protobug
8
+ class Compiler
9
+ def self.compile!(request)
10
+ response = Google::Protobuf::Compiler::CodeGeneratorResponse.new
11
+ begin
12
+ new(request, response).compile!
13
+ rescue StandardError => e
14
+ response.error = e.full_message
15
+ end
16
+ response
17
+ end
18
+
19
+ def initialize(request, response)
20
+ @request = request
21
+ @response = response
22
+ end
23
+
24
+ attr_reader :request, :response, :files
25
+
26
+ class Files
27
+ def initialize
28
+ @descs_by_name = {}
29
+ @files_by_path = {}
30
+ @num_files = 0
31
+ end
32
+
33
+ def register_file(file)
34
+ @files_by_path[file.name] = register_decl(FileDescriptorProto.new(file, nil, path: []))
35
+ end
36
+
37
+ def fetch(name)
38
+ @files_by_path.fetch(name)
39
+ end
40
+
41
+ def fetch_type(name)
42
+ @descs_by_name.fetch(name)
43
+ end
44
+
45
+ def register_decl(decl)
46
+ raise ArgumentError, "parent must be a Descriptor" if decl.parent && !decl.parent.is_a?(Descriptor)
47
+
48
+ unless decl.is_a?(FileDescriptorProto)
49
+ raise "already registered: #{decl.full_name}" if @descs_by_name[decl.full_name]
50
+
51
+ @descs_by_name[decl.full_name] = decl
52
+ end
53
+
54
+ decl.each_declaration do |d|
55
+ register_decl(d)
56
+ end
57
+ decl
58
+ end
59
+ end
60
+
61
+ module Descriptor
62
+ attr_reader :descriptor, :parent, :file, :source_loc
63
+
64
+ def initialize(descriptor, parent, path:)
65
+ unless descriptor.is_a?(self.class.descriptor_class)
66
+ raise ArgumentError,
67
+ "#{descriptor.class} (#{descriptor}) is not #{self.class.descriptor_class}"
68
+ end
69
+
70
+ raise ArgumentError, "parent must be a Descriptor" if parent && !parent.is_a?(Descriptor)
71
+
72
+ @descriptor = descriptor
73
+ @parent = parent
74
+ @file = parent&.file || self
75
+ @path = path
76
+
77
+ super(descriptor)
78
+
79
+ @source_loc = if parent
80
+ file.source_code_info.location.find do |loc|
81
+ loc.path == path
82
+ end
83
+ else
84
+ source_code_info.location[1]
85
+ end
86
+ end
87
+
88
+ def full_name
89
+ if parent
90
+ "#{parent.full_name}.#{name}"
91
+ else
92
+ descriptor.package
93
+ end
94
+ end
95
+
96
+ def to_constant
97
+ if parent
98
+ "#{parent.to_constant}::#{name}"
99
+ else
100
+ return file.options.ruby_package if file.options&.ruby_package?
101
+
102
+ parts = descriptor.package.split(".")
103
+ parts.map! do |part|
104
+ part.split("_").map(&:capitalize).join
105
+ end
106
+ parts.join("::")
107
+ end
108
+ end
109
+
110
+ def self.included(base)
111
+ base.extend(ClassMethods)
112
+ base.class_eval do
113
+ methods = []
114
+
115
+ fields_by_name.each do |name, field|
116
+ unless field.is_a?(Field::MessageField) &&
117
+ /\Agoogle\.protobuf\.([^.]*Descriptor[^.]*)\z/ =~ field.message_type
118
+ next
119
+ end
120
+ raise "expected #{self}.#{name} to be repeated" unless field.repeated?
121
+
122
+ message_name = Regexp.last_match(1)
123
+
124
+ methods << name
125
+
126
+ define_method(name) do
127
+ field_type = Compiler.const_get(message_name)
128
+ super().map.with_index { |d, idx| field_type.new(d, self, path: @path + [field.number, idx]) }
129
+ end
130
+ end
131
+
132
+ define_method(:each_declaration) do |&blk|
133
+ return enum_for(__method__) unless blk # rubocop:disable Lint/ToEnumArguments
134
+
135
+ decls = methods.flat_map { send(_1) }
136
+ decls.sort_by! { |d| d.source_loc&.span || [] }
137
+ decls.each(&blk)
138
+ nil
139
+ end
140
+ end
141
+ end
142
+
143
+ module ClassMethods
144
+ def descriptor_class
145
+ Google::Protobuf.const_get(name.split("::").last)
146
+ end
147
+
148
+ def fields_by_number = descriptor_class.fields_by_number
149
+ def fields_by_name = descriptor_class.fields_by_name
150
+ end
151
+ end
152
+
153
+ class FileDescriptorProto < DelegateClass(Google::Protobuf::FileDescriptorProto)
154
+ include Descriptor
155
+
156
+ def file_name
157
+ ruby_package = options.ruby_package if options&.ruby_package?
158
+ prefix = if ruby_package
159
+ ruby_package.split("::").map!(&:downcase).join("/")
160
+ else
161
+ package.split(".").map!(&:downcase).join("/")
162
+ end
163
+ name.gsub(%r{^.*?([^/]+)\.proto$}, "#{prefix}/\\1_pb.rb")
164
+ end
165
+
166
+ def loc_by_path(path)
167
+ source_code_info.location.find { |loc| loc.path == path }
168
+ end
169
+
170
+ attr_accessor :file_to_generate
171
+ end
172
+
173
+ class DescriptorProto < DelegateClass(Google::Protobuf::DescriptorProto)
174
+ include Descriptor
175
+ end
176
+
177
+ class FieldDescriptorProto < DelegateClass(Google::Protobuf::FieldDescriptorProto)
178
+ include Descriptor
179
+ end
180
+
181
+ class EnumDescriptorProto < DelegateClass(Google::Protobuf::EnumDescriptorProto)
182
+ include Descriptor
183
+ end
184
+
185
+ class EnumValueDescriptorProto < DelegateClass(Google::Protobuf::EnumValueDescriptorProto)
186
+ include Descriptor
187
+ end
188
+
189
+ class ServiceDescriptorProto < DelegateClass(Google::Protobuf::ServiceDescriptorProto)
190
+ include Descriptor
191
+ end
192
+
193
+ class OneofDescriptorProto < DelegateClass(Google::Protobuf::OneofDescriptorProto)
194
+ include Descriptor
195
+ end
196
+
197
+ def compile!
198
+ response.supported_features |=
199
+ Google::Protobuf::Compiler::CodeGeneratorResponse::Feature::FEATURE_PROTO3_OPTIONAL.value
200
+ @files = Files.new
201
+
202
+ request.proto_file.each do |file|
203
+ files.register_file(file)
204
+ end
205
+
206
+ request.file_to_generate.each do |name|
207
+ files.fetch(name).file_to_generate = true
208
+ end
209
+
210
+ request.file_to_generate.each do |name| # rubocop:disable Style/CombinableLoops
211
+ file = files.fetch(name)
212
+ file_out = Google::Protobuf::Compiler::CodeGeneratorResponse::File.new
213
+ file_out.name = file.file_name
214
+ file_out.content = file_contents(files, file)
215
+ response.add_file(file_out)
216
+ end
217
+ end
218
+
219
+ def emit_decls(descriptor, group)
220
+ source_loc = descriptor.source_loc
221
+ return unless source_loc
222
+
223
+ source_loc.leading_detached_comments&.each do |c|
224
+ group.comment(c)
225
+ group.empty
226
+ end
227
+
228
+ group.comment(source_loc.leading_comments) if source_loc.leading_comments?
229
+
230
+ case descriptor
231
+ when DescriptorProto
232
+ group._class.identifier(descriptor.name).block do |g|
233
+ g.identifier("extend").identifier("Protobug::Message")
234
+ g.empty
235
+ g.identifier("self").dot("full_name").op("=").literal(descriptor.full_name)
236
+
237
+ requires_empty = true
238
+ descriptor.each_declaration do |decl|
239
+ g.empty if requires_empty
240
+ requires_empty = emit_decls(decl, g) &&
241
+ !decl.is_a?(FieldDescriptorProto) && !decl.is_a?(OneofDescriptorProto)
242
+ end
243
+
244
+ if descriptor.reserved_range.any? || descriptor.reserved_name.any?
245
+ g.empty
246
+ descriptor.file.loc_by_path(descriptor.source_loc.path +
247
+ [DescriptorProto.fields_by_name.fetch("reserved_range").number])&.tap do |loc|
248
+ g.comment(loc.leading_comments) if loc.leading_comments?
249
+ end
250
+ descriptor.reserved_range.each_with_index do |range, idx|
251
+ descriptor.file.loc_by_path(descriptor.source_loc.path + [
252
+ DescriptorProto.fields_by_name.fetch("reserved_range").number, idx
253
+ ]).tap do |loc|
254
+ g.comment(loc.leading_comments) if loc.leading_comments?
255
+ end
256
+ g.identifier("reserved_range").call do |c|
257
+ c.literal(range.start).op("...").literal(range.end)
258
+ end
259
+ end
260
+ descriptor.reserved_name.each do |name|
261
+ g.identifier("reserved_name").call do |c|
262
+ c.literal(name)
263
+ end
264
+ end
265
+ end
266
+ end
267
+ when EnumDescriptorProto
268
+ group._class.identifier(descriptor.name).block do |g|
269
+ g.identifier("extend").identifier("Protobug::Enum")
270
+ g.empty
271
+ g.identifier("self").dot("full_name").op("=")
272
+ .literal(descriptor.full_name)
273
+ g.empty
274
+
275
+ descriptor.each_declaration do |decl|
276
+ emit_decls(decl, g)
277
+ end
278
+
279
+ if descriptor.reserved_range.any? || descriptor.reserved_name.any?
280
+ g.empty
281
+ descriptor.reserved_range.each do |range|
282
+ g.identifier("reserved_range").call do |c|
283
+ c.literal(range.start).op("..").literal(range.end - 1)
284
+ end
285
+ end
286
+ descriptor.reserved_name.each do |name|
287
+ g.identifier("reserved_name").call do |c|
288
+ c.literal(name)
289
+ end
290
+ end
291
+ end
292
+ end
293
+ when EnumValueDescriptorProto
294
+ const_name = descriptor.name.start_with?("k") ? "K_#{descriptor.name[1..]}" : descriptor.name
295
+ const_name = "K_#{const_name}" unless const_name.match?(/\A[A-Z]/)
296
+ group.identifier(const_name).op("=").identifier("new").call do |c|
297
+ c.literal(descriptor.name)
298
+ c.literal(descriptor.number)
299
+ end.dot("freeze")
300
+ when FieldDescriptorProto
301
+ if descriptor.extendee?
302
+ containing_type = files.fetch_type(
303
+ if descriptor.extendee.start_with?(".")
304
+ descriptor.extendee[1..]
305
+ else
306
+ "#{descriptor.parent.full_name}.#{descriptor.extendee}"
307
+ end
308
+ )
309
+ group.comment("extension: #{containing_type.full_name}\n #{descriptor.name} #{descriptor.number}")
310
+ return # rubocop:disable Lint/NonLocalExitFromIterator
311
+ end
312
+
313
+ type = descriptor.type.name.downcase.delete_prefix("type_").to_sym
314
+
315
+ if descriptor.type_name?
316
+ referenced_type = files.fetch_type(descriptor.type_name.delete_prefix("."))
317
+ type = :map unless referenced_type.source_loc
318
+ end
319
+
320
+ group.identifier(
321
+ case descriptor.label
322
+ when Google::Protobuf::FieldDescriptorProto::Label::LABEL_OPTIONAL
323
+ "optional"
324
+ when Google::Protobuf::FieldDescriptorProto::Label::LABEL_REPEATED
325
+ if type == :map
326
+ "map"
327
+ else
328
+ "repeated"
329
+ end
330
+ when Google::Protobuf::FieldDescriptorProto::Label::LABEL_REQUIRED
331
+ "required"
332
+ else
333
+ raise "Unknown label: #{descriptor.label}"
334
+ end
335
+ ).call do |c|
336
+ c.literal(descriptor.number)
337
+ c.literal(descriptor.name)
338
+ c.identifier("type:").literal(type) unless type == :map
339
+
340
+ if type == :map
341
+ c.identifier("key_type:")
342
+ .literal(referenced_type.field[0].type.name.downcase.delete_prefix("type_").to_sym)
343
+ value_type = referenced_type.field[1].type.name.downcase.delete_prefix("type_").to_sym
344
+ c.identifier("value_type:")
345
+ .literal(value_type)
346
+ if referenced_type.field[1].type_name?
347
+ c.identifier("#{value_type}_type:")
348
+ .literal(referenced_type.field[1].type_name.delete_prefix("."))
349
+ end
350
+ elsif descriptor.type_name?
351
+ c.identifier("#{type}_type:").literal(descriptor.type_name.delete_prefix("."))
352
+ end
353
+
354
+ packed = descriptor.options&.packed
355
+ # TODO: exclude other types that cannot be packed
356
+ if !descriptor.options&.packed? && !%i[message bytes string map].include?(type)
357
+ packed = descriptor.label == Google::Protobuf::FieldDescriptorProto::Label::LABEL_REPEATED &&
358
+ descriptor.file.syntax == "proto3"
359
+ end
360
+ c.identifier("packed:").literal(packed) if packed
361
+ if descriptor.json_name? && descriptor.json_name != descriptor.name
362
+ c.identifier("json_name:").literal(descriptor.json_name)
363
+ end
364
+ if descriptor.oneof_index?
365
+ oneof = descriptor.parent.oneof_decl[descriptor.oneof_index]
366
+ synthetic = descriptor.proto3_optional && (descriptor.parent.field.count do |f|
367
+ f.oneof_index? && f.oneof_index == descriptor.oneof_index
368
+ end == 1)
369
+ c.identifier("oneof:").literal(oneof.name.to_sym) unless synthetic
370
+ end
371
+ if descriptor.label == Google::Protobuf::FieldDescriptorProto::Label::LABEL_OPTIONAL &&
372
+ descriptor.file.syntax == "proto3" && !descriptor.proto3_optional
373
+ c.identifier("proto3_optional:").literal(false)
374
+ end
375
+ end
376
+ when OneofDescriptorProto
377
+ group.empty if source_loc.leading_comments?
378
+ # no-op
379
+ else
380
+ raise "Unknown descriptor type: #{descriptor.class}"
381
+ end.tap do |s|
382
+ s.comment(source_loc.trailing_comments) if source_loc.trailing_comments?
383
+ end
384
+ end
385
+
386
+ def file_contents(files, file)
387
+ Builder.build_file do |f|
388
+ f.header_comment "frozen_string_literal: true"
389
+ f.header_comment "Code generated by protoc-gen-protobug. DO NOT EDIT."
390
+
391
+ f.comment "source: #{file.name}"
392
+ f.comment "syntax: #{file.syntax? ? file.syntax : "proto2"}"
393
+ f.comment "package: #{file.package}"
394
+ f.comment "options:"
395
+ if file.options
396
+ file.options.class.fields_by_name.each_key do |name|
397
+ next unless file.options.send(:"#{name}?")
398
+
399
+ value = case value = file.options.send(name)
400
+ when Enum::InstanceMethods
401
+ value.name
402
+ when Symbol
403
+ raise "Unknown symbol: #{value} for #{name} in #{file.options.inspect}"
404
+ else
405
+ value.inspect
406
+ end
407
+
408
+ f.comment " #{name}: #{value}"
409
+ end
410
+ end
411
+
412
+ file.source_loc.leading_detached_comments.each do |c|
413
+ f.empty
414
+ f.comment c
415
+ end
416
+
417
+ f.empty
418
+ f.identifier("require").literal("protobug")
419
+
420
+ local, external = file.dependency.map do |dep|
421
+ files.fetch(dep)
422
+ end.partition(&:file_to_generate)
423
+ if external.any?
424
+ f.empty
425
+ external.each do |dep|
426
+ f.identifier("require").literal(dep.file_name.delete_suffix(".rb"))
427
+ end
428
+ end
429
+ if local.any?
430
+ f.empty
431
+ local.each do |dep|
432
+ relative_path = Pathname(dep.file_name.delete_suffix(".rb"))
433
+ .relative_path_from(Pathname(file.file_name).dirname)
434
+ .to_path
435
+ f.identifier("require_relative").literal(relative_path)
436
+ end
437
+ end
438
+
439
+ f.empty
440
+
441
+ g = f
442
+ file.to_constant.split("::").each do |part|
443
+ g._module.identifier(part).block { |g_| g = g_ }
444
+ end
445
+
446
+ first = true
447
+ file.each_declaration do |decl|
448
+ g.empty unless first
449
+ emit_decls(decl, g)
450
+ first = false
451
+ end
452
+
453
+ g.empty unless first
454
+ g._def.identifier("self")
455
+ .dot("register_#{File.basename file.name.delete_suffix(".proto")}_protos")
456
+ .call do |c|
457
+ c.identifier("registry")
458
+ end.block do |defn|
459
+ file.dependency.each do |dep|
460
+ defn.identifier(files.fetch(dep).to_constant)
461
+ .dot("register_#{File.basename dep.delete_suffix(".proto")}_protos")
462
+ .call do |c|
463
+ c.identifier("registry")
464
+ end
465
+ end
466
+ emit_register(defn, file)
467
+ end
468
+ end
469
+ end
470
+
471
+ def emit_register(defn, descriptor)
472
+ case descriptor
473
+ when DescriptorProto, EnumDescriptorProto
474
+ return unless descriptor.source_loc
475
+
476
+ defn.identifier("registry").dot("register").call do |c|
477
+ c.identifier(descriptor.to_constant)
478
+ end
479
+ when FieldDescriptorProto
480
+ return unless descriptor.extendee?
481
+
482
+ containing_type = files.fetch_type(
483
+ if descriptor.extendee.start_with?(".")
484
+ descriptor.extendee[1..]
485
+ else
486
+ "#{descriptor.parent.full_name}.#{descriptor.extendee}"
487
+ end
488
+ )
489
+ defn.comment("extension: #{containing_type.full_name}\n #{descriptor.type} #{descriptor.number}")
490
+ when FileDescriptorProto, EnumValueDescriptorProto, ServiceDescriptorProto, OneofDescriptorProto
491
+ # no-op
492
+ else
493
+ raise "Unknown descriptor type: #{descriptor.class}"
494
+ end
495
+ descriptor.each_declaration do |decl|
496
+ emit_register(defn, decl)
497
+ end
498
+ end
499
+ end
500
+ end
metadata ADDED
@@ -0,0 +1,130 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: protobug-compiler
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Samuel Giddins
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2024-04-25 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: delegate
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: forwardable
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: prettier_print
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.2'
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: 1.2.1
51
+ type: :runtime
52
+ prerelease: false
53
+ version_requirements: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - "~>"
56
+ - !ruby/object:Gem::Version
57
+ version: '1.2'
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: 1.2.1
61
+ - !ruby/object:Gem::Dependency
62
+ name: protobug
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - '='
66
+ - !ruby/object:Gem::Version
67
+ version: 0.1.0
68
+ type: :runtime
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - '='
73
+ - !ruby/object:Gem::Version
74
+ version: 0.1.0
75
+ - !ruby/object:Gem::Dependency
76
+ name: protobug_compiler_protos
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - '='
80
+ - !ruby/object:Gem::Version
81
+ version: 0.1.0
82
+ type: :runtime
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - '='
87
+ - !ruby/object:Gem::Version
88
+ version: 0.1.0
89
+ description:
90
+ email:
91
+ - segiddins@segiddins.me
92
+ executables:
93
+ - protoc-gen-protobug
94
+ extensions: []
95
+ extra_rdoc_files: []
96
+ files:
97
+ - CHANGELOG.md
98
+ - LICENSE.txt
99
+ - exe/protoc-gen-protobug
100
+ - lib/protobug/compiler.rb
101
+ - lib/protobug/compiler/builder.rb
102
+ - lib/protobug/compiler/builder_gen.rb
103
+ homepage: https://github.com/segiddins/protobug
104
+ licenses:
105
+ - MIT
106
+ metadata:
107
+ allowed_push_host: https://rubygems.org/
108
+ rubygems_mfa_required: 'true'
109
+ homepage_uri: https://github.com/segiddins/protobug
110
+ source_code_uri: https://github.com/segiddins/protobug
111
+ post_install_message:
112
+ rdoc_options: []
113
+ require_paths:
114
+ - lib
115
+ required_ruby_version: !ruby/object:Gem::Requirement
116
+ requirements:
117
+ - - ">="
118
+ - !ruby/object:Gem::Version
119
+ version: 3.0.0
120
+ required_rubygems_version: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ requirements: []
126
+ rubygems_version: 3.5.9
127
+ signing_key:
128
+ specification_version: 4
129
+ summary: An protobuf compiler for protobug
130
+ test_files: []