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

fix: more flexible polymorphic types lookup #1424

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
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
31 changes: 27 additions & 4 deletions lib/jsonapi/relationship.rb
Original file line number Diff line number Diff line change
Expand Up @@ -90,14 +90,37 @@ def self.polymorphic_types(name)
@poly_hash ||= {}.tap do |hash|
ObjectSpace.each_object do |klass|
next unless Module === klass
if ActiveRecord::Base > klass
is_active_record_inspectable = ActiveRecord::Base > klass
is_active_record_inspectable &&= klass.respond_to?(:reflect_on_all_associations, true)
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@lgebhardt I'll add a test for this. I'm testing against our app on this branch right now and looks good

is_active_record_inspectable &&= format_polymorphic_klass_type(klass).present?
if is_active_record_inspectable
klass.reflect_on_all_associations(:has_many).select { |r| r.options[:as] }.each do |reflection|
(hash[reflection.options[:as]] ||= []) << klass.name.underscore
(hash[reflection.options[:as]] ||= []) << format_polymorphic_klass_type(klass).underscore
end
end
end
end
@poly_hash[name.to_sym]
@poly_hash.fetch(name.to_sym) do
klass = name.classify.safe_constantize
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

fallback strategy I'm trying

if klass.nil?
warn "[POLYMORPHIC TYPE NOT FOUND] No polymorphic types found for #{name}"
else
polymorphic_type = format_polymorphic_klass_type(klass)
warn "[POLYMORPHIC TYPE] Found polymorphic types through reflection for #{name}: #{polymorphic_type}"
@poly_hash[name.to_sym] = [polymorphic_type]
end
end
end

def self.format_polymorphic_klass_type(klass)
klass.name ||
begin
klass.model_name.name
rescue ArgumentError => ex
# klass.base_class may be nil
warn "[POLYMORPHIC TYPE] #{__callee__} #{klass} #{ex.inspect}"
nil
end
end

def resource_types
Expand Down Expand Up @@ -203,7 +226,7 @@ def polymorphic_type
def setup_implicit_relationships_for_polymorphic_types(exclude_linkage_data: true)
types = self.class.polymorphic_types(_relation_name)
unless types.present?
warn "No polymorphic types found for #{parent_resource.name} #{_relation_name}"
warn "[POLYMORPHIC TYPE] No polymorphic types found for #{parent_resource.name} #{_relation_name}"
return
end

Expand Down
12 changes: 1 addition & 11 deletions lib/jsonapi/resource_common.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1020,17 +1020,7 @@ def polymorphic(polymorphic = true)
end

def _polymorphic_types
@poly_hash ||= {}.tap do |hash|
ObjectSpace.each_object do |klass|
next unless Module === klass
if klass < ActiveRecord::Base
klass.reflect_on_all_associations(:has_many).select{|r| r.options[:as] }.each do |reflection|
(hash[reflection.options[:as]] ||= []) << klass.name.underscore
end
end
end
end
@poly_hash[_polymorphic_name.to_sym]
JSONAPI::Relationship.polymorphic_types(_polymorphic_name.to_sym)
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@lgebhardt is a refactor included in the PR as a separate commit

end

def _polymorphic_resource_klasses
Expand Down
60 changes: 60 additions & 0 deletions test/fixtures/active_record.rb
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,18 @@
t.integer :version
t.timestamps null: false
end
create_table :contact_media do |t|
t.string :name
t.references :party, polymorphic: true, index: true
end

create_table :individuals do |t|
t.string :name
end

create_table :organizations do |t|
t.string :name
end
end

### MODELS
Expand Down Expand Up @@ -624,6 +636,24 @@ class Fact < ActiveRecord::Base
class Like < ActiveRecord::Base
end

class TestApplicationRecord < ActiveRecord::Base
self.abstract_class = true
end

class ContactMedium < TestApplicationRecord
belongs_to :party, polymorphic: true, inverse_of: :contact_media
belongs_to :individual, -> { where( contact_media: { party_type: 'Individual' } ).eager_load( :contact_media ) }, foreign_key: 'party_id'
belongs_to :organization, -> { where( contact_media: { party_type: 'Organization' } ).eager_load( :contact_media ) }, foreign_key: 'party_id'
end

class Individual < TestApplicationRecord
has_many :contact_media, as: :party
end

class Organization < TestApplicationRecord
has_many :contact_media, as: :party
end

class Breed
include ActiveModel::Model

Expand Down Expand Up @@ -1227,6 +1257,18 @@ class IndicatorsController < JSONAPI::ResourceController
class RobotsController < JSONAPI::ResourceController
end

