-
Notifications
You must be signed in to change notification settings - Fork 122
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1241 from fcheung/delegated-types
Add compiler for ActiveRecord delegated types
- Loading branch information
Showing
7 changed files
with
475 additions
and
0 deletions.
There are no files selected for viewing
163 changes: 163 additions & 0 deletions
163
lib/tapioca/dsl/compilers/active_record_delegated_types.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
|
||
~~~ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.