# typed: strict # frozen_string_literal: true begin require "active_record" rescue LoadError return end require "tapioca/compilers/dsl/helper/active_record_constants" module Tapioca module Compilers module Dsl # `Tapioca::Compilers::Dsl::ActiveRecordColumns` refines RBI files for subclasses of # [`ActiveRecord::Base`](https://api.rubyonrails.org/classes/ActiveRecord/Base.html). # This generator is only responsible for defining the attribute methods that would be # created for the columns that are defined in the Active Record model. # # 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 generator will 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 # ~~~ class ActiveRecordColumns < Base extend T::Sig include Helper::ActiveRecordConstants sig { override.params(root: RBI::Tree, constant: T.class_of(ActiveRecord::Base)).void } def decorate(root, constant) return unless constant.table_exists? root.create_path(constant) do |model| model.create_module(AttributeMethodsModuleName) do |mod| constant.columns_hash.each_key do |column_name| column_name = column_name.to_s add_methods_for_attribute(mod, constant, column_name) end constant.attribute_aliases.each do |attribute_name, column_name| attribute_name = attribute_name.to_s column_name = column_name.to_s new_method_names = constant.attribute_method_matchers.map { |m| m.method_name(attribute_name) } old_method_names = constant.attribute_method_matchers.map { |m| m.method_name(column_name) } methods_to_add = new_method_names - old_method_names add_methods_for_attribute(mod, constant, column_name, attribute_name, methods_to_add) end end model.create_include(AttributeMethodsModuleName) end end sig { override.returns(T::Enumerable[Module]) } def gather_constants descendants_of(::ActiveRecord::Base).reject(&:abstract_class?) end private sig do params( klass: RBI::Scope, name: String, methods_to_add: T.nilable(T::Array[String]), return_type: String, parameters: T::Array[[String, String]] ).void end def add_method(klass, name, methods_to_add, return_type: "void", parameters: []) klass.create_method( name, parameters: parameters.map { |param, type| create_param(param, type: type) }, return_type: return_type ) if methods_to_add.nil? || methods_to_add.include?(name) end sig do params( klass: RBI::Scope, constant: T.class_of(ActiveRecord::Base), column_name: String, attribute_name: String, methods_to_add: T.nilable(T::Array[String]) ).void end def add_methods_for_attribute(klass, constant, column_name, attribute_name = column_name, methods_to_add = nil) getter_type, setter_type = ActiveRecordColumnTypeHelper.new(constant).type_for(column_name) # Added by ActiveRecord::AttributeMethods::Read # add_method( klass, attribute_name.to_s, methods_to_add, return_type: getter_type ) # Added by ActiveRecord::AttributeMethods::Write # add_method( klass, "#{attribute_name}=", methods_to_add, parameters: [["value", setter_type]], return_type: setter_type ) # Added by ActiveRecord::AttributeMethods::Query # add_method( klass, "#{attribute_name}?", methods_to_add, return_type: "T::Boolean" ) # Added by ActiveRecord::AttributeMethods::Dirty # add_method( klass, "#{attribute_name}_before_last_save", methods_to_add, return_type: as_nilable_type(getter_type) ) add_method( klass, "#{attribute_name}_change_to_be_saved", methods_to_add, return_type: "T.nilable([#{getter_type}, #{getter_type}])" ) add_method( klass, "#{attribute_name}_in_database", methods_to_add, return_type: as_nilable_type(getter_type) ) add_method( klass, "saved_change_to_#{attribute_name}", methods_to_add, return_type: "T.nilable([#{getter_type}, #{getter_type}])" ) add_method( klass, "saved_change_to_#{attribute_name}?", methods_to_add, return_type: "T::Boolean" ) add_method( klass, "will_save_change_to_#{attribute_name}?", methods_to_add, return_type: "T::Boolean" ) # Added by ActiveModel::Dirty # add_method( klass, "#{attribute_name}_change", methods_to_add, return_type: "T.nilable([#{getter_type}, #{getter_type}])" ) add_method( klass, "#{attribute_name}_changed?", methods_to_add, return_type: "T::Boolean" ) add_method( klass, "#{attribute_name}_will_change!", methods_to_add ) add_method( klass, "#{attribute_name}_was", methods_to_add, return_type: as_nilable_type(getter_type) ) add_method( klass, "#{attribute_name}_previous_change", methods_to_add, return_type: "T.nilable([#{getter_type}, #{getter_type}])" ) add_method( klass, "#{attribute_name}_previously_changed?", methods_to_add, return_type: "T::Boolean" ) add_method( klass, "#{attribute_name}_previously_was", methods_to_add, return_type: as_nilable_type(getter_type) ) add_method( klass, "restore_#{attribute_name}!", methods_to_add ) # Added by ActiveRecord::AttributeMethods::BeforeTypeCast # add_method( klass, "#{attribute_name}_before_type_cast", methods_to_add, return_type: "T.untyped" ) add_method( klass, "#{attribute_name}_came_from_user?", methods_to_add, return_type: "T::Boolean" ) end sig { params(type: String).returns(String) } def as_nilable_type(type) return type if type.start_with?("T.nilable(") "T.nilable(#{type})" end end end end end