Skip to content

Commit f6e3183

Browse files
st0012lgebhardt
authored andcommitted
Refactor polymorphic has many link creation
closes cerebrisgh-1231 closes cerebrisgh-1217
1 parent 34852fe commit f6e3183

File tree

5 files changed

+216
-10
lines changed

5 files changed

+216
-10
lines changed

lib/jsonapi/request_parser.rb

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -524,13 +524,37 @@ def parse_to_many_relationship(link_value, relationship, &add_result)
524524
if links_object.length == 0
525525
add_result.call([])
526526
else
527-
defined_relationship_resource = Resource.resource_for(@resource_klass.module_path + unformat_key(relationship.type).to_s)
528-
links_object.each_pair do |type, keys|
529-
relationship_resource = Resource.resource_for(@resource_klass.module_path + unformat_key(type).to_s)
530-
if !relationship_resource.ancestors.include?(defined_relationship_resource)
527+
if relationship.polymorphic?
528+
polymorphic_results = []
529+
530+
links_object.each_pair do |type, keys|
531+
resource = self.resource_klass || Resource
532+
type_name = unformat_key(type).to_s
533+
534+
relationship_resource_klass = resource.resource_for(relationship.class_name)
535+
relationship_klass = relationship_resource_klass._model_class
536+
537+
linkage_object_resource_klass = resource.resource_for(type_name)
538+
linkage_object_klass = linkage_object_resource_klass._model_class
539+
540+
unless linkage_object_klass == relationship_klass || linkage_object_klass.in?(relationship_klass.subclasses)
541+
fail JSONAPI::Exceptions::TypeMismatch.new(type_name)
542+
end
543+
544+
relationship_ids = relationship_resource_klass.verify_keys(keys, @context)
545+
polymorphic_results << { type: type, ids: relationship_ids }
546+
end
547+
548+
add_result.call polymorphic_results
549+
else
550+
relationship_type = unformat_key(relationship.type).to_s
551+
552+
if links_object.length > 1 || !links_object.has_key?(relationship_type)
531553
fail JSONAPI::Exceptions::TypeMismatch.new(links_object[:type])
532554
end
533-
add_result.call relationship_resource.verify_keys(keys, @context)
555+
556+
relationship_resource = Resource.resource_for(@resource_klass.module_path + relationship_type)
557+
add_result.call relationship_resource.verify_keys(links_object[relationship_type], @context)
534558
end
535559
end
536560
end

lib/jsonapi/resource.rb

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,26 @@ def _replace_to_many_links(relationship_type, relationship_key_values, options)
302302
to_add = relationship_key_values - (relationship_key_values & existing)
303303
_create_to_many_links(relationship_type, to_add, {})
304304

305+
@reload_needed = true
306+
elsif relationship.polymorphic?
307+
relationship_key_values.each do |relationship_key_value|
308+
relationship_resource_klass = self.class.resource_for(relationship_key_value[:type])
309+
ids = relationship_key_value[:ids]
310+
311+
related_records = relationship_resource_klass
312+
.records(options)
313+
.where({relationship_resource_klass._primary_key => ids})
314+
315+
missed_ids = ids - related_records.pluck(relationship_resource_klass._primary_key)
316+
317+
if missed_ids.present?
318+
fail JSONAPI::Exceptions::RecordNotFound.new(missed_ids)
319+
end
320+
321+
relation_name = relationship.relation_name(context: @context)
322+
@model.send("#{relation_name}") << related_records
323+
end
324+
305325
@reload_needed = true
306326
else
307327
send("#{relationship.foreign_key}=", relationship_key_values)
@@ -565,7 +585,14 @@ def has_many(*attrs)
565585
_add_relationship(Relationship::ToMany, *attrs)
566586
end
567587

588+
# @model_class is inherited from superclass, and this causes some issues:
589+
# ```
590+
# CarResource._model_class #=> Vehicle # it should be Car
591+
# ```
592+
# so in order to invoke the right class from subclasses,
593+
# we should call this method to override it.
568594
def model_name(model, options = {})
595+
@model_class = nil
569596
@_model_name = model.to_sym
570597

