diff --git a/lib/tapioca/dsl/compilers/kaminari.rb b/lib/tapioca/dsl/compilers/kaminari.rb new file mode 100644 index 0000000000..ef5fd8b698 --- /dev/null +++ b/lib/tapioca/dsl/compilers/kaminari.rb @@ -0,0 +1,83 @@ +# typed: strict +# frozen_string_literal: true + +return unless defined?(Kaminari) + +require "tapioca/dsl/helpers/active_record_constants_helper" + +module Tapioca + module Dsl + module Compilers + # `Tapioca::Dsl::Compilers::Kaminari` decorates RBI files for models + # using Kaminari. + # + # For example, with Kaminari installed and the following `ActiveRecord::Base` subclass: + # + # ~~~rb + # class Post < ApplicationRecord + # end + # ~~~ + # + # This compiler will produce the RBI file `post.rbi` with the following content: + # + # ~~~rbi + # # post.rbi + # # typed: true + # class Post + # extend GeneratedRelationMethods + # + # module GeneratedRelationMethods + # sig do + # params( + # num: T.any(Integer, String) + # ).returns(T.all(PrivateRelation, Kaminari::PageScopeMethods, Kaminari::ActiveRecordRelationMethods)) + # end + # def page(num = nil); end + # end + # end + # ~~~ + class Kaminari < Compiler + extend T::Sig + include Helpers::ActiveRecordConstantsHelper + + ConstantType = type_member { { fixed: T.class_of(::ActiveRecord::Base) } } + + sig { override.void } + def decorate + root.create_path(constant) do |model| + target_modules.each do |module_name, return_type| + model.create_module(module_name).create_method( + ::Kaminari.config.page_method_name.to_s, + parameters: [create_opt_param("num", type: "T.any(Integer, String)", default: "nil")], + return_type: "T.all(#{return_type}, Kaminari::PageScopeMethods, Kaminari::ActiveRecordRelationMethods)", + ) + end + + model.create_extend(RelationMethodsModuleName) + end + end + + class << self + sig { override.returns(T::Enumerable[Module]) } + def gather_constants + descendants_of(::ActiveRecord::Base).reject(&:abstract_class?) + end + end + + private + + sig { returns(T::Array[[String, String]]) } + def target_modules + if compiler_enabled?("ActiveRecordRelations") + [ + [RelationMethodsModuleName, RelationClassName], + [AssociationRelationMethodsModuleName, AssociationRelationClassName], + ] + else + [[RelationMethodsModuleName, "T.untyped"]] + end + end + end + end + end +end diff --git a/spec/tapioca/dsl/compilers/kaminari_spec.rb b/spec/tapioca/dsl/compilers/kaminari_spec.rb new file mode 100644 index 0000000000..db43873a83 --- /dev/null +++ b/spec/tapioca/dsl/compilers/kaminari_spec.rb @@ -0,0 +1,102 @@ +# typed: strict +# frozen_string_literal: true + +require "spec_helper" + +module Tapioca + module Dsl + module Compilers + class KaminariSpec < ::DslSpec + describe "Tapioca::Dsl::Compilers::Kaminari" do + sig { void } + def before_setup + require "active_record" + require "kaminari/activerecord" + end + + describe "initialize" do + it "gathers no constants if there are no ActiveRecord classes" do + assert_empty(gathered_constants) + end + + it "gathers only ActiveRecord constants with no abstract classes" do + add_ruby_file("post.rb", <<~RUBY) + class Post < ActiveRecord::Base + end + + class Product < ActiveRecord::Base + self.abstract_class = true + end + + class User + end + RUBY + + assert_equal(["Post"], gathered_constants) + end + end + + describe "decorate" do + describe "with relations enabled" do + before do + require "tapioca/dsl/compilers/active_record_relations" + activate_other_dsl_compilers(ActiveRecordRelations) + end + + it "generates an RBI file" do + add_ruby_file("post.rb", <<~RUBY) + class Post < ActiveRecord::Base + end + RUBY + + expected = <<~RBI + # typed: strong + + class Post + extend GeneratedRelationMethods + + module GeneratedAssociationRelationMethods + sig { params(num: T.any(Integer, String)).returns(T.all(PrivateAssociationRelation, Kaminari::PageScopeMethods, Kaminari::ActiveRecordRelationMethods)) } + def page(num = nil); end + end + + module GeneratedRelationMethods + sig { params(num: T.any(Integer, String)).returns(T.all(PrivateRelation, Kaminari::PageScopeMethods, Kaminari::ActiveRecordRelationMethods)) } + def page(num = nil); end + end + end + RBI + + assert_equal(expected, rbi_for(:Post)) + end + end + + describe "without relations enabled" do + it "generates an RBI file" do + add_ruby_file("post.rb", <<~RUBY) + class Post < ActiveRecord::Base + end + RUBY + + expected = <<~RBI + # typed: strong + + class Post + extend GeneratedRelationMethods + + module GeneratedRelationMethods + sig { params(num: T.any(Integer, String)).returns(T.all(T.untyped, Kaminari::PageScopeMethods, Kaminari::ActiveRecordRelationMethods)) } + def page(num = nil); end + end + end + RBI + + assert_equal(expected, rbi_for(:Post)) + end + end + end + end + end + end + end +end