Class: Tapioca::Dsl::Compilers::ActiveRecordRelations

Inherits:
Tapioca::Dsl::Compiler show all
Extended by:
T::Sig
Includes:
Helpers::ActiveRecordConstantsHelper, SorbetHelper
Defined in:
lib/tapioca/dsl/compilers/active_record_relations.rb

Overview

‘Tapioca::Dsl::Compilers::ActiveRecordRelations` decorates RBI files for subclasses of `ActiveRecord::Base` and adds [relation](api.rubyonrails.org/classes/ActiveRecord/Relation.html), [collection proxy](api.rubyonrails.org/classes/ActiveRecord/Associations/CollectionProxy.html), [query](api.rubyonrails.org/classes/ActiveRecord/QueryMethods.html), [spawn](api.rubyonrails.org/classes/ActiveRecord/SpawnMethods.html), [finder](api.rubyonrails.org/classes/ActiveRecord/FinderMethods.html), and [calculation](api.rubyonrails.org/classes/ActiveRecord/Calculations.html) methods.

The compiler defines 3 (synthetic) modules and 3 (synthetic) classes to represent relations properly.

For a given model ‘Model`, we generate the following classes:

  1. A ‘Model::PrivateRelation` that subclasses `ActiveRecord::Relation`. This synthetic class represents

a relation on ‘Model` whose methods which return a relation always return a `Model::PrivateRelation` instance.

  1. ‘Model::PrivateAssocationRelation` that subclasses `ActiveRecord::AssociationRelation`. This synthetic

class represents a relation on a singular association of type ‘Model` (e.g. `foo.model`) whose methods which return a relation will always return a `Model::PrivateAssocationRelation` instance. The difference between this class and the previous one is mainly that an association relation also keeps track of the resource association for this relation.

  1. ‘Model::PrivateCollectionProxy` that subclasses from `ActiveRecord::Associations::CollectionProxy`.

This synthetic class represents a relation on a plural association of type ‘Model` (e.g. `foo.models`) whose methods which return a relation will always return a `Model::PrivateAssocationRelation` instance. This class represents a collection of `Model` instances with some extra methods to `build`, `create`, etc new `Model` instances in the collection.

and the following modules:

  1. ‘Model::GeneratedRelationMethods` holds all the relation methods with the return type of

‘Model::PrivateRelation`. For example, calling `all` on the `Model` class or an instance of `Model::PrivateRelation` class will always return a `Model::PrivateRelation` instance, thus the signature of `all` is defined with that return type in this module.

  1. ‘Model::GeneratedAssociationRelationMethods` holds all the relation methods with the return type

of ‘Model::PrivateAssociationRelation`. For example, calling `all` on an instance of `Model::PrivateAssociationRelation` or an instance of `Model::PrivateCollectionProxy` class will always return a `Model::PrivateAssociationRelation` instance, thus the signature of `all` is defined with that return type in this module.

  1. ‘Model::CommonRelationMethods` holds all the relation methods that do not depend on the type of

relation in their return type. For example, ‘find_by!` will always return the same type (a `Model` instance), regardless of what kind of relation it is called on, and so belongs in this module. This module is used to reduce the replication of methods between the previous two modules.

Additionally, the actual ‘Model` class extends both `Model::CommonRelationMethods` and `Model::PrivateRelation` modules, so that, for example, `find_by` and `all` can be chained off of the `Model` class.

CAUTION: The generated relation classes are named ‘PrivateXXX` intentionally to reflect the fact that they represent private subconstants of the Active Record model. As such, these types do not exist at runtime, and their counterparts that do exist at runtime are marked `private_constant` anyway. For that reason, these types cannot be used in user code or in `sig`s inside Ruby files, since that will make the runtime checks fail.

For example, with 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 CommonRelationMethods
extend GeneratedRelationMethods

module CommonRelationMethods
  sig { params(block: T.nilable(T.proc.params(record: ::Post).returns(T.untyped))).returns(T::Boolean) }
  def any?(&block); end

  # ...
end

module GeneratedAssociationRelationMethods
  sig { returns(PrivateAssociationRelation) }
  def all; end

  # ...

  sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) }
  def where(*args, &blk); end
end

module GeneratedRelationMethods
  sig { returns(PrivateRelation) }
  def all; end

  # ...

  sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) }
  def where(*args, &blk); end
