class SorbetRails::ModelPlugins::ActiveRecordAssoc
Public Class Methods
new(model_class, available_classes)
click to toggle source
Calls superclass method
SorbetRails::ModelPlugins::Base::new
# File lib/sorbet-rails/model_plugins/active_record_assoc.rb, line 5 def initialize(model_class, available_classes) super @columns_hash = @model_class.table_exists? ? @model_class.columns_hash : {} end
Public Instance Methods
assoc_should_be_untyped?(reflection)
click to toggle source
# File lib/sorbet-rails/model_plugins/active_record_assoc.rb, line 171 def assoc_should_be_untyped?(reflection) # For some polymorphic associations (e.g. a has-many-through where the `source` # is polymorphic) we can figure out the type from the class_name or source_type. polymorpic_with_unknowable_klass = ( polymorphic_assoc?(reflection) && !reflection.options.key?(:class_name) && !reflection.options.key?(:source_type) ) polymorpic_with_unknowable_klass || !@available_classes.include?(reflection.klass.name) end
generate(root)
click to toggle source
# File lib/sorbet-rails/model_plugins/active_record_assoc.rb, line 11 def generate(root) return unless @model_class.reflections.length > 0 assoc_module_name = self.model_module_name("GeneratedAssociationMethods") assoc_module_rbi = root.create_module(assoc_module_name) model_class_rbi = root.create_class(self.model_class_name) model_class_rbi.create_include(assoc_module_name) @model_class.reflections.sort.each do |assoc_name, reflection| reflection.collection? ? populate_collection_assoc_getter_setter(assoc_module_rbi, assoc_name, reflection) : populate_single_assoc_getter_setter(assoc_module_rbi, assoc_name, reflection) end end
polymorphic_assoc?(reflection)
click to toggle source
# File lib/sorbet-rails/model_plugins/active_record_assoc.rb, line 190 def polymorphic_assoc?(reflection) reflection.through_reflection ? polymorphic_assoc?(reflection.source_reflection) : reflection.polymorphic? end
populate_collection_assoc_getter_setter(assoc_module_rbi, assoc_name, reflection)
click to toggle source
# File lib/sorbet-rails/model_plugins/active_record_assoc.rb, line 129 def populate_collection_assoc_getter_setter(assoc_module_rbi, assoc_name, reflection) # TODO allow people to specify the possible values of polymorphic associations assoc_class = assoc_should_be_untyped?(reflection) ? "T.untyped" : "::#{reflection.klass.name}" relation_class = relation_should_be_untyped?(reflection) ? "ActiveRecord::Associations::CollectionProxy" : "#{assoc_class}::ActiveRecord_Associations_CollectionProxy" assoc_module_rbi.create_method( assoc_name.to_s, return_type: relation_class, ) unless assoc_should_be_untyped?(reflection) id_type = "T.untyped" if reflection.klass.table_exists? # For DB views, the PK column would not exist. id_column = reflection.klass.primary_key if id_column id_column_def = reflection.klass.columns_hash[id_column] # Normally the id_type is an Integer, but it could be a String if using # UUIDs. id_type = type_for_column_def(id_column_def).to_s if id_column_def end end assoc_module_rbi.create_method( "#{assoc_name.singularize}_ids", return_type: "T::Array[#{id_type}]", ) end assoc_module_rbi.create_method( "#{assoc_name}=", parameters: [ Parameter.new("value", type: "T::Enumerable[#{assoc_class}]") ], return_type: nil, ) end
populate_single_assoc_getter_setter(assoc_module_rbi, assoc_name, reflection)
click to toggle source
# File lib/sorbet-rails/model_plugins/active_record_assoc.rb, line 34 def populate_single_assoc_getter_setter(assoc_module_rbi, assoc_name, reflection) # TODO allow people to specify the possible values of polymorphic associations assoc_class = assoc_should_be_untyped?(reflection) ? "T.untyped" : "::#{reflection.klass.name}" assoc_type = (belongs_to_and_required?(reflection) || has_one_and_required?(reflection)) ? assoc_class : "T.nilable(#{assoc_class})" params = [ Parameter.new("*args", type: "T.untyped"), Parameter.new("&block", type: "T.nilable(T.proc.params(object: #{assoc_class}).void)") ] assoc_module_rbi.create_method( assoc_name.to_s, return_type: assoc_type, ) assoc_module_rbi.create_method( "build_#{assoc_name}", parameters: params, return_type: assoc_class, ) assoc_module_rbi.create_method( "create_#{assoc_name}", parameters: params, return_type: assoc_class, ) assoc_module_rbi.create_method( "create_#{assoc_name}!", parameters: params, return_type: assoc_class, ) assoc_module_rbi.create_method( "#{assoc_name}=", parameters: [ Parameter.new("value", type: assoc_type) ], return_type: nil, ) assoc_module_rbi.create_method( "reload_#{assoc_name}", return_type: assoc_type, ) end
relation_should_be_untyped?(reflection)
click to toggle source
# File lib/sorbet-rails/model_plugins/active_record_assoc.rb, line 184 def relation_should_be_untyped?(reflection) # only type the relation we'll generate assoc_should_be_untyped?(reflection) || !@available_classes.include?(reflection.klass.name) end
Private Instance Methods
belongs_to_and_required?(reflection)
click to toggle source
# File lib/sorbet-rails/model_plugins/active_record_assoc.rb, line 77 def belongs_to_and_required?(reflection) # In Rails 5 and later, belongs_to are required unless specified to be # optional (via `optional` or `!required` or `!belongs_to_required_by_default`) return false if !reflection.belongs_to? column_def = @columns_hash[reflection.foreign_key.to_s] db_required_config = column_def.present? && !column_def.null rails_required_config = if reflection.options.key?(:required) !!reflection.options[:required] elsif reflection.options.key?(:optional) !reflection.options[:optional] else !!reflection.active_record.belongs_to_required_by_default end # We check for validations on both the column name (e.g. wizard_id) and # association name (e.g. wizard). rails_required_config ||= [column_def&.name, reflection.name].compact.any? { |n| attribute_has_unconditional_presence_validation?(n) } if rails_required_config && !db_required_config puts "Warning: belongs_to association #{reflection.name} is required at the application level but **nullable** at the DB level.\n Add a constraint at the DB level (using `null: false` and foreign key constraint) to ensure it is enforced.".squish! elsif !rails_required_config && db_required_config if habtm_class? puts "Note: belongs_to association #{reflection.name} is specified as not-null at the DB level but will always be **optional** at the application level since it's part of a has_and_belongs_to_many association.\n To resolve move to a 'has_many through:' association.".squish! else puts "Note: belongs_to association #{reflection.name} is specified as not-null at the DB level but **optional** at the application level.\n Add a constraint at the app level (using `optional: false`) as a validation hint to Rails.".squish! end end rails_required_config || db_required_config end
has_one_and_required?(reflection)
click to toggle source
# File lib/sorbet-rails/model_plugins/active_record_assoc.rb, line 118 def has_one_and_required?(reflection) !!(reflection.has_one? && attribute_has_unconditional_presence_validation?(reflection.name)) end