571598
model_hint(model: @_model_name, resource: self) unless options[:add_model_hint] == false

test/fixtures/active_record.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1028,10 +1028,12 @@ class VehicleResource < JSONAPI::Resource
10281028
end
10291029

10301030
class CarResource < VehicleResource
1031+
model_name "Car"
10311032
attributes :drive_layout
10321033
end
10331034

10341035
class BoatResource < VehicleResource
1036+
model_name "Boat"
10351037
attributes :length_at_water_line
10361038
end
10371039

test/fixtures/vehicles.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,21 @@ Launch20:
1515
length_at_water_line: 15.5ft
1616
serial_number: 434253JJJSD
1717
person_id: 1
18+
19+
M5:
20+
id: 3
21+
type: Car
22+
make: BMW
23+
model: M5
24+
drive_layout: Front Engine RWD
25+
serial_number: 56256
26+
person_id: 2
27+
28+
M3:
29+
id: 4
30+
type: Car
31+
make: BMW
32+
model: M3
33+
drive_layout: Front Engine RWD
34+
serial_number: 894345
35+
person_id: 2

test/integration/requests/request_test.rb

Lines changed: 140 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ def test_post_single
216216
assert_jsonapi_response 201
217217
end
218218

219-
def test_post_polymorphic
219+
def test_post_polymorphic_with_has_many_relationship
220220
post '/people', params:
221221
{
222222
'data' => {
@@ -227,7 +227,14 @@ def test_post_polymorphic
227227
'date_joined' => 'Thu, 01 Jan 2019 00:00:00 UTC +00:00',
228228
},
229229
'relationships' => {
230-
'vehicles' => {'data' => [{'type' => 'car', 'id' => '1'}]},
230+
'vehicles' => {
231+
'data' => [
232+
{'type' => 'car', 'id' => '1'},
233+
{'type' => 'boat', 'id' => '2'},
234+
{'type' => 'car', 'id' => '3'},
235+
{'type' => 'car', 'id' => '4'}
236+
]
237+
}
231238
}
232239
}
233240
}.to_json,
@@ -237,9 +244,19 @@ def test_post_polymorphic
237244
}
238245

239246
assert_jsonapi_response 201
247+
248+
body = JSON.parse(response.body)
249+
person = Person.find(body.dig("data", "id"))
250+
251+
assert_equal "Reo", person.name
252+
assert_equal 4, person.vehicles.count
253+
assert_equal Car, person.vehicles.first.class
254+
assert_equal Boat, person.vehicles.second.class
255+
assert_equal Car, person.vehicles.third.class
256+
assert_equal Car, person.vehicles.fourth.class
240257
end
241258

