Skip to content

Commit 6fb56db

Browse files
authored
feat: Add have_delegated_type matcher (#1606)
1 parent d65c54b commit 6fb56db

File tree

3 files changed

+988
-1
lines changed

3 files changed

+988
-1
lines changed

lib/shoulda/matchers/active_record/association_matcher.rb

Lines changed: 340 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,316 @@ def belong_to(name)
356356
AssociationMatcher.new(:belongs_to, name)
357357
end
358358

359+
# The `have_delegated_type` matcher is used to ensure that a `belong_to` association
360+
# exists on your model using the delegated_type macro.
361+
#
362+
# class Vehicle < ActiveRecord::Base
363+
# delegated_type :drivable, types: %w(Car Truck)
364+
# end
365+
#
366+
# # RSpec
367+
# RSpec.describe Vehicle, type: :model do
368+
# it { should have_delegated_type(:drivable) }
369+
# end
370+
#
371+
# # Minitest (Shoulda)
372+
# class VehicleTest < ActiveSupport::TestCase
373+
# should have_delegated_type(:drivable)
374+
# end
375+
#
376+
# #### Qualifiers
377+
#
378+
# ##### types
379+
#
380+
# Use `types` to test the types that are allowed for the association.
381+
#
382+
# class Vehicle < ActiveRecord::Base
383+
# delegated_type :drivable, types: %w(Car Truck)
384+
# end
385+
#
386+
# # RSpec
387+
# RSpec.describe Vehicle, type: :model do
388+
# it do
389+
# should have_delegated_type(:drivable).
390+
# types(%w(Car Truck))
391+
# end
392+
# end
393+
#
394+
# # Minitest (Shoulda)
395+
# class VehicleTest < ActiveSupport::TestCase
396+
# should have_delegated_type(:drivable).
397+
# types(%w(Car Truck))
398+
# end
399+
#
400+
# ##### conditions
401+
#
402+
# Use `conditions` if your association is defined with a scope that sets
403+
# the `where` clause.
404+
#
405+
# class Vehicle < ActiveRecord::Base
406+
# delegated_type :drivable, types: %w(Car Truck), scope: -> { where(with_wheels: true) }
407+
# end
408+
#
409+
# # RSpec
410+
# RSpec.describe Vehicle, type: :model do
411+
# it do
412+
# should have_delegated_type(:drivable).
413+
# conditions(with_wheels: true)
414+
# end
415+
# end
416+
#
417+
# # Minitest (Shoulda)
418+
# class VehicleTest < ActiveSupport::TestCase
419+
# should have_delegated_type(:drivable).
420+
# conditions(everyone_is_perfect: false)
421+
# end
422+
#
423+
# ##### order
424+
#
425+
# Use `order` if your association is defined with a scope that sets the
426+
# `order` clause.
427+
#
428+
# class Person < ActiveRecord::Base
429+
# delegated_type :drivable, types: %w(Car Truck), scope: -> { order('wheels desc') }
430+
# end
431+
#
432+
# # RSpec
433+
# RSpec.describe Vehicle, type: :model do
434+
# it { should have_delegated_type(:drivable).order('wheels desc') }
435+
# end
436+
#
437+
# # Minitest (Shoulda)
438+
# class VehicleTest < ActiveSupport::TestCase
439+
# should have_delegated_type(:drivable).order('wheels desc')
440+
# end
441+
#
442+
# ##### with_primary_key
443+
#
444+
# Use `with_primary_key` to test usage of the `:primary_key` option.
445+
#
446+
# class Vehicle < ActiveRecord::Base
447+
# delegated_type :drivable, types: %w(Car Truck), primary_key: 'vehicle_id'
448+
# end
449+
#
450+
# # RSpec
451+
# RSpec.describe Vehicle, type: :model do
452+
# it do
453+
# should have_delegated_type(:drivable).
454+
# with_primary_key('vehicle_id')
455+
# end
456+
# end
457+
#
458+
# # Minitest (Shoulda)
459+
# class VehicleTest < ActiveSupport::TestCase
460+
# should have_delegated_type(:drivable).
461+
# with_primary_key('vehicle_id')
462+
# end
463+
#
464+
# ##### with_foreign_key
465+
#
466+
# Use `with_foreign_key` to test usage of the `:foreign_key` option.
467+
#
468+
# class Vehicle < ActiveRecord::Base
469+
# delegated_type :drivable, types: %w(Car Truck), foreign_key: 'drivable_uuid'
470+
# end
471+
#
472+
# # RSpec
473+
# RSpec.describe Vehicle, type: :model do
474+
# it do
475+
# should have_delegated_type(:drivable).
476+
# with_foreign_key('drivable_uuid')
477+
# end
478+
# end
479+
#
480+
# # Minitest (Shoulda)
481+
# class VehicleTest < ActiveSupport::TestCase
482+
# should have_delegated_type(:drivable).
483+
# with_foreign_key('drivable_uuid')
484+
# end
485+
#
486+
# ##### dependent
487+
#
488+
# Use `dependent` to assert that the `:dependent` option was specified.
489+
#
490+
# class Vehicle < ActiveRecord::Base
491+
# delegated_type :drivable, types: %w(Car Truck), dependent: :destroy
492+
# end
493+
#
494+
# # RSpec
495+
# RSpec.describe Vehicle, type: :model do
496+
# it { should have_delegated_type(:drivable).dependent(:destroy) }
497+
# end
498+
#
499+
# # Minitest (Shoulda)
500+
# class VehicleTest < ActiveSupport::TestCase
501+
# should have_delegated_type(:drivable).dependent(:destroy)
502+
# end
503+
#
504+
# To assert that *any* `:dependent` option was specified, use `true`:
505+
#
506+
# # RSpec
507+
# RSpec.describe Vehicle, type: :model do
508+
# it { should have_delegated_type(:drivable).dependent(true) }
509+
# end
510+
#
511+
# To assert that *no* `:dependent` option was specified, use `false`:
512+
#
513+
# class Vehicle < ActiveRecord::Base
514+
# delegated_type :drivable, types: %w(Car Truck)
515+
# end
516+
#
517+
# # RSpec
518+
# RSpec.describe Vehicle, type: :model do
519+
# it { should have_delegated_type(:drivable).dependent(false) }
520+
# end
521+
#
522+
# ##### counter_cache
523+
#
524+
# Use `counter_cache` to assert that the `:counter_cache` option was
525+
# specified.
526+
#
527+
# class Vehicle < ActiveRecord::Base
528+
# delegated_type :drivable, types: %w(Car Truck), counter_cache: true
529+
# end
530+
#
531+
# # RSpec
532+
# RSpec.describe Vehicle, type: :model do
533+
# it { should have_delegated_type(:drivable).counter_cache(true) }
534+
# end
535+
#
536+
# # Minitest (Shoulda)
537+
# class VehicleTest < ActiveSupport::TestCase
538+
# should have_delegated_type(:drivable).counter_cache(true)
539+
# end
540+
#
541+
# ##### touch
542+
#
543+
# Use `touch` to assert that the `:touch` option was specified.
544+
#
545+
# class Vehicle < ActiveRecord::Base
546+
# delegated_type :drivable, types: %w(Car Truck), touch: true
547+
# end
548+
#
549+
# # RSpec
550+
# RSpec.describe Vehicle, type: :model do
551+
# it { should have_delegated_type(:drivable).touch(true) }
552+
# end
553+
#
554+
# # Minitest (Shoulda)
555+
# class VehicleTest < ActiveSupport::TestCase
556+
# should have_delegated_type(:drivable).touch(true)
557+
# end
558+
#
559+
# ##### autosave
560+
#
561+
# Use `autosave` to assert that the `:autosave` option was specified.
562+
#
563+
# class Vehicle < ActiveRecord::Base
564+
# delegated_type :drivable, types: %w(Car Truck), autosave: true
565+
# end
566+
#
567+
# # RSpec
568+
# RSpec.describe Vehicle, type: :model do
569+
# it { should have_delegated_type(:drivable).autosave(true) }
570+
# end
571+
#
572+
# # Minitest (Shoulda)
573+
# class VehicleTest < ActiveSupport::TestCase
574+
# should have_delegated_type(:drivable).autosave(true)
575+
# end
576+
#
577+
# ##### inverse_of
578+
#
579+
# Use `inverse_of` to assert that the `:inverse_of` option was specified.
580+
#
581+
# class Vehicle < ActiveRecord::Base
582+
# delegated_type :drivable, types: %w(Car Truck), inverse_of: :vehicle
583+
# end
584+
#
585+
# # RSpec
586+
# describe Vehicle
587+
# it { should have_delegated_type(:drivable).inverse_of(:vehicle) }
588+
# end
589+
#
590+
# # Minitest (Shoulda)
591+
# class VehicleTest < ActiveSupport::TestCase
592+
# should have_delegated_type(:drivable).inverse_of(:vehicle)
593+
# end
594+
#
595+
# ##### required
596+
#
597+
# Use `required` to assert that the association is not allowed to be nil.
598+
# (Enabled by default in Rails 5+.)
599+
#
600+
# class Vehicle < ActiveRecord::Base
601+
# delegated_type :drivable, types: %w(Car Truck), required: true
602+
# end
603+
#
604+
# # RSpec
605+
# describe Vehicle
606+
# it { should have_delegated_type(:drivable).required }
607+
# end
608+
#
609+
# # Minitest (Shoulda)
610+
# class VehicleTest < ActiveSupport::TestCase
611+
# should have_delegated_type(:drivable).required
612+
# end
613+
#
614+
# ##### without_validating_presence
615+
#
616+
# Use `without_validating_presence` with `belong_to` to prevent the
617+
# matcher from checking whether the association disallows nil (Rails 5+
618+
# only). This can be helpful if you have a custom hook that always sets
619+
# the association to a meaningful value:
620+
#
621+
# class Vehicle < ActiveRecord::Base
622+
# delegated_type :drivable, types: %w(Car Truck)
623+
#
624+
# before_validation :autoassign_drivable
625+
#
626+
# private
627+
#
628+
# def autoassign_drivable
629+
# self.drivable = Car.create!
630+
# end
631+
# end
632+
#
633+
# # RSpec
634+
# describe Vehicle
635+
# it { should have_delegated_type(:drivable).without_validating_presence }
636+
# end
637+
#
638+
# # Minitest (Shoulda)
639+
# class VehicleTest < ActiveSupport::TestCase
640+
# should have_delegated_type(:drivable).without_validating_presence
641+
# end
642+
#
643+
# ##### optional
644+
#
645+
# Use `optional` to assert that the association is allowed to be nil.
646+
# (Rails 5+ only.)
647+
#
648+
# class Vehicle < ActiveRecord::Base
649+
# delegated_type :drivable, types: %w(Car Truck), optional: true
650+
# end
651+
#
652+
# # RSpec
653+
# describe Vehicle
654+
# it { should have_delegated_type(:drivable).optional }
655+
# end
656+
#
657+
# # Minitest (Shoulda)
658+
# class VehicleTest < ActiveSupport::TestCase
659+
# should have_delegated_type(:drivable).optional
660+
# end
661+
#
662+
# @return [AssociationMatcher]
663+
#
664+
665+
def have_delegated_type(name)
666+
AssociationMatcher.new(:belongs_to, name)
667+
end
668+
359669
# The `have_many` matcher is used to test that a `has_many` or `has_many
360670
# :through` association exists on your model.
361671
#
@@ -1098,6 +1408,11 @@ def conditions(conditions)
10981408
self
10991409
end
11001410

1411+
def types(types)
1412+
@options[:types] = types
1413+
self
1414+
end
1415+
11011416
def autosave(autosave)
11021417
@options[:autosave] = autosave
11031418
self
@@ -1205,6 +1520,7 @@ def matches?(subject)
12051520
conditions_correct? &&
12061521
validate_correct? &&
12071522
touch_correct? &&
1523+
types_correct? &&
12081524
strict_loading_correct? &&
12091525
submatchers_match?
12101526
end
@@ -1450,6 +1766,30 @@ def touch_correct?
14501766
end
14511767
end
14521768

1769+
def types_correct?
1770+
if options.key?(:types)
1771+
types = options[:types]
1772+
1773+
correct = types.all? do |type|
1774+
scope_name = type.tableize.tr('/', '_')
1775+
singular = scope_name.singularize
1776+
query = "#{singular}?"
1777+
1778+
Object.const_defined?(type) && @subject.respond_to?(query) &&
1779+
@subject.respond_to?(singular)
1780+
end
1781+
1782+
if correct
1783+
true
1784+
else
1785+
@missing = "#{name} should have types: #{options[:types]}"
1786+
false
1787+
end
1788+
else
1789+
true
1790+
end
1791+
end
1792+
14531793
def strict_loading_correct?
14541794
return true unless options.key?(:strict_loading)
14551795

lib/shoulda/matchers/active_record/association_matchers/model_reflection.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,11 @@ def initialize(reflection)
1313
end
1414

1515
def associated_class
16-
reflection.klass
16+
if polymorphic?
17+
subject
18+
else
19+
reflection.klass
20+
end
1721
end
1822

1923
def polymorphic?

0 commit comments

Comments
 (0)