end

class PrivateAssociationRelation < ::ActiveRecord::AssociationRelation
  include CommonRelationMethods
  include GeneratedAssociationRelationMethods

  sig { returns(T::Array[::Post]) }
  def to_a; end

  sig { returns(T::Array[::Post]) }
  def to_ary; end

  Elem = type_member { { fixed: ::Post } }
end

class PrivateCollectionProxy < ::ActiveRecord::Associations::CollectionProxy
  include CommonRelationMethods
  include GeneratedAssociationRelationMethods

  sig do
    params(records: T.any(::Post, T::Array[::Post], T::Array[PrivateCollectionProxy]))
      .returns(PrivateCollectionProxy)
  end
  def <<(*records); end

  # ...
end

class PrivateRelation < ::ActiveRecord::Relation
  include CommonRelationMethods
  include GeneratedRelationMethods

  sig { returns(T::Array[::Post]) }
  def to_a; end

  sig { returns(T::Array[::Post]) }
  def to_ary; end

  Elem = type_member { { fixed: ::Post } }
end

end ~~~

Constant Summary collapse

ConstantType =
type_member { { fixed: T.class_of(::ActiveRecord::Base) } }
ASSOCIATION_METHODS =
T.let(
  ::ActiveRecord::AssociationRelation.instance_methods -
    ::ActiveRecord::Relation.instance_methods,
  T::Array[Symbol],
)
COLLECTION_PROXY_METHODS =
T.let(
  ::ActiveRecord::Associations::CollectionProxy.instance_methods -
    ::ActiveRecord::AssociationRelation.instance_methods,
  T::Array[Symbol],
)
QUERY_METHODS =
T.let(
  begin
    # Grab all Query methods
    query_methods = ActiveRecord::QueryMethods.instance_methods(false)
    # Grab all Spawn methods
    query_methods |= ActiveRecord::SpawnMethods.instance_methods(false)
    # Remove the ones we know are private API
    query_methods -= [:arel, :build_subquery, :construct_join_dependency, :extensions, :spawn]
    # Remove "group" which needs a custom return type for GroupChains
    query_methods -= [:group]
    # Remove "where" which needs a custom return type for WhereChains
    query_methods -= [:where]
    # Remove the methods that ...
    query_methods
      .grep_v(/_clause$/) # end with "_clause"
      .grep_v(/_values?$/) # end with "_value" or "_values"
      .grep_v(/=$/) # end with "=""
      .grep_v(/(?<!uniq)!$/) # end with "!" except for "uniq!"
  end,
  T::Array[Symbol],
)
WHERE_CHAIN_QUERY_METHODS =
T.let(
  ActiveRecord::QueryMethods::WhereChain.instance_methods(false),
  T::Array[Symbol],
)
FINDER_METHODS =
T.let(ActiveRecord::FinderMethods.instance_methods(false), T::Array[Symbol])
SIGNED_FINDER_METHODS =
T.let(
  defined?(ActiveRecord::SignedId) ? ActiveRecord::SignedId::ClassMethods.instance_methods(false) : [],
  T::Array[Symbol],
)
BATCHES_METHODS =
T.let(ActiveRecord::Batches.instance_methods(false), T::Array[Symbol])
CALCULATION_METHODS =
T.let(ActiveRecord::Calculations.instance_methods(false), T::Array[Symbol])
ENUMERABLE_QUERY_METHODS =
T.let([:any?, :many?, :none?, :one?], T::Array[Symbol])
FIND_OR_CREATE_METHODS =
T.let(
  [:find_or_create_by, :find_or_create_by!, :find_or_initialize_by, :create_or_find_by, :create_or_find_by!],
  T::Array[Symbol],
)
BUILDER_METHODS =
T.let([:new, :build, :create, :create!], T::Array[Symbol])
TO_ARRAY_METHODS =
T.let([:to_ary, :to_a], T::Array[Symbol])

Constants included from SorbetHelper

SorbetHelper::FEATURE_REQUIREMENTS, SorbetHelper::SORBET_BIN, SorbetHelper::SORBET_EXE_PATH_ENV_VAR, SorbetHelper::SORBET_GEM_SPEC, SorbetHelper::SORBET_PAYLOAD_URL, SorbetHelper::SPOOM_CONTEXT

