Class: Tapioca::Dsl::Compilers::ActiveRecordColumns

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

Overview

‘Tapioca::Dsl::Compilers::ActiveRecordColumns` refines RBI files for subclasses of [`ActiveRecord::Base`](api.rubyonrails.org/classes/ActiveRecord/Base.html). This compiler is only responsible for defining the attribute methods that would be created for columns and virtual attributes that are defined in the Active Record model.

This compiler accepts a ‘ActiveRecordColumnTypes` option that can be used to specify how the types of the column related methods should be generated. The option can be one of the following:

- `persisted` (_default_): The methods will be generated with the type that matches the actual database
column type as the return type. This means that if the column is a string, the method return type
will be `String`, but if the column is also nullable, then the return type will be `T.nilable(String)`. This
mode basically treats each model as if it was a valid and persisted model. Note that this makes typing
Active Record models easier, but does not match the behaviour of non-persisted or invalid models, which can
have all kinds of non-sensical values in their column attributes.
- `nilable`: All column methods will be generated with `T.nilable` return types. This is strictly the most
correct way to type the methods, but it can make working with the models more cumbersome, as you will have to
handle the `nil` cases explicitly using `T.must` or the safe navigation operator `&.`, even for valid
persisted models.
- `untyped`: The methods will be generated with `T.untyped` return types. This mode is practical if you are not
ready to start typing your models strictly yet, but still want to generate RBI files for them.

For example, with the following model class: ~~~rb class Post < ActiveRecord::Base end ~~~

and the following database schema:

~~~rb # db/schema.rb create_table :posts do |t|

t.string :title, null: false
t.string :body
t.boolean :published
t.timestamps

end ~~~

this compiler will, by default, produce the following methods in the RBI file ‘post.rbi`:

~~~rbi # post.rbi # typed: true class Post

include GeneratedAttributeMethods

module GeneratedAttributeMethods
  sig { returns(T.nilable(::String)) }
  def body; end

  sig { params(value: T.nilable(::String)).returns(T.nilable(::String)) }
  def body=; end

  sig { returns(T::Boolean) }
  def body?; end

  sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
  def created_at; end

  sig { params(value: ::ActiveSupport::TimeWithZone).returns(::ActiveSupport::TimeWithZone) }
  def created_at=; end

  sig { returns(T::Boolean) }
  def created_at?; end

  sig { returns(T.nilable(T::Boolean)) }
  def published; end

  sig { params(value: T::Boolean).returns(T::Boolean) }
  def published=; end

  sig { returns(T::Boolean) }
  def published?; end

  sig { returns(::String) }
  def title; end

  sig { params(value: ::String).returns(::String) }
  def title=(value); end

  sig { returns(T::Boolean) }
  def title?; end

  sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
  def updated_at; end

  sig { params(value: ::ActiveSupport::TimeWithZone).returns(::ActiveSupport::TimeWithZone) }
  def updated_at=; end

  sig { returns(T::Boolean) }
  def updated_at?; end

  ## Also the methods added by https://api.rubyonrails.org/classes/ActiveRecord/AttributeMethods/Dirty.html
  ## Also the methods added by https://api.rubyonrails.org/classes/ActiveModel/Dirty.html
  ## Also the methods added by https://api.rubyonrails.org/classes/ActiveRecord/AttributeMethods/BeforeTypeCast.html
end

end ~~~

However, if ‘ActiveRecordColumnTypes` is set to `nilable`, the `title` method will be generated as: ~~~rbi

sig { returns(T.nilable(::String)) }
def title; end

~~~ and if the option is set to ‘untyped`, the `title` method will be generated as: ~~~rbi

sig { returns(T.untyped) }
def title; end

~~~

Constant Summary collapse

ConstantType =
type_member { { fixed: T.class_of(ActiveRecord::Base) } }

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

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

Instance Attribute Summary

Attributes inherited from Tapioca::Dsl::Compiler

#constant, #options, #root

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Tapioca::Dsl::Compiler

#add_error, #compiler_enabled?, handles?, #initialize, processable_constants, requested_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, #signature_of!, #singleton_class_of, #superclass_of

Methods included from Runtime::AttachedClassOf

#attached_class_of

Methods included from RBIHelper

#as_nilable_type, #as_non_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?

Methods included from SorbetHelper

#sorbet, #sorbet_path, #sorbet_supports?

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_columns.rb', line 169

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

Instance Method Details

#decorateObject



130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
# File 'lib/tapioca/dsl/compilers/active_record_columns.rb', line 130

def decorate
  return unless constant.table_exists?

  # We need to call this to ensure that some attribute aliases are defined, e.g.
  # `id_value` as an alias for `id`.
  # I think this is a regression on Rails 7.1, but we are where we are.
  constant.define_attribute_methods

  root.create_path(constant) do |model|
    model.create_module(AttributeMethodsModuleName) do |mod|
      (constant.attribute_names + ["id"]).uniq.each do |attribute_name|
        add_methods_for_attribute(mod, attribute_name)
      end

      constant.attribute_aliases.each do |attribute_name, column_name|
        attribute_name = attribute_name.to_s
        column_name = column_name.to_s
        patterns = if constant.respond_to?(:attribute_method_patterns)
          # https://github.com/rails/rails/pull/44367
          constant.attribute_method_patterns
        else
          T.unsafe(constant).attribute_method_matchers
        end
        new_method_names = patterns.map { |m| m.method_name(attribute_name) }
        old_method_names = patterns.map { |m| m.method_name(column_name) }
        methods_to_add = new_method_names - old_method_names

        add_methods_for_attribute(mod, attribute_name, column_name, methods_to_add)
      end
    end

    model.create_include(AttributeMethodsModuleName)
  end
end