Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add have_delegated_type matcher #1606

Merged
merged 2 commits into from
Mar 15, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
340 changes: 340 additions & 0 deletions lib/shoulda/matchers/active_record/association_matcher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,316 @@ def belong_to(name)
AssociationMatcher.new(:belongs_to, name)
end

# The `have_delegated_type` matcher is used to ensure that a `belong_to` association
# exists on your model using the delegated_type macro.
#
# class Vehicle < ActiveRecord::Base
# delegated_type :drivable, types: %w(Car Truck)
# end
#
# # RSpec
# RSpec.describe Vehicle, type: :model do
# it { should have_delegated_type(:drivable) }
# end
#
# # Minitest (Shoulda)
# class VehicleTest < ActiveSupport::TestCase
# should have_delegated_type(:drivable)
# end
#
# #### Qualifiers
#
# ##### types
#
# Use `types` to test the types that are allowed for the association.
#
# class Vehicle < ActiveRecord::Base
# delegated_type :drivable, types: %w(Car Truck)
# end
#
# # RSpec
# RSpec.describe Vehicle, type: :model do
# it do
# should have_delegated_type(:drivable).
# types(%w(Car Truck))
# end
# end
#
# # Minitest (Shoulda)
# class VehicleTest < ActiveSupport::TestCase
# should have_delegated_type(:drivable).
# types(%w(Car Truck))
# end
#
# ##### conditions
#
# Use `conditions` if your association is defined with a scope that sets
# the `where` clause.
#
# class Vehicle < ActiveRecord::Base
# delegated_type :drivable, types: %w(Car Truck), scope: -> { where(with_wheels: true) }
# end
#
# # RSpec
# RSpec.describe Vehicle, type: :model do
# it do
# should have_delegated_type(:drivable).
# conditions(with_wheels: true)
# end
# end
#
# # Minitest (Shoulda)
# class VehicleTest < ActiveSupport::TestCase
# should have_delegated_type(:drivable).
# conditions(everyone_is_perfect: false)
# end
#
# ##### order
#
# Use `order` if your association is defined with a scope that sets the
# `order` clause.
#
# class Person < ActiveRecord::Base
# delegated_type :drivable, types: %w(Car Truck), scope: -> { order('wheels desc') }
# end
#
# # RSpec
# RSpec.describe Vehicle, type: :model do
# it { should have_delegated_type(:drivable).order('wheels desc') }
# end
#
# # Minitest (Shoulda)
# class VehicleTest < ActiveSupport::TestCase
# should have_delegated_type(:drivable).order('wheels desc')
# end
#
# ##### with_primary_key
#
# Use `with_primary_key` to test usage of the `:primary_key` option.
#
# class Vehicle < ActiveRecord::Base
# delegated_type :drivable, types: %w(Car Truck), primary_key: 'vehicle_id'
# end
#
# # RSpec
# RSpec.describe Vehicle, type: :model do
# it do
# should have_delegated_type(:drivable).
# with_primary_key('vehicle_id')
# end
# end
#
# # Minitest (Shoulda)
# class VehicleTest < ActiveSupport::TestCase
# should have_delegated_type(:drivable).
# with_primary_key('vehicle_id')
# end
#
# ##### with_foreign_key
#
# Use `with_foreign_key` to test usage of the `:foreign_key` option.
#
# class Vehicle < ActiveRecord::Base
# delegated_type :drivable, types: %w(Car Truck), foreign_key: 'drivable_uuid'
# end
#
# # RSpec
# RSpec.describe Vehicle, type: :model do
# it do
# should have_delegated_type(:drivable).
# with_foreign_key('drivable_uuid')
# end
# end
#
# # Minitest (Shoulda)
# class VehicleTest < ActiveSupport::TestCase
# should have_delegated_type(:drivable).
# with_foreign_key('drivable_uuid')
# end
#
# ##### dependent
#
# Use `dependent` to assert that the `:dependent` option was specified.
#
# class Vehicle < ActiveRecord::Base
# delegated_type :drivable, types: %w(Car Truck), dependent: :destroy
# end
#
# # RSpec
# RSpec.describe Vehicle, type: :model do
# it { should have_delegated_type(:drivable).dependent(:destroy) }
# end
#
# # Minitest (Shoulda)
# class VehicleTest < ActiveSupport::TestCase
# should have_delegated_type(:drivable).dependent(:destroy)
# end
#
# To assert that *any* `:dependent` option was specified, use `true`:
#
# # RSpec
# RSpec.describe Vehicle, type: :model do
# it { should have_delegated_type(:drivable).dependent(true) }
# end
#
# To assert that *no* `:dependent` option was specified, use `false`:
#
# class Vehicle < ActiveRecord::Base
# delegated_type :drivable, types: %w(Car Truck)
# end
#
# # RSpec
# RSpec.describe Vehicle, type: :model do
# it { should have_delegated_type(:drivable).dependent(false) }
# end
#
# ##### counter_cache
#
# Use `counter_cache` to assert that the `:counter_cache` option was
# specified.
#
# class Vehicle < ActiveRecord::Base
# delegated_type :drivable, types: %w(Car Truck), counter_cache: true
# end
#
# # RSpec
# RSpec.describe Vehicle, type: :model do
# it { should have_delegated_type(:drivable).counter_cache(true) }
# end
#
# # Minitest (Shoulda)
# class VehicleTest < ActiveSupport::TestCase
# should have_delegated_type(:drivable).counter_cache(true)
# end
#
# ##### touch
#
# Use `touch` to assert that the `:touch` option was specified.
#
# class Vehicle < ActiveRecord::Base
# delegated_type :drivable, types: %w(Car Truck), touch: true
# end
#
# # RSpec
# RSpec.describe Vehicle, type: :model do
# it { should have_delegated_type(:drivable).touch(true) }
# end
#
# # Minitest (Shoulda)
# class VehicleTest < ActiveSupport::TestCase
# should have_delegated_type(:drivable).touch(true)
# end
#
# ##### autosave
#
# Use `autosave` to assert that the `:autosave` option was specified.
#
# class Vehicle < ActiveRecord::Base
# delegated_type :drivable, types: %w(Car Truck), autosave: true
# end
#
# # RSpec
# RSpec.describe Vehicle, type: :model do
# it { should have_delegated_type(:drivable).autosave(true) }
# end
#
# # Minitest (Shoulda)
# class VehicleTest < ActiveSupport::TestCase
# should have_delegated_type(:drivable).autosave(true)
# end
#
# ##### inverse_of
#
# Use `inverse_of` to assert that the `:inverse_of` option was specified.
#
# class Vehicle < ActiveRecord::Base
# delegated_type :drivable, types: %w(Car Truck), inverse_of: :vehicle
# end
#
# # RSpec
# describe Vehicle
# it { should have_delegated_type(:drivable).inverse_of(:vehicle) }
# end
#
# # Minitest (Shoulda)
# class VehicleTest < ActiveSupport::TestCase
# should have_delegated_type(:drivable).inverse_of(:vehicle)
# end
#
# ##### required
#
# Use `required` to assert that the association is not allowed to be nil.
# (Enabled by default in Rails 5+.)
#
# class Vehicle < ActiveRecord::Base
# delegated_type :drivable, types: %w(Car Truck), required: true
# end
#
# # RSpec
# describe Vehicle
# it { should have_delegated_type(:drivable).required }
# end
#
# # Minitest (Shoulda)
# class VehicleTest < ActiveSupport::TestCase
# should have_delegated_type(:drivable).required
# end
#
# ##### without_validating_presence
#
# Use `without_validating_presence` with `belong_to` to prevent the
# matcher from checking whether the association disallows nil (Rails 5+
# only). This can be helpful if you have a custom hook that always sets
# the association to a meaningful value:
#
# class Vehicle < ActiveRecord::Base
# delegated_type :drivable, types: %w(Car Truck)
#
# before_validation :autoassign_drivable
#
# private
#
# def autoassign_drivable
# self.drivable = Car.create!
# end
# end
#
# # RSpec
# describe Vehicle
# it { should have_delegated_type(:drivable).without_validating_presence }
# end
#
# # Minitest (Shoulda)
# class VehicleTest < ActiveSupport::TestCase
# should have_delegated_type(:drivable).without_validating_presence
# end
#
# ##### optional
#
# Use `optional` to assert that the association is allowed to be nil.
# (Rails 5+ only.)
#
# class Vehicle < ActiveRecord::Base
# delegated_type :drivable, types: %w(Car Truck), optional: true
# end
#
# # RSpec
# describe Vehicle
# it { should have_delegated_type(:drivable).optional }
# end
#
# # Minitest (Shoulda)
# class VehicleTest < ActiveSupport::TestCase
# should have_delegated_type(:drivable).optional
# end
#
# @return [AssociationMatcher]
#

