diff --git a/lib/tapioca/dsl/compilers/protobuf.rb b/lib/tapioca/dsl/compilers/protobuf.rb index ba458a777..1273841d1 100644 --- a/lib/tapioca/dsl/compilers/protobuf.rb +++ b/lib/tapioca/dsl/compilers/protobuf.rb @@ -177,6 +177,18 @@ def gather_constants private + sig { params(desc: Google::Protobuf::FieldDescriptor).returns(T::Boolean) } + def has_presence?(desc) + if desc.respond_to?(:has_presence?) + # This method is only defined in google-protobuf 3.26.0 and later + desc.has_presence? + else + # In older versions of the gem, the only way we can get this information is + # by checking if an instance of the class responds to the expected method + T.unsafe(constant.allocate).respond_to?("has_#{desc.name}?") + end + end + sig { params(klass: RBI::Scope, names: String).void } def create_type_members(klass, *names) klass.create_extend("T::Generic") @@ -302,7 +314,7 @@ def create_descriptor_method(klass, desc) return_type: "void", ) - if desc.has_presence? + if has_presence?(desc) klass.create_method( "has_#{field.name}?", return_type: "Object", diff --git a/spec/tapioca/cli/dsl_spec.rb b/spec/tapioca/cli/dsl_spec.rb index fb665b93f..ee5934bed 100644 --- a/spec/tapioca/cli/dsl_spec.rb +++ b/spec/tapioca/cli/dsl_spec.rb @@ -2715,6 +2715,44 @@ class Application < Rails::Application assert_success_status(res) end end + + # These tests are in this file rather than in google_protobuf_spec.rb because they require + # a different version of the google-protobuf gem than the one that is used in the rest of the tests. + describe "google-protobuf" do + describe "version <= 3.25" do + before(:all) do + @project.require_real_gem("google-protobuf", "~>3.25") + @project.bundle_install! + end + + it "generates has_{field.name}? methods in RBI files for classes with Protobuf" do + @project.exec("mkdir lib") + @project.write!("proto/cart.proto", <<~PROTO) + syntax = "proto3"; + + message Cart { + enum VALUE_TYPE { + NULL = 0; + FIXED_AMOUNT = 1; + PERCENTAGE = 2; + } + + optional VALUE_TYPE value_type = 1; + } + PROTO + + result = @project.exec("protoc --proto_path=proto --ruby_out=lib proto/cart.proto") + raise "Error executing protoc: #{result.err}" unless result.status + + result = @project.tapioca("dsl Cart") + assert_empty_stderr(result) + assert_success_status(result) + + cart_rbi = @project.read("sorbet/rbi/dsl/cart.rbi") + assert_includes(cart_rbi, "def has_value_type?; end") + end + end + end end end end