protobug-compiler 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml 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: []