Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add compiler for ActiveRecord delegated types #1241

Merged
merged 13 commits into from
Dec 13, 2022
163 changes: 163 additions & 0 deletions lib/tapioca/dsl/compilers/active_record_delegated_types.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
# typed: strict
# frozen_string_literal: true

begin
require "active_record"
rescue LoadError
return
end

require "tapioca/dsl/helpers/active_record_column_type_helper"
require "tapioca/dsl/helpers/active_record_constants_helper"

module Tapioca
module Dsl
module Compilers
# `Tapioca::Dsl::Compilers::DelegatedTypes` defines RBI files for subclasses of
# [`ActiveRecord::Base`](https://api.rubyonrails.org/classes/ActiveRecord/Base.html).
# This compiler is only responsible for defining the methods that would be created for delegated_types that
# are defined in the Active Record model.
#
# For example, with the following model class:
#
# ~~~rb
# class Entry < ActiveRecord::Base
# delegated_type :entryable, types: %w[ Message Comment ]
# end
# ~~~
#
# this compiler will produce the following methods in the RBI file
# `entry.rbi`:
#
# ~~~rbi
# # entry.rbi
# # typed: true
#
# class Entry
# include GeneratedDelegatedTypeMethods
#
# module GeneratedDelegatedTypeMethods
# sig { params(args: T.untyped).returns(T.any(Message, Comment)) }
# def build_entryable(*args); end
#
# sig { returns(Class) }
# def entryable_class; end
#
# sig { returns(ActiveSupport::StringInquirer) }
# def entryable_name; end
#
# sig { returns(T::Boolean) }
# def message?; end
#
# sig { returns(T.nilable(Message)) }
# def message; end
#
# sig { returns(T.nilable(Integer)) }
# def message_id; end
#
# sig { returns(T::Boolean) }
# def comment?; end
#
# sig { returns(T.nilable(Comment)) }
# def comment; end
#
# sig { returns(T.nilable(Integer)) }
# def comment_id; end
KaanOzkan marked this conversation as resolved.
Show resolved Hide resolved
# end
# end
#
# ~~~
class ActiveRecordDelegatedTypes < Compiler
extend T::Sig
include Helpers::ActiveRecordConstantsHelper

ConstantType = type_member { { fixed: T.all(T.class_of(ActiveRecord::Base), Extensions::ActiveRecord) } }

sig { override.void }
def decorate
return if constant.__tapioca_delegated_types.nil?

root.create_path(constant) do |model|
model.create_module(DelegatedTypesModuleName) do |mod|
constant.__tapioca_delegated_types.each do |role, data|
types = data.fetch(:types)
options = data.fetch(:options, {})
KaanOzkan marked this conversation as resolved.
Show resolved Hide resolved
populate_role_accessors(mod, role, types)
populate_type_helpers(mod, role, types, options)
end
end

model.create_include(DelegatedTypesModuleName)
end
end

class << self
extend T::Sig

sig { override.returns(T::Enumerable[Module]) }
def gather_constants
descendants_of(::ActiveRecord::Base).reject(&:abstract_class?)
end
end

private

sig { params(mod: RBI::Scope, role: Symbol, types: T::Array[String]).void }
def populate_role_accessors(mod, role, types)
mod.create_method(
"#{role}_name",
parameters: [],
return_type: "ActiveSupport::StringInquirer",
)

mod.create_method(
"#{role}_class",
parameters: [],
return_type: "Class",
)

mod.create_method(
"build_#{role}",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ins't build_#{role} already added by the belongs_to DSL generator?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don’t think so because the belongs_to is polymorphic

parameters: [create_rest_param("args", type: "T.untyped")],
return_type: "T.any(#{types.join(", ")})",
)
end

sig { params(mod: RBI::Scope, role: Symbol, types: T::Array[String], options: T::Hash[Symbol, T.untyped]).void }
def populate_type_helpers(mod, role, types, options)
types.each do |type|
populate_type_helper(mod, role, type, options)
end
end

sig { params(mod: RBI::Scope, role: Symbol, type: String, options: T::Hash[Symbol, T.untyped]).void }
def populate_type_helper(mod, role, type, options)
singular = type.tableize.tr("/", "_").singularize
KaanOzkan marked this conversation as resolved.
Show resolved Hide resolved
query = "#{singular}?"
primary_key = options[:primary_key] || "id"
role_id = options[:foreign_key] || "#{role}_id"

getter_type, _ = Helpers::ActiveRecordColumnTypeHelper.new(constant).type_for(role_id.to_s)

mod.create_method(
query,
parameters: [],
return_type: "T::Boolean",
)

mod.create_method(
singular,
parameters: [],
return_type: "T.nilable(#{type})",
)

mod.create_method(
"#{singular}_#{primary_key}",
parameters: [],
return_type: as_nilable_type(getter_type),
)
end
end
end
end
end
29 changes: 29 additions & 0 deletions lib/tapioca/dsl/extensions/active_record.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# typed: true
# frozen_string_literal: true

begin
require "active_record"
rescue LoadError
return
end

