diff --git a/lib/associations/associations.rb b/lib/associations/associations.rb index 60d9f3a..3ffd77d 100644 --- a/lib/associations/associations.rb +++ b/lib/associations/associations.rb @@ -8,14 +8,16 @@ def self.extended(base) def has_many(association_id, scope = nil, **options, &extension) if options[:through] - klass_name = association_id.to_s.classify - klass = klass_name.safe_constantize + source_association_name = options[:source]&.to_s || association_id.to_s.singularize + + through_klass = reflect_on_association(options[:through])&.klass + klass = through_klass&.reflect_on_association(source_association_name)&.klass if klass && klass < ActiveHash::Base define_method(association_id) do join_models = send(options[:through]) join_models.flat_map do |join_model| - join_model.send(association_id.to_s.singularize) + join_model.send(source_association_name) end.uniq end diff --git a/spec/associations/active_record_extensions_spec.rb b/spec/associations/active_record_extensions_spec.rb index be773cf..50b6fb9 100644 --- a/spec/associations/active_record_extensions_spec.rb +++ b/spec/associations/active_record_extensions_spec.rb @@ -234,6 +234,56 @@ def define_doctor_classes expect(patient.physicians).to contain_exactly(physician1, physician2) end + + describe "with the :source option" do + before do + # NOTE: Removing the Patient#physicians association and adding Patient#doctors + Patient._reflections.delete('physicians') + Patient.class_eval do + define_method(:physicians) { raise NoMethodError, "The #physicians association is removed in this spec, use #doctors" } + define_method(:physicians=) { |_| raise NoMethodError, "The #physicians association is removed in this spec, use #doctors" } + end + Patient.has_many :doctors, through: :appointments, source: :physician + end + + it "finds ActiveHash records through the join model" do + patient = Patient.create! + + physician = Physician.last + Appointment.create!(physician: physician, patient: patient) + + expect(patient.doctors).to contain_exactly(physician) + end + end + + describe ":through when the join model uses an aliased association" do + before do + # NOTE: Removing the Appointment#physician association and adding Appointment#doctor + Appointment._reflections.delete('physician') + Appointment.class_eval do + define_method(:physician) { raise NoMethodError, "The #physician association is removed in this spec, use #doctor" } + define_method(:physician=) { |_| raise NoMethodError, "The #physician association is removed in this spec, use #doctor" } + end + Appointment.belongs_to :doctor, class_name: 'Physician', foreign_key: :physician_id + + # NOTE: Removing the Patient#physicians association and adding Patient#doctors + Patient._reflections.delete('physicians') + Patient.class_eval do + define_method(:physicians) { raise NoMethodError, "The #physicians association is removed in this spec, use #doctors" } + define_method(:physicians=) { |_| raise NoMethodError, "The #physicians association is removed in this spec, use #doctors" } + end + Patient.has_many :doctors, through: :appointments + end + + it "finds ActiveHash records through the join model" do + patient = Patient.create! + + physician = Physician.last + Appointment.create!(doctor: physician, patient: patient) + + expect(patient.doctors).to contain_exactly(physician) + end + end end describe "with a lambda" do