dep-protobuf/ruby/generate_stubs.rb
Daniel Orner 5e3dd3d6bc Add Ruby files for documentation purposes (#21212)
Partial fix for #21092 .

This adds a number of Ruby files to the `lib/docs` directory. These files are not actually required by the Protobuf gem, so they should not affect any functionality. They are only there to key off of the auto-generation by [https://www.rubydoc.info](https://www.rubydoc.info) .

For now I've left the comments in the C and Java code. However, they duplicate what I have there (since I largely copy/pasted the English content from those comments). One thing we could do is change the comments to reference the Ruby files, e.g.:

```c
/*
 * See documentation: ../../../lib/stubs/descriptor_pool#lookup
 */
static VALUE DescriptorPool_lookup(VALUE _self, VALUE name) {
```

I can make those changes if approved.

This PR does *not* include files generated by Protobuf from `.proto` files. That means that references to things like `ServiceDescriptorProto` don't go anywhere. I can look at generating similar files to this via a protoc plugin, although it'd be a bit in-depth. Or I could do it by hand of course :)

Note that the [current rubydocs](https://www.rubydoc.info/gems/google-protobuf) does have some entries, but they are mostly from the FFI implementation and are kind of an "accident". My PR specifies the files to generate should *only* come from the `lib/docs` folder.

Feedback welcome!

Closes #21212

COPYBARA_INTEGRATE_REVIEW=https://github.com/protocolbuffers/protobuf/pull/21212 from dorner:ruby-doc 9cc3613339
PiperOrigin-RevId: 768309166
2025-06-06 19:11:48 -07:00

95 lines
3.1 KiB
Ruby

# This script parses the comments in the Ruby C extension files and translates them into Ruby
# stubs. It looks for an instance of `ruby-doc:` and takes the following string as the object
# under documentation (a class, instance method, or class method).
# The intention of generating these files is for use by YARD to auto-generate documentation
# here: https://www.rubydoc.info/gems/google-protobuf/
#
# Because these comments are essentially "pretending" to be Ruby, we had to add one additional
# YARD tag, `@paramdefault`, which is not a standard YARD tag. This tag is used to specify default
# values for parameters.
require 'erb'
files = Dir.glob('ext/**/*.c')
classes = {}
MethodDefn = Struct.new(:name, :body, keyword_init: true) do
def params
params = body.scan(/@param (\S+) /).map { |p| p[0]}
defaults = body.scan(/@paramdefault (\S+) (\S+)/).map { |p| [p[0], p[1]]}.to_h
params = params.map { |p| p == 'kwargs' ? '**kwargs' : p }
params = params.map { |p| defaults[p] ? "#{p}=#{defaults[p]}" : p }
if body.include?('@yield')
params.push('&block')
end
params
end
end
Defn = Struct.new(:name, :instance_meths, :class_meths, :body) do
def initialize(*args, **kwargs)
super
self.instance_meths ||= []
self.class_meths ||= []
end
end
files.each do |file|
defs = File.read(file).scan(/ruby-doc:(.*?)\n(.*?)\*\//m)
defs.each do |definition|
name = definition[0].strip
body = definition[1].strip.
gsub(/^\s*\*/, '').
gsub(/\n/m, "\n # ").strip
if name.include?('.')
klass, method_name = name.split('.')
method = MethodDefn.new(name: method_name, body: body)
classes[klass] ||= Defn.new(name: klass)
classes[klass].class_meths.push(method)
elsif name.include?('#')
klass, method_name = name.split('#')
method = MethodDefn.new(name: method_name, body: body)
classes[klass] ||= Defn.new(name: klass)
classes[klass].instance_meths.push(method)
else
classes[name] = Defn.new(name: name, body: body)
end
end
end
# copied from ActiveSupport::Inflector
def underscore(str)
regex = /(?:(?<=([A-Za-z\d]))|\b)((?=a)b)(?=\b|[^a-z])/
str.gsub(regex) { "#{$1 && '_' }#{$2.downcase}" }.
gsub(/([A-Z])(?=[A-Z][a-z])|([a-z\d])(?=[A-Z])/) { ($1 || $2) << "_" }.
downcase
end
class_erb = <<-ERB
# This file was generated by generate_stubs.rb
# Do not edit this file directly.
<%- if defn.body -%>
<%= defn.body.gsub(' #', '#').gsub('# ', '# ') %>
<%- end -%>
class Google::Protobuf::<%= name %>
<%- defn.instance_meths.each do |meth| -%>
<%= meth.body.gsub('# ', '# ') %>
def <%= meth.name %><% if meth.params.any? %>(<%= meth.params.join(', ') %>)<% end %>; end
<%- end -%>
<%- defn.class_meths.each do |meth| -%>
<%= meth.body.gsub('# ', '# ') %>
def self.<%= meth.name %><% if meth.params.any? %>(<%= meth.params.join(', ') %>)<% end %>; end
<%- end -%>
end # class Google::Protobuf::<%= name %>
ERB
classes.each do |name, defn|
file = File.join('lib/stubs', "#{underscore(name)}.rb")
File.open(file, 'w') do |f|
result = ERB.new(class_erb, trim_mode: '-').result(binding)
f.puts result
end
end