def have_delegated_type(name)
AssociationMatcher.new(:belongs_to, name)
end

# The `have_many` matcher is used to test that a `has_many` or `has_many
# :through` association exists on your model.
#
Expand Down Expand Up @@ -1068,6 +1378,11 @@ def conditions(conditions)
self
end

def types(types)
@options[:types] = types
self
end

def autosave(autosave)
@options[:autosave] = autosave
self
Expand Down Expand Up @@ -1170,6 +1485,7 @@ def matches?(subject)
conditions_correct? &&
validate_correct? &&
touch_correct? &&
types_correct? &&
submatchers_match?
end

Expand Down Expand Up @@ -1414,6 +1730,30 @@ def touch_correct?
end
end

def types_correct?
if options.key?(:types)
types = options[:types]

correct = types.all? do |type|
scope_name = type.tableize.tr('/', '_')
singular = scope_name.singularize
query = "#{singular}?"

Object.const_defined?(type) && @subject.respond_to?(query) &&
@subject.respond_to?(singular)
end
Comment on lines +1773 to +1780
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not great, but currently, there's no way to check the types besides doing that. A PR was merged recently, adding a types method to the object, which will greatly simplify this check. I will update this method once it is released in Rails.

rails/rails#50662


if correct
true
else
@missing = "#{name} should have types: #{options[:types]}"
false
end
else
true
end
end

def class_has_foreign_key?(klass)
@missing = validate_foreign_key(klass)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ def initialize(reflection)
end

def associated_class
reflection.klass
if polymorphic?
subject
else
reflection.klass
end
Comment on lines +16 to +20
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll wait to see what happens when running all the specs, but when the relation is polymorphic, the reflection.klass method raises an error. I found this while trying to create a spec to replicate a case where you have a polymorphic association or delegated_type one, but you also pass the scopes qualifier to that association. When trying to make a spec for that cause, I noticed this problem.

My solution was to return a valid ActiveRecord object(which in that case, I returned the subject itself) because this will be used only to grab the value of the where_values_hash in the ModelReflector#extract_relation_clause_from, which I think doesn't matter which model you're using to. But we can think on another solution here.

end

def polymorphic?
Expand Down
Loading