Constants included from Helpers::ActiveRecordConstantsHelper

Helpers::ActiveRecordConstantsHelper::AssociationMethodsModuleName, Helpers::ActiveRecordConstantsHelper::AssociationRelationClassName, Helpers::ActiveRecordConstantsHelper::AssociationRelationGroupChainClassName, Helpers::ActiveRecordConstantsHelper::AssociationRelationMethodsModuleName, Helpers::ActiveRecordConstantsHelper::AssociationRelationWhereChainClassName, Helpers::ActiveRecordConstantsHelper::AssociationsCollectionProxyClassName, Helpers::ActiveRecordConstantsHelper::AttributeMethodsModuleName, Helpers::ActiveRecordConstantsHelper::CommonRelationMethodsModuleName, Helpers::ActiveRecordConstantsHelper::DelegatedTypesModuleName, Helpers::ActiveRecordConstantsHelper::ReflectionType, Helpers::ActiveRecordConstantsHelper::RelationClassName, Helpers::ActiveRecordConstantsHelper::RelationGroupChainClassName, Helpers::ActiveRecordConstantsHelper::RelationMethodsModuleName, Helpers::ActiveRecordConstantsHelper::RelationWhereChainClassName, Helpers::ActiveRecordConstantsHelper::SecureTokensModuleName, Helpers::ActiveRecordConstantsHelper::StoredAttributesModuleName

Constants included from Runtime::Reflection

Runtime::Reflection::ANCESTORS_METHOD, Runtime::Reflection::CLASS_METHOD, Runtime::Reflection::CONSTANTS_METHOD, Runtime::Reflection::EQUAL_METHOD, Runtime::Reflection::METHOD_METHOD, Runtime::Reflection::NAME_METHOD, Runtime::Reflection::OBJECT_ID_METHOD, Runtime::Reflection::PRIVATE_INSTANCE_METHODS_METHOD, Runtime::Reflection::PROTECTED_INSTANCE_METHODS_METHOD, Runtime::Reflection::PUBLIC_INSTANCE_METHODS_METHOD, Runtime::Reflection::REQUIRED_FROM_LABELS, Runtime::Reflection::SINGLETON_CLASS_METHOD, Runtime::Reflection::SUPERCLASS_METHOD, Runtime::Reflection::UNDEFINED_CONSTANT

Instance Attribute Summary

Attributes inherited from Tapioca::Dsl::Compiler

#constant, #root

Class Method Summary collapse

Instance Method Summary collapse

Methods included from SorbetHelper

#sorbet, #sorbet_path, #sorbet_supports?

Methods inherited from Tapioca::Dsl::Compiler

#add_error, #compiler_enabled?, handles?, #initialize, processable_constants

Methods included from T::Generic::TypeStoragePatch

#[], #has_attached_class!, #type_member, #type_template

Methods included from Runtime::Reflection

#abstract_type_of, #ancestors_of, #are_equal?, #class_of, #constant_defined?, #constantize, #constants_of, #descendants_of, #file_candidates_for, #final_module?, #inherited_ancestors_of, #method_of, #name_of, #name_of_type, #object_id_of, #private_instance_methods_of, #protected_instance_methods_of, #public_instance_methods_of, #qualified_name_of, #resolve_loc, #sealed_module?, #signature_of, #singleton_class_of, #superclass_of

Methods included from Runtime::AttachedClassOf

#attached_class_of

Methods included from RBIHelper

#as_nilable_type, #create_block_param, #create_kw_opt_param, #create_kw_param, #create_kw_rest_param, #create_opt_param, #create_param, #create_rest_param, #create_typed_param, #sanitize_signature_types, serialize_type_variable, #valid_method_name?, #valid_parameter_name?

Constructor Details

This class inherits a constructor from Tapioca::Dsl::Compiler

Class Method Details

.gather_constantsObject



169
170
171
# File 'lib/tapioca/dsl/compilers/active_record_relations.rb', line 169

def gather_constants
  ActiveRecord::Base.descendants.reject(&:abstract_class?)
end

Instance Method Details

#decorateObject



158
159
160
161
162
163
# File 'lib/tapioca/dsl/compilers/active_record_relations.rb', line 158

def decorate
  create_classes_and_includes
  create_common_methods
  create_relation_methods
  create_association_relation_methods
end