Skip to content

Commit

Permalink
Merge pull request #1241 from fcheung/delegated-types
Browse files Browse the repository at this point in the history
Add compiler for ActiveRecord delegated types
  • Loading branch information
paracycle authored Dec 13, 2022
2 parents 7c01c57 + 216f6e6 commit e0d3922
Show file tree
Hide file tree
Showing 7 changed files with 475 additions and 0 deletions.
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
# 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, {})
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}",
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
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
1 change: 1 addition & 0 deletions lib/tapioca/dsl/helpers/active_record_constants_helper.rb
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

0 comments on commit e0d3922

Please sign in to comment.