Skip to content

Commit

Permalink
Merge pull request #296 from active-hash/flavorjones-239-has-many-thr…
Browse files Browse the repository at this point in the history
…ough

support `has_many :through` associations
  • Loading branch information
flavorjones authored Dec 6, 2023
2 parents 48f1181 + 1ffe44a commit 523bfae
Show file tree
Hide file tree
Showing 2 changed files with 122 additions and 32 deletions.
32 changes: 26 additions & 6 deletions lib/associations/associations.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,35 @@ module ActiveHash
module Associations

module ActiveRecordExtensions

def self.extended(base)
require_relative 'reflection_extensions'
end

def has_many(association_id, **options)
if options[:through]
klass_name = association_id.to_s.classify
klass =
begin
klass_name.constantize
rescue StandardError, LoadError
nil
end

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)
end.uniq
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 =
Expand Down Expand Up @@ -55,9 +79,6 @@ def belongs_to_active_hash(association_id, options = {})
if ActiveRecord::Reflection.respond_to?(:create)
if defined?(ActiveHash::Reflection::BelongsToReflection)
reflection = ActiveHash::Reflection::BelongsToReflection.new(association_id.to_sym, nil, options, self)
if options[:through]
reflection = ActiveRecord::ThroughReflection.new(reflection)
end
else
reflection = ActiveRecord::Reflection.create(
:belongs_to,
Expand Down Expand Up @@ -120,6 +141,7 @@ def has_many(association_id, options = {})
klass.where(foreign_key => primary_key_value)
end
end

define_method("#{association_id.to_s.underscore.singularize}_ids") do
public_send(association_id).map(&:id)
end
Expand All @@ -143,7 +165,6 @@ def has_one(association_id, options = {})
end

def belongs_to(association_id, options = {})

options = {
:class_name => association_id.to_s.classify,
:foreign_key => association_id.to_s.foreign_key,
Expand All @@ -159,7 +180,6 @@ def belongs_to(association_id, options = {})
define_method("#{association_id}=") do |new_value|
attributes[options[:foreign_key].to_sym] = new_value ? new_value.send(options[:primary_key]) : nil
end

end
end

Expand Down
122 changes: 96 additions & 26 deletions spec/associations/active_record_extensions_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,65 +3,115 @@
unless SKIP_ACTIVE_RECORD
describe ActiveHash::Base, "active record extensions" do

before do
class Country < ActiveRecord::Base
def define_ephemeral_class(name, superclass, &block)
klass = Class.new(superclass)
Object.const_set(name, klass)
klass.class_eval(&block) if block_given?
@ephemeral_classes << name
end

def define_book_classes
define_ephemeral_class(:Author, ActiveHash::Base) do
include ActiveHash::Associations
end

define_ephemeral_class(:Book, ActiveRecord::Base) do
establish_connection :adapter => "sqlite3", :database => ":memory:"
connection.create_table(:books, :force => true) do |t|
t.integer :author_id
t.integer :author_code
t.boolean :published
end

if Object.const_defined?(:ActiveModel)
scope(:published, proc { where(:published => true) })
else
named_scope :published, {:conditions => {:published => true}}
end
end
end

def define_school_classes
define_ephemeral_class(:Country, ActiveRecord::Base) do
establish_connection :adapter => "sqlite3", :database => ":memory:"
connection.create_table(:countries, :force => true) do |t|
t.string :name
end
extend ActiveHash::Associations::ActiveRecordExtensions
end

class School < ActiveRecord::Base
define_ephemeral_class(:School, ActiveRecord::Base) do
establish_connection :adapter => "sqlite3", :database => ":memory:"
connection.create_table(:schools, :force => true) do |t|
t.integer :country_id
t.string :locateable_type
t.integer :locateable_id
t.integer :city_id
end

extend ActiveHash::Associations::ActiveRecordExtensions
end

class City < ActiveHash::Base
define_ephemeral_class(:City, ActiveHash::Base) do
include ActiveHash::Associations
end

class Author < ActiveHash::Base
define_ephemeral_class(:SchoolStatus, ActiveHash::Base)
end

def define_doctor_classes
define_ephemeral_class(:Physician, ActiveHash::Base) do
include ActiveHash::Associations
end

class SchoolStatus < ActiveHash::Base
has_many :appointments
has_many :patients, through: :appointments

self.data = [
{:id => 1, :name => "ikeda"},
{:id => 2, :name => "sato"}
]
end

class Book < ActiveRecord::Base
define_ephemeral_class(:Appointment, ActiveRecord::Base) do
establish_connection :adapter => "sqlite3", :database => ":memory:"
connection.create_table(:books, :force => true) do |t|
t.integer :author_id
t.integer :author_code
t.boolean :published
connection.create_table :appointments, force: true do |t|
t.references :physician
t.references :patient
end

if Object.const_defined?(:ActiveModel)
scope(:published, proc { where(:published => true) })
else
named_scope :published, {:conditions => {:published => true}}
extend ActiveHash::Associations::ActiveRecordExtensions

belongs_to :physician
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

has_many :appointments
has_many :physicians, through: :appointments
end

end

before do
@ephemeral_classes = []
end

after do
Object.send :remove_const, :City
Object.send :remove_const, :Author
Object.send :remove_const, :Country
Object.send :remove_const, :School
Object.send :remove_const, :SchoolStatus
Object.send :remove_const, :Book
@ephemeral_classes.each do |klass_name|
Object.send :remove_const, klass_name
end
end

describe "#has_many" do

context "with ActiveRecord children" do
before { define_book_classes }

context "with default options" do
before do
@book_1 = Book.create! :author_id => 1, :published => true
Expand Down Expand Up @@ -143,10 +193,28 @@ class Book < ActiveRecord::Base
end
end

describe ":through" do
before { define_doctor_classes }

it "finds ActiveHash records through the join model" do
patient = Patient.create!

physician1 = Physician.first
Appointment.create!(physician: physician1, patient: patient)
Appointment.create!(physician: physician1, patient: patient)

physician2 = Physician.last
Appointment.create!(physician: physician2, patient: patient)

expect(patient.physicians).to contain_exactly(physician1, physician2)
end
end
end

describe ActiveHash::Associations::ActiveRecordExtensions do
describe "#belongs_to" do
before { define_school_classes }

it "doesn't interfere with AR's procs in belongs_to methods" do
School.belongs_to :country, lambda { where(name: 'Japan') }
school = School.new
Expand Down Expand Up @@ -210,6 +278,8 @@ class Book < ActiveRecord::Base
end

describe "#belongs_to_active_hash" do
before { define_school_classes }

context "setting by id" do
it "finds the correct records" do
School.belongs_to_active_hash :city
Expand Down Expand Up @@ -282,8 +352,9 @@ class Book < ActiveRecord::Base
end

describe "#belongs_to" do

context "with an ActiveRecord parent" do
before { define_school_classes }

it "find the correct records" do
City.belongs_to :country
country = Country.create
Expand All @@ -297,12 +368,12 @@ class Book < ActiveRecord::Base
expect(city.country).to be_nil
end
end

end

describe "#has_one" do
context "with ActiveRecord children" do
before do
define_book_classes
Author.has_one :book
end

Expand All @@ -318,6 +389,5 @@ class Book < ActiveRecord::Base
end
end
end

end
end

0 comments on commit 523bfae

Please sign in to comment.