class IndividualsController < BaseController
end

class OrganizationsController < BaseController
end

class ContactMediaController < BaseController
end

class PartiesController < BaseController
end

### RESOURCES
class BaseResource < JSONAPI::Resource
abstract
Expand Down Expand Up @@ -2685,6 +2727,24 @@ class RobotResource < ::JSONAPI::Resource
end
end

class ContactMediumResource < JSONAPI::Resource
attribute :name
has_one :party, polymorphic: true
end

class IndividualResource < JSONAPI::Resource
attribute :name
has_many :contact_media
end

class OrganizationResource < JSONAPI::Resource
attribute :name
has_many :contact_media
end

class PartyResource < JSONAPI::Resource
end

### PORO Data - don't do this in a production app
$breed_data = BreedData.new
$breed_data.add(Breed.new(0, 'persian'))
Expand Down
83 changes: 83 additions & 0 deletions test/integration/requests/polymorphic_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
require File.expand_path('../../../test_helper', __FILE__)

# copied from https://github.com/cerebris/jsonapi-resources/blob/e60dc7dd2c7fdc85834163a7e706a10a8940a62b/test/integration/requests/polymorphic_test.rb
# https://github.com/cerebris/jsonapi-resources/compare/bf4_fix_polymorphic_relations_lookup?expand=1
class PolymorphicTest < ActionDispatch::IntegrationTest

def json_api_headers
{'Accept' => JSONAPI::MEDIA_TYPE, 'CONTENT_TYPE' => JSONAPI::MEDIA_TYPE}
end

def test_find_party_via_contact_medium
individual = Individual.create(name: 'test')
contact_medium = ContactMedium.create(party: individual, name: 'test contact medium')
fetched_party = contact_medium.party
assert_same individual, fetched_party, "Expect an individual to have been found via contact medium model's relationship 'party'"
end

def test_get_individual
individual = Individual.create(name: 'test')
ContactMedium.create(party: individual, name: 'test contact medium')
get "/individuals/#{individual.id}"
assert_jsonapi_response 200
end

def test_get_party_via_contact_medium
individual = Individual.create(name: 'test')
contact_medium = ContactMedium.create(party: individual, name: 'test contact medium')
get "/contact_media/#{contact_medium.id}/party"
assert_jsonapi_response 200, "Expect an individual to have been found via contact medium resource's relationship 'party'"
end
end

# copied from https://github.com/cerebris/jsonapi-resources/pull/1349/files
# require File.expand_path('../test_helper', __FILE__)
#
# Replace this with the code necessary to make your test fail.
# class BugTest < Minitest::Test
# include Rack::Test::Methods
#
# def json_api_headers
# {'Accept' => JSONAPI::MEDIA_TYPE, 'CONTENT_TYPE' => JSONAPI::MEDIA_TYPE}
# end
#
# def teardown
# Individual.delete_all
# ContactMedium.delete_all
# end
#
# def test_find_party_via_contact_medium
# individual = Individual.create(name: 'test')
# contact_medium = ContactMedium.create(party: individual, name: 'test contact medium')
# fetched_party = contact_medium.party
# assert_same individual, fetched_party, "Expect an individual to have been found via contact medium model's relationship 'party'"
# end
#
# def test_get_individual
# individual = Individual.create(name: 'test')
# ContactMedium.create(party: individual, name: 'test contact medium')
# get "/individuals/#{individual.id}"
# assert_last_response_status 200
# end
#
# def test_get_party_via_contact_medium
# individual = Individual.create(name: 'test')
# contact_medium = ContactMedium.create(party: individual, name: 'test contact medium')
# get "/contact_media/#{contact_medium.id}/party"
# assert_last_response_status 200, "Expect an individual to have been found via contact medium resource's relationship 'party'"
# end
#
# private
#
# def assert_last_response_status(status, failure_reason=nil)
# if last_response.status != status
# json_response = JSON.parse(last_response.body) rescue last_response.body
# pp json_response
# end
# assert_equal status, last_response.status, failure_reason
# end
#
# def app
# Rails.application
# end
# end
3 changes: 3 additions & 0 deletions test/test_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,9 @@ class CatResource < JSONAPI::Resource
jsonapi_resources :widgets, only: [:index]
jsonapi_resources :indicators, only: [:index]
jsonapi_resources :robots, only: [:index]
jsonapi_resources :contact_media
jsonapi_resources :individuals
jsonapi_resources :organizations

mount MyEngine::Engine => "/boomshaka", as: :my_engine
mount ApiV2Engine::Engine => "/api_v2", as: :api_v2_engine
Expand Down