From 5ab66858a96da8807c55e26c0afdd6ee35e7be47 Mon Sep 17 00:00:00 2001 From: Denis Talakevich Date: Thu, 7 Sep 2017 14:21:55 +0300 Subject: [PATCH] WIP #1105 fix bug with inclusion and linkage of has_one polymorphic add tests for link/unlink polymorphic relationships with include them in response add tests for include not linked relationships --- lib/jsonapi/relationship.rb | 3 + lib/jsonapi/resource.rb | 6 +- test/fixtures/active_record.rb | 49 ++++++++++++ test/integration/requests/request_test.rb | 93 +++++++++++++++++++++++ test/test_helper.rb | 1 + 5 files changed, 150 insertions(+), 2 deletions(-) diff --git a/lib/jsonapi/relationship.rb b/lib/jsonapi/relationship.rb index 4449742be..9d361b791 100644 --- a/lib/jsonapi/relationship.rb +++ b/lib/jsonapi/relationship.rb @@ -41,6 +41,9 @@ def self.polymorphic_types(name) klass.reflect_on_all_associations(:has_many).select{|r| r.options[:as] }.each do |reflection| (hash[reflection.options[:as]] ||= []) << klass.name.downcase end + klass.reflect_on_all_associations(:has_one).select{|r| r.options[:as] }.each do |reflection| + (hash[reflection.options[:as]] ||= []) << klass.name.downcase + end end end end diff --git a/lib/jsonapi/resource.rb b/lib/jsonapi/resource.rb index bd1c6e9f4..69685b7e3 100644 --- a/lib/jsonapi/resource.rb +++ b/lib/jsonapi/resource.rb @@ -1039,8 +1039,10 @@ def define_relationship_methods(relationship_name, relationship_klass, options) def define_foreign_key_setter(relationship) if relationship.polymorphic? define_on_resource "#{relationship.foreign_key}=" do |v| - _model.method("#{relationship.foreign_key}=").call(v[:id]) - _model.public_send("#{relationship.polymorphic_type}=", v[:type]) + model_id = v.nil? ? nil : v[:id] + model_type = v.nil? ? nil : self.class.resource_klass_for(v[:type].to_s)._model_class.to_s + _model.method("#{relationship.foreign_key}=").call(model_id) + _model.public_send("#{relationship.polymorphic_type}=", model_type) end else define_on_resource "#{relationship.foreign_key}=" do |value| diff --git a/test/fixtures/active_record.rb b/test/fixtures/active_record.rb index b5e3dc40b..907235d48 100644 --- a/test/fixtures/active_record.rb +++ b/test/fixtures/active_record.rb @@ -356,6 +356,25 @@ t.integer :version t.timestamps null: false end + + create_table :options, force: true do |t| + t.integer :optionable_id + t.string :optionable_type + t.integer :maintainer_id + t.boolean :enabled, default: false, null: false + t.timestamps null: false + end + + create_table :androids, force: true do |t| + t.string :version_name + t.timestamps null: false + end + + create_table :maintainers, force: true do |t| + t.string :name + t.timestamps null: false + end + end ### MODELS @@ -734,6 +753,19 @@ class Widget < ActiveRecord::Base class Robot < ActiveRecord::Base end +class Option < ActiveRecord::Base + belongs_to :optionable, polymorphic: true, required: false + belongs_to :maintainer, required: false +end + +class Android < ActiveRecord::Base + has_one :option, as: :optionable +end + +class Maintainer < ActiveRecord::Base + has_one :maintained_option +end + ### CONTROLLERS class AuthorsController < JSONAPI::ResourceControllerMetal end @@ -1033,6 +1065,9 @@ class IndicatorsController < JSONAPI::ResourceController class RobotsController < JSONAPI::ResourceController end +class OptionsController < JSONAPI::ResourceController +end + ### RESOURCES class BaseResource < JSONAPI::Resource abstract @@ -2207,6 +2242,20 @@ class RobotResource < ::JSONAPI::Resource end end +class OptionResource < JSONAPI::Resource + attribute :enabled + has_one :optionable, polymorphic: true, class_name: 'Android' + has_one :maintainer, class_name: 'Maintainer' +end + +class AndroidResource < JSONAPI::Resource + attribute :version_name +end + +class MaintainerResource < JSONAPI::Resource + attribute :name +end + ### PORO Data - don't do this in a production app $breed_data = BreedData.new $breed_data.add(Breed.new(0, 'persian')) diff --git a/test/integration/requests/request_test.rb b/test/integration/requests/request_test.rb index 4b0e4cc10..b1c4891ea 100644 --- a/test/integration/requests/request_test.rb +++ b/test/integration/requests/request_test.rb @@ -1162,4 +1162,97 @@ def test_get_resource_with_belongs_to_relationship_and_changed_primary_key assert_equal 'access_cards', included.first['type'] assert_equal access_card.token, included.first['id'] end + + def test_update_option_link_with_optionable_include_optionable + android = Android.create! version_name: '1.0' + option = Option.create! + json_request = { + data: { + id: option.id.to_s, + type: 'options', + relationships: { + optionable: { + data: { + id: android.id.to_s, + type: 'androids' + } + } + } + } + } + json_api_headers = { + 'CONTENT_TYPE' => JSONAPI::MEDIA_TYPE, + 'Accept' => JSONAPI::MEDIA_TYPE + } + patch "/options/#{option.id}?include=optionable", params: json_request.to_json, headers: json_api_headers + assert_jsonapi_response 200 + relationship_data = {'id' => android.id.to_s, 'type' => 'androids'} + assert_equal relationship_data, json_response['data']['relationships']['optionable']['data'] + assert_equal relationship_data, json_response['included'].first.slice('id', 'type') + assert_equal android, option.reload.optionable + end + + def test_update_option_unlink_from_optionable_include_optionable + android = Android.create! version_name: '1.0' + option = Option.create! optionable: android + json_request = { + data: { + id: option.id.to_s, + type: 'options', + relationships: { + optionable: { + data: nil + } + } + } + } + json_api_headers = { + 'CONTENT_TYPE' => JSONAPI::MEDIA_TYPE, + 'Accept' => JSONAPI::MEDIA_TYPE + } + patch "/options/#{option.id}?include=optionable", params: json_request.to_json, headers: json_api_headers + assert_jsonapi_response 200 + assert_equal true, json_response['data']['relationships']['optionable'].has_key?('data') + assert_nil json_response['data']['relationships']['optionable']['data'] + assert_equal false, json_response.has_key?('included') + assert_nil option.reload.optionable + end + + def test_fetch_option_linked_with_optionable_include_optionable + android = Android.create! version_name: '1.0' + option = Option.create! optionable: android + assert_cacheable_jsonapi_get "/options/#{option.id}?include=optionable" + assert_jsonapi_response 200 + relationship_data = {'id' => android.id.to_s, 'type' => 'androids'} + assert_equal relationship_data, json_response['data']['relationships']['optionable']['data'] + assert_equal relationship_data, json_response['included'].first.slice('id', 'type') + end + + def test_fetch_option_not_linked_with_optionable_include_optionable + option = Option.create! + assert_cacheable_jsonapi_get "/options/#{option.id}?include=optionable" + assert_jsonapi_response 200 + assert_equal true, json_response['data']['relationships']['optionable'].has_key?('data') + assert_nil json_response['data']['relationships']['optionable']['data'] + assert_equal false, json_response.has_key?('included') + end + + def test_fetch_option_not_linked_with_maintainer_include_maintainer + option = Option.create! + assert_cacheable_jsonapi_get "/options/#{option.id}?include=maintainer" + assert_jsonapi_response 200 + assert_equal true, json_response['data']['relationships']['maintainer'].has_key?('data') + assert_nil json_response['data']['relationships']['maintainer']['data'] + assert_equal false, json_response.has_key?('included') + end + + def test_fetch_option_linked_with_maintainer_include_maintainer + maintainer = Maintainer.create! name: 'John Doe' + option = Option.create! maintainer: maintainer + assert_cacheable_jsonapi_get "/options/#{option.id}?include=maintainer" + assert_jsonapi_response 200 + relationship_data = {'id' => maintainer.id.to_s, 'type' => 'maintainers'} + assert_equal relationship_data, json_response['data']['relationships']['maintainer']['data'] + assert_equal relationship_data, json_response['included'].first.slice('id', 'type') + end end diff --git a/test/test_helper.rb b/test/test_helper.rb index 249638f94..6d473b7a9 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -401,6 +401,7 @@ class CatResource < JSONAPI::Resource jsonapi_resources :widgets, only: [:index] jsonapi_resources :indicators, only: [:index] jsonapi_resources :robots, only: [:index] + jsonapi_resources :options, only: [:show, :update] mount MyEngine::Engine => "/boomshaka", as: :my_engine mount ApiV2Engine::Engine => "/api_v2", as: :api_v2_engine