From aeb6eef855a2ee5f7ab096a1585afdad556cb234 Mon Sep 17 00:00:00 2001 From: Ufuk Kayserilioglu Date: Mon, 22 Jul 2024 19:10:33 +0300 Subject: [PATCH] Make it possible to extend list of id types for ActiveRecord relations By moving the list of id types to a constant, we are now making it possible to extend the list of id types for ActiveRecord relations in gem or application level "compilers". All that a "compiler" needs to do is to read the list of id types from the `Tapioca::DSL::Compilers::ActiveRecordRelations::ID_TYPES` constant, add the new id type to the list and then assign the result back to `Tapioca::DSL::Compilers::ActiveRecordRelations::ID_TYPES`. --- .../dsl/compilers/active_record_relations.rb | 38 ++++++----- spec/tapioca/cli/dsl_spec.rb | 65 +++++++++++++++++++ 2 files changed, 87 insertions(+), 16 deletions(-) diff --git a/lib/tapioca/dsl/compilers/active_record_relations.rb b/lib/tapioca/dsl/compilers/active_record_relations.rb index 762ab5a9d..5ade1cb8c 100644 --- a/lib/tapioca/dsl/compilers/active_record_relations.rb +++ b/lib/tapioca/dsl/compilers/active_record_relations.rb @@ -155,6 +155,25 @@ class ActiveRecordRelations < Compiler ConstantType = type_member { { fixed: T.class_of(::ActiveRecord::Base) } } + # From ActiveRecord::ConnectionAdapter::Quoting#quote, minus nil + ID_TYPES = T.let( + [ + "String", + "Symbol", + "::ActiveSupport::Multibyte::Chars", + "T::Boolean", + "BigDecimal", + "Numeric", + "::ActiveRecord::Type::Binary::Data", + "::ActiveRecord::Type::Time::Value", + "Date", + "Time", + "::ActiveSupport::Duration", + "T::Class[T.anything]", + ].to_set.freeze, + T::Set[String], + ) + sig { override.void } def decorate create_classes_and_includes @@ -663,27 +682,14 @@ def create_common_methods return_type: "T::Boolean", ) when :find - # From ActiveRecord::ConnectionAdapter::Quoting#quote, minus nil - id_types = [ - "String", - "Symbol", - "::ActiveSupport::Multibyte::Chars", - "T::Boolean", - "BigDecimal", - "Numeric", - "::ActiveRecord::Type::Binary::Data", - "::ActiveRecord::Type::Time::Value", - "Date", - "Time", - "::ActiveSupport::Duration", - "T::Class[T.anything]", - ].to_set + id_types = ID_TYPES if constant.table_exists? primary_key_type = constant.type_for_attribute(constant.primary_key) type = Tapioca::Dsl::Helpers::ActiveModelTypeHelper.type_for(primary_key_type) type = RBIHelper.as_non_nilable_type(type) - id_types << type if type != "T.untyped" + + id_types = ID_TYPES.union([type]) if type != "T.untyped" end id_types = "T.any(#{id_types.to_a.join(", ")})" diff --git a/spec/tapioca/cli/dsl_spec.rb b/spec/tapioca/cli/dsl_spec.rb index 55921b07e..f5740acd2 100644 --- a/spec/tapioca/cli/dsl_spec.rb +++ b/spec/tapioca/cli/dsl_spec.rb @@ -5,6 +5,8 @@ module Tapioca class DslSpec < SpecWithProject + include Tapioca::Helpers::Test::Template + describe "cli::dsl" do before(:all) do @project.write!("config/application.rb", <<~RB) @@ -2050,6 +2052,10 @@ class Post end describe "custom compilers" do + after do + project.remove!("sorbet/rbi/dsl") + end + it "must load custom compilers from gems" do @project.write!("lib/post.rb", <<~RB) class Post @@ -2179,6 +2185,65 @@ module GeneratedBar; end assert_empty_stderr(result) assert_success_status(result) end + + it "must be able to modify behaviour of existing compilers" do + @project.write!("lib/post.rb", <<~RB) + ::ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:") + + class Post < ActiveRecord::Base + end + RB + + foo = mock_gem("foo", "0.0.1") do + write!("lib/tapioca/dsl/compilers/active_record_relation_ext.rb", <<~RB) + class Tapioca::Dsl::Compilers::ActiveRecordRelations + ID_TYPES = ID_TYPES.union(["Foo"]).freeze + end + RB + end + + @project.require_mock_gem(foo) + @project.require_real_gem("activerecord", require: "active_record") + @project.require_real_gem("sqlite3", "1.7.3") + @project.bundle_install! + + result = @project.tapioca("dsl Post") + + assert_stdout_equals(<<~OUT, result) + Loading DSL extension classes... Done + Loading Rails application... Done + Loading DSL compiler classes... Done + Compiling DSL RBI files... + + create sorbet/rbi/dsl/post.rbi + + Done + + Checking generated RBI files... Done + No errors found + + All operations performed in working directory. + Please review changes and commit them. + OUT + + assert_project_file_includes("sorbet/rbi/dsl/post.rbi", indented(<<~RBI, 4)) + sig do + params( + args: T.any(String, Symbol, ::ActiveSupport::Multibyte::Chars, T::Boolean, BigDecimal, Numeric, ::ActiveRecord::Type::Binary::Data, ::ActiveRecord::Type::Time::Value, Date, Time, ::ActiveSupport::Duration, T::Class[T.anything], Foo) + ).returns(::Post) + end + sig do + params( + args: T::Array[T.any(String, Symbol, ::ActiveSupport::Multibyte::Chars, T::Boolean, BigDecimal, Numeric, ::ActiveRecord::Type::Binary::Data, ::ActiveRecord::Type::Time::Value, Date, Time, ::ActiveSupport::Duration, T::Class[T.anything], Foo)] + ).returns(T::Enumerable[::Post]) + end + sig { params(args: NilClass, block: T.proc.params(object: ::Post).void).returns(T.nilable(::Post)) } + def find(args = nil, &block); end + RBI + + assert_empty_stderr(result) + assert_success_status(result) + end end describe "custom extensions" do