diff --git a/lib/associations/associations.rb b/lib/associations/associations.rb index 3ffd77d..26f0384 100644 --- a/lib/associations/associations.rb +++ b/lib/associations/associations.rb @@ -7,36 +7,63 @@ def self.extended(base) end def has_many(association_id, scope = nil, **options, &extension) + super + if options[:through] 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 options[:source_type] + source_type = options[:source_type] + source_foreign_key = "#{source_association_name}_id" - 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(source_association_name) - end.uniq + klass = source_type.safe_constantize + if klass < ActiveHash::Base + ids = send(options[:through]).map { |jm| jm.send(source_foreign_key) }.compact.uniq + ids.flat_map { |id| klass.find_by_id(id) }.compact + else + super() + end + end + else + define_method(association_id) do + through_klass = self.class.reflect_on_association(options[:through])&.klass + source_klass = through_klass&.reflect_on_association(source_association_name)&.class_name&.safe_constantize + + if source_klass && source_klass < ActiveHash::Base + send(options[:through]).flat_map do |join_model| + join_model.send(source_association_name) + end.uniq + else + super() + end end - - return end end - - super end def belongs_to(name, scope = nil, **options) klass_name = options.key?(:class_name) ? options[:class_name] : name.to_s.camelize - klass = klass_name.safe_constantize + foreign_key = options[:foreign_key] || name.to_s.foreign_key - if klass && klass < ActiveHash::Base - options = { class_name: klass_name }.merge(options) - belongs_to_active_hash(name, options) - else - super + super + + define_method(name) do + klass = klass_name.safe_constantize + if klass && klass < ActiveHash::Base + klass.send("find_by_#{klass.primary_key}", send(foreign_key)) + else + super() + end + end + + define_method("#{name}=") do |new_value| + klass = klass_name.safe_constantize + if klass && klass < ActiveHash::Base + send("#{foreign_key}=", new_value ? new_value.send(klass.primary_key) : nil) + else + super(new_value) + end end end diff --git a/spec/associations/active_record_extensions_spec.rb b/spec/associations/active_record_extensions_spec.rb index 50b6fb9..d124357 100644 --- a/spec/associations/active_record_extensions_spec.rb +++ b/spec/associations/active_record_extensions_spec.rb @@ -124,6 +124,47 @@ def define_doctor_classes end + # Physician(AH) <-- Appointment(AR) --> Patient(AR) + # polymorphic: providerable on Appointment, source_type targets ActiveHash model + def define_polymorphic_doctor_classes + define_ephemeral_class(:Physician, ActiveHash::Base) do + include ActiveHash::Associations + + self.data = [ + {:id => 1, :name => "ikeda"}, + {:id => 2, :name => "sato"} + ] + end + + define_ephemeral_class(:Appointment, ActiveRecord::Base) do + establish_connection :adapter => "sqlite3", :database => ":memory:" + connection.create_table :appointments, force: true do |t| + t.references :providerable, polymorphic: true + t.references :patient + end + + extend ActiveHash::Associations::ActiveRecordExtensions + + # AR belongs_to (polymorphic) + belongs_to :providerable, polymorphic: true + # AR belongs_to + belongs_to :patient + end + + define_ephemeral_class(:Patient, ActiveRecord::Base) do + establish_connection :adapter => "sqlite3", :database => ":memory:" + connection.create_table :patients, force: true do |t| + end + + extend ActiveHash::Associations::ActiveRecordExtensions + + # AR has_many + has_many :appointments + # AR has_many :through (source_type points to ActiveHash model) + has_many :physicians, through: :appointments, source: :providerable, source_type: "Physician" + end + end + before do @ephemeral_classes = [] end @@ -286,6 +327,22 @@ def define_doctor_classes end end + describe ":through with a polymorphic source and source_type" do + before { define_polymorphic_doctor_classes } + + it "does not raise when defining the association" do + expect(Patient.instance_method(:physicians)).to be_a(UnboundMethod) + end + + it "returns the correct ActiveHash records" do + physician = Physician.find(1) + patient = Patient.create! + Appointment.create!(providerable_type: "Physician", providerable_id: physician.id, patient_id: patient.id) + + expect(patient.physicians).to contain_exactly(physician) + end + end + describe "with a lambda" do before do define_person_classes