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, collection proxy, query, spawn, finder, and calculation 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.

  2. 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.

  3. 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.

  2. 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.

  3. 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 sigs inside Ruby files, since that will make the runtime checks fail.

For example, with the following ActiveRecord::Base subclass:

class Post < ApplicationRecord
end

this compiler will produce the RBI file post.rbi with the following content:

# 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_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_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 "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])
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])

Constants included from SorbetHelper

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

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

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

#[], #type_member, #type_template

Methods included from Runtime::Reflection

#ancestors_of, #are_equal?, #class_of, #constant_from_singleton_class, #constant_name_from_singleton_class, #constantize, #constants_of, #descendants_of, #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, #signature_of, #singleton_class_of, #superclass_of

Methods included from RBIHelper

#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



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

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

Instance Method Details

#decorateObject



156
157
158
159
160
161
162
163
164
165
166
167
# File 'lib/tapioca/dsl/compilers/active_record_relations.rb', line 156

def decorate
  root.create_path(constant) do |model|
    relation_methods_module = model.create_module(RelationMethodsModuleName)
    association_relation_methods_module = model.create_module(AssociationRelationMethodsModuleName)
    common_relation_methods_module = model.create_module(CommonRelationMethodsModuleName)

    create_classes_and_includes(model)
    create_common_methods(common_relation_methods_module)
    create_relation_methods(relation_methods_module, association_relation_methods_module)
    create_association_relation_methods(association_relation_methods_module)
  end
end