Skip to content

Commit e6922ed

Browse files
st0012lgebhardt
authored andcommitted
Port polymorphic to many linkage parsing from 0.9
Closes gh-1233
1 parent 50144c4 commit e6922ed

File tree

5 files changed

+259
-12
lines changed

5 files changed

+259
-12
lines changed

lib/jsonapi/request_parser.rb

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -556,20 +556,39 @@ def parse_to_many_relationship(resource_klass, link_value, relationship, &add_re
556556

557557
links_object = parse_to_many_links_object(linkage)
558558

559-
# Since we do not yet support polymorphic to_many relationships we will raise an error if the type does not match the
560-
# relationship's type.
561-
# ToDo: Support Polymorphic relationships
562-
563559
if links_object.length == 0
564560
add_result.call([])
565561
else
566-
if links_object.length > 1 || !links_object.has_key?(unformat_key(relationship.type).to_s)
567-
fail JSONAPI::Exceptions::TypeMismatch.new(links_object[:type], error_object_overrides)
568-
end
562+
if relationship.polymorphic?
563+
polymorphic_results = []
564+
565+
links_object.each_pair do |type, keys|
566+
type_name = unformat_key(type).to_s
567+
568+
relationship_resource_klass = resource_klass.resource_klass_for(relationship.class_name)
569+
relationship_klass = relationship_resource_klass._model_class
570+
571+
linkage_object_resource_klass = resource_klass.resource_klass_for(type_name)
572+
linkage_object_klass = linkage_object_resource_klass._model_class
573+
574+
unless linkage_object_klass == relationship_klass || linkage_object_klass.in?(relationship_klass.subclasses)
575+
fail JSONAPI::Exceptions::TypeMismatch.new(type_name)
576+
end
577+
578+
relationship_ids = relationship_resource_klass.verify_keys(keys, @context)
579+
polymorphic_results << { type: type, ids: relationship_ids }
580+
end
581+
582+
add_result.call polymorphic_results
583+
else
584+
relationship_type = unformat_key(relationship.type).to_s
585+
586+
if links_object.length > 1 || !links_object.has_key?(relationship_type)
587+
fail JSONAPI::Exceptions::TypeMismatch.new(links_object[:type])
588+
end
569589

570-
links_object.each_pair do |type, keys|
571-
relationship_resource = Resource.resource_klass_for(resource_klass.module_path + unformat_key(type).to_s)
572-
add_result.call relationship_resource.verify_keys(keys, @context)
590+
relationship_resource_klass = Resource.resource_klass_for(resource_klass.module_path + relationship_type)
591+
add_result.call relationship_resource_klass.verify_keys(links_object[relationship_type], @context)
573592
end
574593
end
575594
end

lib/jsonapi/resource.rb

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

296+
@reload_needed = true
297+
elsif relationship.polymorphic?
298+
relationship_key_values.each do |relationship_key_value|
299+
relationship_resource_klass = self.class.resource_klass_for(relationship_key_value[:type])
300+
ids = relationship_key_value[:ids]
301+
302+
related_records = relationship_resource_klass
303+
.records(options)
304+
.where({relationship_resource_klass._primary_key => ids})
305+
306+
missed_ids = ids - related_records.pluck(relationship_resource_klass._primary_key)
307+
308+
if missed_ids.present?
309+
fail JSONAPI::Exceptions::RecordNotFound.new(missed_ids)
310+
end
311+
312+
relation_name = relationship.relation_name(context: @context)
313+
@model.send("#{relation_name}") << related_records
314+
end
315+
296316
@reload_needed = true
297317
else
298318
send("#{relationship.foreign_key}=", relationship_key_values)
@@ -595,7 +615,14 @@ def has_many(*attrs)
595615
_add_relationship(Relationship::ToMany, *attrs)
596616
end
597617

618+
# @model_class is inherited from superclass, and this causes some issues:
619+
# ```
620+
# CarResource._model_class #=> Vehicle # it should be Car
621+
# ```
622+
# so in order to invoke the right class from subclasses,
623+
# we should call this method to override it.
598624
def model_name(model, options = {})
625+
@model_class = nil
599626
@_model_name = model.to_sym
600627

601628
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
@@ -1318,10 +1318,12 @@ class VehicleResource < JSONAPI::Resource
13181318
end
13191319

13201320
class CarResource < VehicleResource
1321+
model_name "Car"
13211322
attributes :drive_layout
13221323
end
13231324

13241325
class BoatResource < VehicleResource
1326+
model_name "Boat"
13251327
attributes :length_at_water_line
13261328
end
13271329

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: 1001
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: 183 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,97 @@ def test_post_single
312312
assert_jsonapi_response 201
313313
end
314314

315+
def test_post_polymorphic_with_has_many_relationship
316+
post '/people', params:
317+
{
318+
'data' => {
319+
'type' => 'people',
320+
'attributes' => {
321+
'name' => 'Reo',
322+
'email' => '[email protected]',
323+
'date_joined' => 'Thu, 01 Jan 2019 00:00:00 UTC +00:00',
324+
},
325+
'relationships' => {
326+
'vehicles' => {
327+
'data' => [
328+
{'type' => 'car', 'id' => '1'},
329+
{'type' => 'boat', 'id' => '2'},
330+
{'type' => 'car', 'id' => '3'},
331+
{'type' => 'car', 'id' => '4'}
332+
]
333+
}
334+
}
335+
}
336+
}.to_json,
337+
headers: {
338+
'CONTENT_TYPE' => JSONAPI::MEDIA_TYPE,
339+
'Accept' => JSONAPI::MEDIA_TYPE
340+
}
341+
342+
assert_jsonapi_response 201
343+
344+
body = JSON.parse(response.body)
345+
person = Person.find(body.dig("data", "id"))
346+
347+
assert_equal "Reo", person.name
348+
assert_equal 4, person.vehicles.count
349+
assert_equal Car, person.vehicles.first.class
350+
assert_equal Boat, person.vehicles.second.class
351+
assert_equal Car, person.vehicles.third.class
352+
assert_equal Car, person.vehicles.fourth.class
353+
end
354+
355+
def test_post_polymorphic_invalid_with_wrong_type
356+
post '/people', params:
357+
{
358+
'data' => {
359+
'type' => 'people',
360+
'attributes' => {
361+
'name' => 'Reo',
362+
'email' => '[email protected]',
363+
'date_joined' => 'Thu, 01 Jan 2019 00:00:00 UTC +00:00',
364+
},
365+
'relationships' => {
366+
'vehicles' => {'data' => [{'type' => 'author', 'id' => '1'}]},
367+
}
368+
}
369+
}.to_json,
370+
headers: {
371+
'CONTENT_TYPE' => JSONAPI::MEDIA_TYPE,
372+
'Accept' => JSONAPI::MEDIA_TYPE
373+
}
374+
375+
assert_jsonapi_response 400, msg: "Submitting a thing as a vehicle should raise a type mismatch error"
376+
end
377+
378+
def test_post_polymorphic_invalid_with_not_matched_type_and_id
379+
post '/people', params:
380+
{
381+
'data' => {
382+
'type' => 'people',
383+
'attributes' => {
384+
'name' => 'Reo',
385+
'email' => '[email protected]',
386+
'date_joined' => 'Thu, 01 Jan 2019 00:00:00 UTC +00:00',
387+
},
388+
'relationships' => {
389+
'vehicles' => {
390+
'data' => [
391+
{'type' => 'car', 'id' => '1'},
392+
{'type' => 'car', 'id' => '2'} #vehicle 2 is actually a boat
393+
]
394+
}
395+
}
396+
}
397+
}.to_json,
398+
headers: {
399+
'CONTENT_TYPE' => JSONAPI::MEDIA_TYPE,
400+
'Accept' => JSONAPI::MEDIA_TYPE
401+
}
402+
403+
assert_jsonapi_response 404, msg: "Submitting a thing as a vehicle should raise a record not found"
404+
end
405+
315406
def test_post_single_missing_data_contents
316407
post '/posts', params:
317408
{
@@ -523,6 +614,96 @@ def test_patch_content_type
523614
assert_match JSONAPI::MEDIA_TYPE, headers['Content-Type']
524615
end
525616

617+
def test_patch_polymorphic_with_has_many_relationship
618+
patch '/people/1000', params:
619+
{
620+
'data' => {
621+
'id' => 1000,
622+
'type' => 'people',
623+
'attributes' => {
624+
'name' => 'Reo',
625+
'email' => '[email protected]',
626+
'date_joined' => 'Thu, 01 Jan 2019 00:00:00 UTC +00:00',
627+
},
628+
'relationships' => {
629+
'vehicles' => {
630+
'data' => [
631+
{'type' => 'car', 'id' => '1'},
632+
{'type' => 'boat', 'id' => '2'}
633+
]
634+
}
635+
}
636+
}
637+
}.to_json,
638+
headers: {
639+
'CONTENT_TYPE' => JSONAPI::MEDIA_TYPE,
640+
'Accept' => JSONAPI::MEDIA_TYPE
641+
}
642+
643+
assert_jsonapi_response 200
644+
645+
body = JSON.parse(response.body)
646+
person = Person.find(body.dig("data", "id"))
647+
648+
assert_equal "Reo", person.name
649+
assert_equal 2, person.vehicles.count
650+
assert_equal Car, person.vehicles.first.class
651+
assert_equal Boat, person.vehicles.second.class
652+
end
653+
654+
def test_patch_polymorphic_invalid_with_wrong_type
655+
patch '/people/1000', params:
656+
{
657+
'data' => {
658+
'id' => 1000,
659+
'type' => 'people',
660+
'attributes' => {
661+
'name' => 'Reo',
662+
'email' => '[email protected]',
663+
'date_joined' => 'Thu, 01 Jan 2019 00:00:00 UTC +00:00',
664+
},
665+
'relationships' => {
666+
'vehicles' => {'data' => [{'type' => 'author', 'id' => '1'}]},
667+
}
668+
}
669+
}.to_json,
670+
headers: {
671+
'CONTENT_TYPE' => JSONAPI::MEDIA_TYPE,
672+
'Accept' => JSONAPI::MEDIA_TYPE
673+
}
674+
675+
assert_jsonapi_response 400, msg: "Submitting a thing as a vehicle should raise a type mismatch error"
676+
end
677+
678+
def test_patch_polymorphic_invalid_with_not_matched_type_and_id
679+
patch '/people/1000', params:
680+
{
681+
'data' => {
682+
'id' => 1000,
683+
'type' => 'people',
684+
'attributes' => {
685+
'name' => 'Reo',
686+
'email' => '[email protected]',
687+
'date_joined' => 'Thu, 01 Jan 2019 00:00:00 UTC +00:00',
688+
},
689+
'relationships' => {
690+
'vehicles' => {
691+
'data' => [
692+
{'type' => 'car', 'id' => '1'},
693+
{'type' => 'car', 'id' => '2'} #vehicle 2 is actually a boat
694+
]
695+
}
696+
}
697+
}
698+
}.to_json,
699+
headers: {
700+
'CONTENT_TYPE' => JSONAPI::MEDIA_TYPE,
701+
'Accept' => JSONAPI::MEDIA_TYPE
702+
}
703+
704+
assert_jsonapi_response 404, msg: "Submitting a thing as a vehicle should raise a record not found"
705+
end
706+
526707
def test_post_correct_content_type
527708
post '/posts', params:
528709
{
@@ -1278,8 +1459,8 @@ def test_include_parameter_openquoted
12781459

12791460
def test_getting_different_resources_when_sti
12801461
assert_cacheable_jsonapi_get '/vehicles'
1281-
types = json_response['data'].map{|r| r['type']}.sort
1282-
assert_array_equals ['boats', 'cars'], types
1462+
types = json_response['data'].map{|r| r['type']}.to_set
1463+
assert types == Set['cars', 'boats']
12831464
end
12841465

12851466
def test_getting_resource_with_correct_type_when_sti

0 commit comments

Comments
 (0)