242-
def test_post_polymorphic_invalid
259+
def test_post_polymorphic_invalid_with_wrong_type
243260
post '/people', params:
244261
{
245262
'data' => {
@@ -262,6 +279,34 @@ def test_post_polymorphic_invalid
262279
assert_jsonapi_response 400, msg: "Submitting a thing as a vehicle should raise a type mismatch error"
263280
end
264281

282+
def test_post_polymorphic_invalid_with_not_matched_type_and_id
283+
post '/people', params:
284+
{
285+
'data' => {
286+
'type' => 'people',
287+
'attributes' => {
288+
'name' => 'Reo',
289+
'email' => 'reo@xyz.fake',
290+
'date_joined' => 'Thu, 01 Jan 2019 00:00:00 UTC +00:00',
291+
},
292+
'relationships' => {
293+
'vehicles' => {
294+
'data' => [
295+
{'type' => 'car', 'id' => '1'},
296+
{'type' => 'car', 'id' => '2'} #vehicle 2 is actually a boat
297+
]
298+
}
299+
}
300+
}
301+
}.to_json,
302+
headers: {
303+
'CONTENT_TYPE' => JSONAPI::MEDIA_TYPE,
304+
'Accept' => JSONAPI::MEDIA_TYPE
305+
}
306+
307+
assert_jsonapi_response 404, msg: "Submitting a thing as a vehicle should raise a record not found"
308+
end
309+
265310
def test_post_single_missing_data_contents
266311
post '/posts', params:
267312
{
@@ -473,6 +518,96 @@ def test_patch_content_type
473518
assert_match JSONAPI::MEDIA_TYPE, headers['Content-Type']
474519
end
475520

521+
def test_patch_polymorphic_with_has_many_relationship
522+
patch '/people/1', params:
523+
{
524+
'data' => {
525+
'id' => 1,
526+
'type' => 'people',
527+
'attributes' => {
528+
'name' => 'Reo',
529+
'email' => 'reo@xyz.fake',
530+
'date_joined' => 'Thu, 01 Jan 2019 00:00:00 UTC +00:00',
531+
},
532+
'relationships' => {
533+
'vehicles' => {
534+
'data' => [
535+
{'type' => 'car', 'id' => '1'},
536+
{'type' => 'boat', 'id' => '2'}
537+
]
538+
}
539+
}
540+
}
541+
}.to_json,
542+
headers: {
543+
'CONTENT_TYPE' => JSONAPI::MEDIA_TYPE,
544+
'Accept' => JSONAPI::MEDIA_TYPE
545+
}
546+
547+
assert_jsonapi_response 200
548+
549+
body = JSON.parse(response.body)
550+
person = Person.find(body.dig("data", "id"))
551+
552+
assert_equal "Reo", person.name
553+
assert_equal 2, person.vehicles.count
554+
assert_equal Car, person.vehicles.first.class
555+
assert_equal Boat, person.vehicles.second.class
556+
end
557+
558+
def test_patch_polymorphic_invalid_with_wrong_type
559+
patch '/people/1', params:
560+
{
561+
'data' => {
562+
'id' => 1,
563+
'type' => 'people',
564+
'attributes' => {
565+
'name' => 'Reo',
566+
'email' => 'reo@xyz.fake',
567+
'date_joined' => 'Thu, 01 Jan 2019 00:00:00 UTC +00:00',
568+
},
569+
'relationships' => {
570+
'vehicles' => {'data' => [{'type' => 'author', 'id' => '1'}]},
571+
}
572+
}
573+
}.to_json,
574+
headers: {
575+
'CONTENT_TYPE' => JSONAPI::MEDIA_TYPE,
576+
'Accept' => JSONAPI::MEDIA_TYPE
577+
}
578+
579+
assert_jsonapi_response 400, msg: "Submitting a thing as a vehicle should raise a type mismatch error"
580+
end
581+
582+
def test_patch_polymorphic_invalid_with_not_matched_type_and_id
583+
patch '/people/1', params:
584+
{
585+
'data' => {
586+
'id' => 1,
587+
'type' => 'people',
588+
'attributes' => {
589+
'name' => 'Reo',
590+
'email' => 'reo@xyz.fake',
591+
'date_joined' => 'Thu, 01 Jan 2019 00:00:00 UTC +00:00',
592+
},
593+
'relationships' => {
594+
'vehicles' => {
595+
'data' => [
596+
{'type' => 'car', 'id' => '1'},
597+
{'type' => 'car', 'id' => '2'} #vehicle 2 is actually a boat
598+
]
599+
}
600+
}
601+
}
602+
}.to_json,
603+
headers: {
604+
'CONTENT_TYPE' => JSONAPI::MEDIA_TYPE,
605+
'Accept' => JSONAPI::MEDIA_TYPE
606+
}
607+
608+
assert_jsonapi_response 404, msg: "Submitting a thing as a vehicle should raise a record not found"
609+
end
610+
476611
def test_post_correct_content_type
477612
post '/posts', params:
478613
{
@@ -1177,8 +1312,8 @@ def test_include_value_missing
11771312

11781313
def test_getting_different_resources_when_sti
11791314
assert_cacheable_jsonapi_get '/vehicles'
1180-
types = json_response['data'].map{|r| r['type']}.sort
1181-
assert_array_equals ['boats', 'cars'], types
1315+
types = json_response['data'].map{|r| r['type']}.to_set
1316+
assert types == Set['cars', 'boats']
11821317
end
11831318

11841319
def test_getting_resource_with_correct_type_when_sti

0 commit comments

Comments
 (0)