module Tapioca
module Dsl
module Compilers
module Extensions
module ActiveRecord
attr_reader :__tapioca_delegated_types

def delegated_type(role, types:, **options)
@__tapioca_delegated_types ||= {}
@__tapioca_delegated_types[role] = { types: types, options: options }

super
end

::ActiveRecord::Base.singleton_class.prepend(self)
end
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ module ActiveRecordConstantsHelper

AttributeMethodsModuleName = T.let("GeneratedAttributeMethods", String)
AssociationMethodsModuleName = T.let("GeneratedAssociationMethods", String)
DelegatedTypesModuleName = T.let("GeneratedDelegatedTypeMethods", String)

RelationMethodsModuleName = T.let("GeneratedRelationMethods", String)
AssociationRelationMethodsModuleName = T.let("GeneratedAssociationRelationMethods", String)
Expand Down
56 changes: 56 additions & 0 deletions manual/compiler_activerecorddelegatedtypes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
## ActiveRecordDelegatedTypes

`Tapioca::Dsl::Compilers::DelegatedTypes` defines RBI files for subclasses of
[`ActiveRecord::Base`](https://api.rubyonrails.org/classes/ActiveRecord/Base.html).
This compiler is only responsible for defining the methods that would be created for delegated_types that
are defined in the Active Record model.

For example, with the following model class:

~~~rb
class Entry < ActiveRecord::Base
delegated_type :entryable, types: %w[ Message Comment ]
end
~~~

this compiler will produce the following methods in the RBI file
`entry.rbi`:

~~~rbi
# entry.rbi
# typed: true

class Entry
include GeneratedDelegatedTypeMethods

module GeneratedDelegatedTypeMethods
sig { params(args: T.untyped).returns(T.any(Message, Comment)) }
def build_entryable(*args); end

sig { returns(Class) }
def entryable_class; end

sig { returns(ActiveSupport::StringInquirer) }
def entryable_name; end

sig { returns(T::Boolean) }
def message?; end

sig { returns(T.nilable(Message)) }
def message; end

sig { returns(T.nilable(Integer)) }
def message_id; end

sig { returns(T::Boolean) }
def comment?; end

sig { returns(T.nilable(Comment)) }
def comment; end

sig { returns(T.nilable(Integer)) }
def comment_id; end
end
end

~~~
1 change: 1 addition & 0 deletions manual/compilers.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ In the following section you will find all available DSL compilers:
* [ActiveModelSecurePassword](compiler_activemodelsecurepassword.md)
* [ActiveRecordAssociations](compiler_activerecordassociations.md)
* [ActiveRecordColumns](compiler_activerecordcolumns.md)
* [ActiveRecordDelegatedTypes](compiler_activerecorddelegatedtypes.md)
* [ActiveRecordEnum](compiler_activerecordenum.md)
* [ActiveRecordFixtures](compiler_activerecordfixtures.md)
* [ActiveRecordRelations](compiler_activerecordrelations.md)
Expand Down
3 changes: 3 additions & 0 deletions spec/tapioca/cli/dsl_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1869,6 +1869,7 @@ class PostCompiler < Tapioca::Dsl::Compiler
Tapioca::Dsl::Compilers::ActiveModelSecurePassword enabled
Tapioca::Dsl::Compilers::ActiveRecordAssociations enabled
Tapioca::Dsl::Compilers::ActiveRecordColumns enabled
Tapioca::Dsl::Compilers::ActiveRecordDelegatedTypes enabled
Tapioca::Dsl::Compilers::ActiveRecordEnum enabled
Tapioca::Dsl::Compilers::ActiveRecordRelations enabled
Tapioca::Dsl::Compilers::ActiveRecordScope enabled
Expand Down Expand Up @@ -1897,6 +1898,7 @@ class PostCompiler < Tapioca::Dsl::Compiler
Tapioca::Dsl::Compilers::ActiveModelSecurePassword enabled
Tapioca::Dsl::Compilers::ActiveRecordAssociations enabled
Tapioca::Dsl::Compilers::ActiveRecordColumns enabled
Tapioca::Dsl::Compilers::ActiveRecordDelegatedTypes enabled
Tapioca::Dsl::Compilers::ActiveRecordEnum disabled
Tapioca::Dsl::Compilers::ActiveRecordRelations enabled
Tapioca::Dsl::Compilers::ActiveRecordScope enabled
Expand Down Expand Up @@ -1925,6 +1927,7 @@ class PostCompiler < Tapioca::Dsl::Compiler
Tapioca::Dsl::Compilers::ActiveModelSecurePassword disabled
Tapioca::Dsl::Compilers::ActiveRecordAssociations disabled
Tapioca::Dsl::Compilers::ActiveRecordColumns disabled
Tapioca::Dsl::Compilers::ActiveRecordDelegatedTypes disabled
Tapioca::Dsl::Compilers::ActiveRecordEnum enabled
Tapioca::Dsl::Compilers::ActiveRecordRelations disabled
Tapioca::Dsl::Compilers::ActiveRecordScope disabled
Expand Down
Loading