class Tapioca::Dsl::Compilers::ActiveRecordRelations
~~~
end
end
Elem = type_member { { fixed: ::Post } }
def to_ary; end
sig { returns(T::Array) }
def to_a; end
sig { returns(T::Array) }
include GeneratedRelationMethods
include CommonRelationMethods
class PrivateRelation < ::ActiveRecord::Relation
end
# …
def <<(*records); end
end
.returns(PrivateCollectionProxy)
params(records: T.any(::Post, T::Array, T::Array))
sig do
include GeneratedAssociationRelationMethods
include CommonRelationMethods
class PrivateCollectionProxy < ::ActiveRecord::Associations::CollectionProxy
end
Elem = type_member { { fixed: ::Post } }
def to_ary; end
sig { returns(T::Array) }
def to_a; end
sig { returns(T::Array) }
include GeneratedAssociationRelationMethods
include CommonRelationMethods
class PrivateAssociationRelation < ::ActiveRecord::AssociationRelation
end
def where(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) }
# …
def all; end
sig { returns(PrivateRelation) }
module GeneratedRelationMethods
end
def where(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) }
# …
def all; end
sig { returns(PrivateAssociationRelation) }
module GeneratedAssociationRelationMethods
end
# …
def any?(&block); end
sig { params(block: T.nilable(T.proc.params(record: ::Post).returns(T.untyped))).returns(T::Boolean) }
module CommonRelationMethods
extend GeneratedRelationMethods
extend CommonRelationMethods
class Post
# typed: true
# post.rbi
~~~rbi
this compiler will produce the RBI file ‘post.rbi` with the following content:
~~~
end
class Post < ApplicationRecord
~~~rb
For example, with the following `ActiveRecord::Base` subclass:
make the runtime checks fail.
For that reason, these types cannot be used in user code or in `sig`s inside Ruby files, since that will
exist at runtime, and their counterparts that do exist at runtime are marked `private_constant` anyway.
that they represent private subconstants of the Active Record model. As such, these types do not
CAUTION: The generated relation classes are named `PrivateXXX` intentionally to reflect the fact
`Model` class.
`Model::PrivateRelation` modules, so that, for example, `find_by` and `all` can be chained off of the
Additionally, the actual `Model` class extends both `Model::CommonRelationMethods` and
This module is used to reduce the replication of methods between the previous two modules.
instance), regardless of what kind of relation it is called on, and so belongs in this module.
relation in their return type. For example, `find_by!` will always return the same type (a `Model`
3. `Model::CommonRelationMethods` holds all the relation methods that do not depend on the type of
with that return type in this module.
always return a `Model::PrivateAssociationRelation` instance, thus the signature of `all` is defined
`Model::PrivateAssociationRelation` or an instance of `Model::PrivateCollectionProxy` class will
of `Model::PrivateAssociationRelation`. For example, calling `all` on an instance of
2. `Model::GeneratedAssociationRelationMethods` holds all the relation methods with the return type
signature of `all` is defined with that return type in this module.
`Model::PrivateRelation` class will always return a `Model::PrivateRelation` instance, thus the
`Model::PrivateRelation`. For example, calling `all` on the `Model` class or an instance of
1. `Model::GeneratedRelationMethods` holds all the relation methods with the return type of
and the following modules:
etc new `Model` instances in the collection.
This class represents a collection of `Model` instances with some extra methods to `build`, `create`,
whose methods which return a relation will always return a `Model::PrivateAssociationRelation` instance.
This synthetic class represents a relation on a plural association of type `Model` (e.g. `foo.models`)
3. `Model::PrivateCollectionProxy` that subclasses from `ActiveRecord::Associations::CollectionProxy`.
for this relation.
class and the previous one is mainly that an association relation also keeps track of the resource association
return a relation will always return a `Model::PrivateAssociationRelation` instance. The difference between this
class represents a relation on a singular association of type `Model` (e.g. `foo.model`) whose methods which
2. `Model::PrivateAssociationRelation` that subclasses `ActiveRecord::AssociationRelation`. This synthetic
a relation on `Model` whose methods which return a relation always return a `Model::PrivateRelation` instance.
1. A `Model::PrivateRelation` that subclasses `ActiveRecord::Relation`. This synthetic class represents
For a given model `Model`, we generate the following classes:
The compiler defines 3 (synthetic) modules and 3 (synthetic) classes to represent relations properly.<br><br>(api.rubyonrails.org/classes/ActiveRecord/Calculations.html) methods.<br>(api.rubyonrails.org/classes/ActiveRecord/FinderMethods.html), and<br>(api.rubyonrails.org/classes/ActiveRecord/SpawnMethods.html),<br>[query](api.rubyonrails.org/classes/ActiveRecord/QueryMethods.html),
[collection proxy](https://api.rubyonrails.org/classes/ActiveRecord/Associations/CollectionProxy.html),<br>(api.rubyonrails.org/classes/ActiveRecord/Relation.html),
`ActiveRecord::Base` and adds
`Tapioca::Dsl::Compilers::ActiveRecordRelations` decorates RBI files for subclasses of
def association_relation_methods_module
def association_relation_methods_module @association_relation_methods_module ||= T.let( model.create_module(AssociationRelationMethodsModuleName), T.nilable(RBI::Scope), ) end
def bang_method?(method_name)
def bang_method?(method_name) method_name.to_s.end_with?("!") end
def common_relation_methods_module
def common_relation_methods_module @common_relation_methods_module ||= T.let( model.create_module(CommonRelationMethodsModuleName), T.nilable(RBI::Scope), ) end
def constant_name
def constant_name @constant_name ||= T.let(T.must(qualified_name_of(constant)), T.nilable(String)) end
def create_association_relation_class
def create_association_relation_class superclass = "::ActiveRecord::AssociationRelation" # Association subclasses include the generated association relation module model.create_class(AssociationRelationClassName, superclass_name: superclass) do |klass| klass.create_include(CommonRelationMethodsModuleName) klass.create_include(AssociationRelationMethodsModuleName) klass.create_type_variable("Elem", type: "type_member", fixed: constant_name) TO_ARRAY_METHODS.each do |method_name| klass.create_method(method_name.to_s, return_type: "T::Array[#{constant_name}]") end end create_association_relation_group_chain_class create_association_relation_where_chain_class end
def create_association_relation_group_chain_class
def create_association_relation_group_chain_class model.create_class( AssociationRelationGroupChainClassName, superclass_name: AssociationRelationClassName, ) do |klass| create_group_chain_methods(klass) klass.create_type_variable("Elem", type: "type_member", fixed: constant_name) end end
def create_association_relation_methods
def create_association_relation_methods returning_type = "T.nilable(T.any(T::Array[Symbol], FalseClass))" unique_by_type = "T.nilable(T.any(T::Array[Symbol], Symbol))" ASSOCIATION_METHODS.each do |method_name| case method_name when :insert_all, :insert_all!, :upsert_all parameters = [ create_param("attributes", type: "T::Array[Hash]"), create_kw_opt_param("returning", type: returning_type, default: "nil"), ] # Bang methods don't have the `unique_by` parameter unless bang_method?(method_name) parameters << create_kw_opt_param("unique_by", type: unique_by_type, default: "nil") end association_relation_methods_module.create_method( method_name.to_s, parameters: parameters, return_type: "ActiveRecord::Result", ) when :insert, :insert!, :upsert parameters = [ create_param("attributes", type: "Hash"), create_kw_opt_param("returning", type: returning_type, default: "nil"), ] # Bang methods don't have the `unique_by` parameter unless bang_method?(method_name) parameters << create_kw_opt_param("unique_by", type: unique_by_type, default: "nil") end association_relation_methods_module.create_method( method_name.to_s, parameters: parameters, return_type: "ActiveRecord::Result", ) when :proxy_association # skip - private method end end end
def create_association_relation_where_chain_class
def create_association_relation_where_chain_class model.create_class(AssociationRelationWhereChainClassName) do |klass| create_where_chain_methods(klass, AssociationRelationClassName) klass.create_type_variable("Elem", type: "type_member", fixed: constant_name) end end
def create_classes_and_includes
def create_classes_and_includes model.create_extend(CommonRelationMethodsModuleName) # The model always extends the generated relation module model.create_extend(RelationMethodsModuleName) # Type the `to_ary` method as returning `NilClass` so that flatten stops recursing # See https://github.com/sorbet/sorbet/pull/4706 for details model.create_method("to_ary", return_type: "NilClass", visibility: RBI::Private.new) create_relation_class create_association_relation_class create_collection_proxy_class end
def create_collection_proxy_class
def create_collection_proxy_class superclass = "::ActiveRecord::Associations::CollectionProxy" # The relation subclass includes the generated association relation module model.create_class(AssociationsCollectionProxyClassName, superclass_name: superclass) do |klass| klass.create_include(CommonRelationMethodsModuleName) klass.create_include(AssociationRelationMethodsModuleName) klass.create_type_variable("Elem", type: "type_member", fixed: constant_name) TO_ARRAY_METHODS.each do |method_name| klass.create_method(method_name.to_s, return_type: "T::Array[#{constant_name}]") end create_collection_proxy_methods(klass) end end
def create_collection_proxy_methods(klass)
def create_collection_proxy_methods(klass) # For these cases, it is valid to pass: # - a model instance, thus `Model` # - a model collection which can be: # - an array of models, thus `T::Enumerable[Model]` # - an association relation of a model, thus `T::Enumerable[Model]` # - a collection proxy of a model, thus, again, a `T::Enumerable[Model]` # - a collection of relations or collection proxies, thus `T::Enumerable[T::Enumerable[Model]]` # - or, any mix of the above, thus `T::Enumerable[T.any(Model, T::Enumerable[Model])]` # which altogether gives us: # `T.any(Model, T::Enumerable[T.any(Model, T::Enumerable[Model])])` model_collection = "T.any(#{constant_name}, T::Enumerable[T.any(#{constant_name}, T::Enumerable[#{constant_name}])])" # For these cases, it is valid to pass the above kind of things, but also: # - a model identifier, which can be: # - a numeric id, thus `Integer` # - a string id, thus `String` # - a collection of identifiers # - a collection of identifiers, thus `T::Enumerable[T.any(Integer, String)]` # which, coupled with the above case, gives us: # `T.any(Model, Integer, String, T::Enumerable[T.any(Model, Integer, String, T::Enumerable[Model])])` model_or_id_collection = "T.any(#{constant_name}, Integer, String" \ ", T::Enumerable[T.any(#{constant_name}, Integer, String, T::Enumerable[#{constant_name}])])" COLLECTION_PROXY_METHODS.each do |method_name| case method_name when :<<, :append, :concat, :prepend, :push klass.create_method( method_name.to_s, parameters: [ create_rest_param("records", type: model_collection), ], return_type: AssociationsCollectionProxyClassName, ) when :clear klass.create_method( method_name.to_s, return_type: AssociationsCollectionProxyClassName, ) when :delete, :destroy klass.create_method( method_name.to_s, parameters: [ create_rest_param("records", type: model_or_id_collection), ], return_type: "T::Array[#{constant_name}]", ) when :load_target klass.create_method( method_name.to_s, return_type: "T::Array[#{constant_name}]", ) when :replace klass.create_method( method_name.to_s, parameters: [ create_param("other_array", type: model_collection), ], return_type: "T::Array[#{constant_name}]", ) when :reset_scope # skip when :scope klass.create_method( method_name.to_s, return_type: AssociationRelationClassName, ) when :target klass.create_method( method_name.to_s, return_type: "T::Array[#{constant_name}]", ) end end end
def create_common_method(name, parameters: [], return_type: nil)
def create_common_method(name, parameters: [], return_type: nil) common_relation_methods_module.create_method( name.to_s, parameters: parameters, return_type: return_type || "void", ) end
def create_common_methods
def create_common_methods create_common_method( "destroy_all", return_type: "T::Array[#{constant_name}]", ) FINDER_METHODS.each do |method_name| case method_name when :exists? create_common_method( "exists?", parameters: [ create_opt_param("conditions", type: "T.untyped", default: ":none"), ], return_type: "T::Boolean", ) when :include?, :member? create_common_method( method_name, parameters: [ create_param("record", type: "T.untyped"), ], return_type: "T::Boolean", ) when :find 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 = ID_TYPES.union([type]) if type != "T.untyped" end id_types = "T.any(#{id_types.to_a.join(", ")})" if constant.try(:composite_primary_key?) id_types = "T::Array[#{id_types}]" end array_type = "T::Array[#{id_types}]" common_relation_methods_module.create_method("find") do |method| method.add_opt_param("args", "nil") method.add_block_param("block") method.add_sig do |sig| sig.add_param("args", id_types) sig.return_type = constant_name end method.add_sig do |sig| sig.add_param("args", array_type) sig.return_type = "T::Enumerable[#{constant_name}]" end method.add_sig do |sig| sig.add_param("args", "NilClass") sig.add_param("block", "T.proc.params(object: #{constant_name}).void") sig.return_type = as_nilable_type(constant_name) end end when :find_by create_common_method( "find_by", parameters: [ create_rest_param("args", type: "T.untyped"), ], return_type: as_nilable_type(constant_name), ) when :find_by! create_common_method( "find_by!", parameters: [ create_rest_param("args", type: "T.untyped"), ], return_type: constant_name, ) when :find_sole_by create_common_method( "find_sole_by", parameters: [ create_param("arg", type: "T.untyped"), create_rest_param("args", type: "T.untyped"), ], return_type: constant_name, ) when :sole create_common_method( "sole", parameters: [], return_type: constant_name, ) when :first, :last, :take common_relation_methods_module.create_method(method_name.to_s) do |method| method.add_opt_param("limit", "nil") method.add_sig do |sig| sig.return_type = as_nilable_type(constant_name) end method.add_sig do |sig| sig.add_param("limit", "Integer") sig.return_type = "T::Array[#{constant_name}]" end end when :raise_record_not_found_exception! # skip else return_type = if bang_method?(method_name) constant_name else as_nilable_type(constant_name) end create_common_method( method_name, return_type: return_type, ) end end SIGNED_FINDER_METHODS.each do |method_name| case method_name when :find_signed create_common_method( "find_signed", parameters: [ create_param("signed_id", type: "T.untyped"), create_kw_opt_param("purpose", type: "T.untyped", default: "nil"), ], return_type: as_nilable_type(constant_name), ) when :find_signed! create_common_method( "find_signed!", parameters: [ create_param("signed_id", type: "T.untyped"), create_kw_opt_param("purpose", type: "T.untyped", default: "nil"), ], return_type: constant_name, ) end end CALCULATION_METHODS.each do |method_name| case method_name when :average, :maximum, :minimum create_common_method( method_name, parameters: [ create_param("column_name", type: "T.any(String, Symbol)"), ], return_type: method_name == :average ? "T.any(Integer, Float, BigDecimal)" : "T.untyped", ) when :calculate create_common_method( "calculate", parameters: [ create_param("operation", type: "Symbol"), create_param("column_name", type: "T.any(String, Symbol)"), ], return_type: "T.any(Integer, Float, BigDecimal)", ) when :count common_relation_methods_module.create_method(method_name.to_s) do |method| method.add_opt_param("column_name", "nil") method.add_block_param("block") method.add_sig do |sig| sig.add_param("column_name", "T.nilable(T.any(String, Symbol))") sig.return_type = "Integer" end method.add_sig do |sig| sig.add_param("column_name", "NilClass") sig.add_param("block", "T.proc.params(object: #{constant_name}).void") sig.return_type = "Integer" end end when :ids create_common_method("ids", return_type: "Array") when :pick, :pluck create_common_method( method_name, parameters: [ create_rest_param("column_names", type: "T.untyped"), ], return_type: "T.untyped", ) when :sum common_relation_methods_module.create_method(method_name.to_s) do |method| method.add_opt_param("initial_value_or_column", "nil") method.add_block_param("block") method.add_sig do |sig| sig.add_param("initial_value_or_column", "T.untyped") sig.return_type = "T.any(Integer, Float, BigDecimal)" end method.add_sig(type_params: ["U"]) do |sig| sig.add_param("initial_value_or_column", "T.nilable(T.type_parameter(:U))") sig.add_param("block", "T.proc.params(object: #{constant_name}).returns(T.type_parameter(:U))") sig.return_type = "T.type_parameter(:U)" end end end end BATCHES_METHODS.each do |method_name| case method_name when :find_each order = ActiveRecord::Batches.instance_method(:find_each).parameters.include?([:key, :order]) common_relation_methods_module.create_method("find_each") do |method| method.add_kw_opt_param("start", "nil") method.add_kw_opt_param("finish", "nil") method.add_kw_opt_param("batch_size", "1000") method.add_kw_opt_param("error_on_ignore", "nil") method.add_kw_opt_param("order", ":asc") if order method.add_block_param("block") method.add_sig do |sig| sig.add_param("start", "T.untyped") sig.add_param("finish", "T.untyped") sig.add_param("batch_size", "Integer") sig.add_param("error_on_ignore", "T.untyped") sig.add_param("order", "Symbol") if order sig.add_param("block", "T.proc.params(object: #{constant_name}).void") sig.return_type = "void" end method.add_sig do |sig| sig.add_param("start", "T.untyped") sig.add_param("finish", "T.untyped") sig.add_param("batch_size", "Integer") sig.add_param("error_on_ignore", "T.untyped") sig.add_param("order", "Symbol") if order sig.return_type = "T::Enumerator[#{constant_name}]" end end when :find_in_batches order = ActiveRecord::Batches.instance_method(:find_in_batches).parameters.include?([:key, :order]) common_relation_methods_module.create_method("find_in_batches") do |method| method.add_kw_opt_param("start", "nil") method.add_kw_opt_param("finish", "nil") method.add_kw_opt_param("batch_size", "1000") method.add_kw_opt_param("error_on_ignore", "nil") method.add_kw_opt_param("order", ":asc") if order method.add_block_param("block") method.add_sig do |sig| sig.add_param("start", "T.untyped") sig.add_param("finish", "T.untyped") sig.add_param("batch_size", "Integer") sig.add_param("error_on_ignore", "T.untyped") sig.add_param("order", "Symbol") if order sig.add_param("block", "T.proc.params(object: T::Array[#{constant_name}]).void") sig.return_type = "void" end method.add_sig do |sig| sig.add_param("start", "T.untyped") sig.add_param("finish", "T.untyped") sig.add_param("batch_size", "Integer") sig.add_param("error_on_ignore", "T.untyped") sig.add_param("order", "Symbol") if order sig.return_type = "T::Enumerator[T::Enumerator[#{constant_name}]]" end end when :in_batches order = ActiveRecord::Batches.instance_method(:in_batches).parameters.include?([:key, :order]) use_ranges = ActiveRecord::Batches.instance_method(:in_batches).parameters.include?([:key, :use_ranges]) common_relation_methods_module.create_method("in_batches") do |method| method.add_kw_opt_param("of", "1000") method.add_kw_opt_param("start", "nil") method.add_kw_opt_param("finish", "nil") method.add_kw_opt_param("load", "false") method.add_kw_opt_param("error_on_ignore", "nil") method.add_kw_opt_param("order", ":asc") if order method.add_kw_opt_param("use_ranges", "nil") if use_ranges method.add_block_param("block") method.add_sig do |sig| sig.add_param("of", "Integer") sig.add_param("start", "T.untyped") sig.add_param("finish", "T.untyped") sig.add_param("load", "T.untyped") sig.add_param("error_on_ignore", "T.untyped") sig.add_param("order", "Symbol") if order sig.add_param("use_ranges", "T.untyped") if use_ranges sig.add_param("block", "T.proc.params(object: #{RelationClassName}).void") sig.return_type = "void" end method.add_sig do |sig| sig.add_param("of", "Integer") sig.add_param("start", "T.untyped") sig.add_param("finish", "T.untyped") sig.add_param("load", "T.untyped") sig.add_param("error_on_ignore", "T.untyped") sig.add_param("order", "Symbol") if order sig.add_param("use_ranges", "T.untyped") if use_ranges sig.return_type = "::ActiveRecord::Batches::BatchEnumerator" end end end end ENUMERABLE_QUERY_METHODS.each do |method_name| block_type = "T.nilable(T.proc.params(record: #{constant_name}).returns(T.untyped))" create_common_method( method_name, parameters: [ create_block_param("block", type: block_type), ], return_type: "T::Boolean", ) end FIND_OR_CREATE_METHODS.each do |method_name| common_relation_methods_module.create_method(method_name.to_s) do |method| method.add_param("attributes") method.add_block_param("block") # `T.untyped` matches `T::Array[T.untyped]` so the array signature # must be defined first for Sorbet to pick it, if valid. method.add_sig do |sig| sig.add_param("attributes", "T::Array[T.untyped]") sig.add_param("block", "T.nilable(T.proc.params(object: #{constant_name}).void)") sig.return_type = "T::Array[#{constant_name}]" end method.add_sig do |sig| sig.add_param("attributes", "T.untyped") sig.add_param("block", "T.nilable(T.proc.params(object: #{constant_name}).void)") sig.return_type = constant_name end end end BUILDER_METHODS.each do |method_name| common_relation_methods_module.create_method(method_name.to_s) do |method| method.add_opt_param("attributes", "nil") method.add_block_param("block") method.add_sig do |sig| sig.add_param("block", "T.nilable(T.proc.params(object: #{constant_name}).void)") sig.return_type = constant_name end # `T.untyped` matches `T::Array[T.untyped]` so the array signature # must be defined first for Sorbet to pick it, if valid. method.add_sig do |sig| sig.add_param("attributes", "T::Array[T.untyped]") sig.add_param("block", "T.nilable(T.proc.params(object: #{constant_name}).void)") sig.return_type = "T::Array[#{constant_name}]" end method.add_sig do |sig| sig.add_param("attributes", "T.untyped") sig.add_param("block", "T.nilable(T.proc.params(object: #{constant_name}).void)") sig.return_type = constant_name end end end # We are creating `#new` on the class itself since when called as `Model.new` # it doesn't allow for an array to be passed. If we kept it as a blanket it # would mean the passing any `T.untyped` value to the method would assume # the result is `T::Array` which is not the case majority of the time. model.create_method("new", class_method: true) do |method| method.add_opt_param("attributes", "nil") method.add_block_param("block") method.add_sig do |sig| sig.add_param("attributes", "T.untyped") sig.add_param("block", "T.nilable(T.proc.params(object: #{constant_name}).void)") sig.return_type = constant_name end end end
def create_group_chain_methods(klass)
def create_group_chain_methods(klass) # Calculation methods used with `group` return a hash where the keys cannot be typed # but the values can. Technically a `group` anywhere in the query chain produces # this behavior but to avoid needing to re-type every query method inside this module # we make a simplifying assumption that the calculation method is called immediately # after the group (e.g. `group().count` and not `group().where().count`). The one # exception is `group().having().count` which is fairly idiomatic so that gets handled # without breaking the chain. klass.create_method( "having", parameters: [ create_rest_param("args", type: "T.untyped"), create_block_param("blk", type: "T.untyped"), ], return_type: "T.self_type", ) klass.create_method( "size", return_type: "T::Hash[T.untyped, Integer]", ) CALCULATION_METHODS.each do |method_name| case method_name when :average, :maximum, :minimum klass.create_method( method_name.to_s, parameters: [ create_param("column_name", type: "T.any(String, Symbol)"), ], return_type: "T::Hash[T.untyped, " \ "#{method_name == :average ? "T.any(Integer, Float, BigDecimal)" : "T.untyped"}]", ) when :calculate klass.create_method( "calculate", parameters: [ create_param("operation", type: "Symbol"), create_param("column_name", type: "T.any(String, Symbol)"), ], return_type: "T::Hash[T.untyped, T.any(Integer, Float, BigDecimal)]", ) when :count klass.create_method( "count", parameters: [ create_opt_param("column_name", type: "T.untyped", default: "nil"), ], return_type: "T::Hash[T.untyped, Integer]", ) when :sum klass.create_method( "sum", parameters: [ create_opt_param("column_name", type: "T.nilable(T.any(String, Symbol))", default: "nil"), create_block_param("block", type: "T.nilable(T.proc.params(record: T.untyped).returns(T.untyped))"), ], return_type: "T::Hash[T.untyped, T.any(Integer, Float, BigDecimal)]", ) end end end
def create_relation_class
def create_relation_class superclass = "::ActiveRecord::Relation" # The relation subclass includes the generated relation module model.create_class(RelationClassName, superclass_name: superclass) do |klass| klass.create_include(CommonRelationMethodsModuleName) klass.create_include(RelationMethodsModuleName) klass.create_type_variable("Elem", type: "type_member", fixed: constant_name) TO_ARRAY_METHODS.each do |method_name| klass.create_method(method_name.to_s, return_type: "T::Array[#{constant_name}]") end end create_relation_group_chain_class create_relation_where_chain_class end
def create_relation_group_chain_class
def create_relation_group_chain_class model.create_class(RelationGroupChainClassName, superclass_name: RelationClassName) do |klass| create_group_chain_methods(klass) klass.create_type_variable("Elem", type: "type_member", fixed: constant_name) end end
def create_relation_method(
def create_relation_method( name, parameters: [], relation_return_type: RelationClassName, association_return_type: AssociationRelationClassName ) relation_methods_module.create_method( name.to_s, parameters: parameters, return_type: relation_return_type, ) association_relation_methods_module.create_method( name.to_s, parameters: parameters, return_type: association_return_type, ) end
def create_relation_methods
def create_relation_methods create_relation_method("all") QUERY_METHODS.each do |method_name| case method_name when :where create_where_relation_method when :group create_relation_method( "group", parameters: [ create_rest_param("args", type: "T.untyped"), create_block_param("blk", type: "T.untyped"), ], relation_return_type: RelationGroupChainClassName, association_return_type: AssociationRelationGroupChainClassName, ) when :distinct create_relation_method( method_name.to_s, parameters: [create_opt_param("value", type: "T::Boolean", default: "true")], ) when :extract_associated parameters = [create_param("association", type: "Symbol")] return_type = "T::Array[T.untyped]" relation_methods_module.create_method( method_name.to_s, parameters: parameters, return_type: return_type, ) association_relation_methods_module.create_method( method_name.to_s, parameters: parameters, return_type: return_type, ) when :select [relation_methods_module, association_relation_methods_module].each do |mod| mod.create_method(method_name.to_s) do |method| method.add_rest_param("args") method.add_block_param("blk") method.add_sig do |sig| sig.add_param("args", "T.untyped") sig.return_type = mod == relation_methods_module ? RelationClassName : AssociationRelationClassName end method.add_sig do |sig| sig.add_param("blk", "T.proc.params(record: #{constant_name}).returns(BasicObject)") sig.return_type = "T::Array[#{constant_name}]" end end end else create_relation_method( method_name, parameters: [ create_rest_param("args", type: "T.untyped"), create_block_param("blk", type: "T.untyped"), ], ) end end end
def create_relation_where_chain_class
def create_relation_where_chain_class model.create_class(RelationWhereChainClassName) do |klass| create_where_chain_methods(klass, RelationClassName) klass.create_type_variable("Elem", type: "type_member", fixed: constant_name) end end
def create_where_chain_methods(klass, return_type)
def create_where_chain_methods(klass, return_type) WHERE_CHAIN_QUERY_METHODS.each do |method_name| case method_name when :not klass.create_method( method_name.to_s, parameters: [ create_param("opts", type: "T.untyped"), create_rest_param("rest", type: "T.untyped"), ], return_type: return_type, ) when :associated, :missing klass.create_method( method_name.to_s, parameters: [ create_rest_param("args", type: "T.untyped"), ], return_type: return_type, ) end end end
def create_where_relation_method
def create_where_relation_method relation_methods_module.create_method("where") do |method| method.add_rest_param("args") method.add_sig do |sig| sig.return_type = RelationWhereChainClassName end method.add_sig do |sig| sig.add_param("args", "T.untyped") sig.return_type = RelationClassName end end association_relation_methods_module.create_method("where") do |method| method.add_rest_param("args") method.add_sig do |sig| sig.return_type = AssociationRelationWhereChainClassName end method.add_sig do |sig| sig.add_param("args", "T.untyped") sig.return_type = AssociationRelationClassName end end end
def decorate
def decorate create_classes_and_includes create_common_methods create_relation_methods create_association_relation_methods end
def gather_constants
def gather_constants ActiveRecord::Base.descendants.reject(&:abstract_class?) end
def model
def model @model ||= T.let( root.create_path(constant), T.nilable(RBI::Scope), ) end
def relation_methods_module
def relation_methods_module @relation_methods_module ||= T.let( model.create_module(RelationMethodsModuleName), T.nilable(RBI::Scope), ) end