Skip to content

Commit 2e8afd4

Browse files
committed
Support for polymorphic associations
1 parent bb7bc45 commit 2e8afd4

File tree

7 files changed

+302
-7
lines changed

7 files changed

+302
-7
lines changed

lib/fast_jsonapi/object_serializer.rb

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,8 @@ def has_many(relationship_name, options = {})
147147
object_method_name: options[:object_method_name] || name,
148148
serializer: compute_serializer_name(serializer_key),
149149
relationship_type: :has_many,
150-
cached: options[:cached] || false
150+
cached: options[:cached] || false,
151+
polymorphic: options.fetch(:polymorphic, false)
151152
}
152153
add_relationship(name, relationship)
153154
end
@@ -169,7 +170,8 @@ def belongs_to(relationship_name, options = {})
169170
object_method_name: options[:object_method_name] || name,
170171
serializer: compute_serializer_name(serializer_key),
171172
relationship_type: :belongs_to,
172-
cached: options[:cached] || true
173+
cached: options[:cached] || true,
174+
polymorphic: options.fetch(:polymorphic, false)
173175
}
174176
add_relationship(name, relationship)
175177
end
@@ -191,7 +193,8 @@ def has_one(relationship_name, options = {})
191193
object_method_name: options[:object_method_name] || name,
192194
serializer: compute_serializer_name(serializer_key),
193195
relationship_type: :has_one,
194-
cached: options[:cached] || false
196+
cached: options[:cached] || false,
197+
polymorphic: options.fetch(:polymorphic, false)
195198
}
196199
add_relationship(name, relationship)
197200
end

lib/fast_jsonapi/serialization_core.rb

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,21 @@ def ids_hash(ids, record_type)
2626
id_hash(ids, record_type) # ids variable is just a single id here
2727
end
2828

29+
def id_hash_from_record(record)
30+
{ id: record.id.to_s, type: record.class.name.underscore.to_sym }
31+
end
32+
33+
def ids_hash_from_record_and_relationship(record, relationship)
34+
return ids_hash(record.send(relationship[:id_method_name]), relationship[:record_type]) \
35+
unless relationship[:polymorphic]
36+
37+
object_method_name = relationship.fetch(:object_method_name, relationship[:name])
38+
return unless associated_object = record.send(object_method_name)
39+
40+
return id_hash_from_record(associated_object) unless associated_object.respond_to? :map
41+
associated_object.map { |object| id_hash_from_record(object) }
42+
end
43+
2944
def attributes_hash(record)
3045
attributes_hash = {}
3146
attributes_to_serialize.each do |key, method_name|
@@ -40,11 +55,9 @@ def relationships_hash(record, relationships = nil)
4055

4156
relationships.each do |_k, relationship|
4257
name = relationship[:key]
43-
id_method_name = relationship[:id_method_name]
44-
record_type = relationship[:record_type]
4558
empty_case = relationship[:relationship_type] == :has_many ? [] : nil
4659
relationships_hash[name] = {
47-
data: ids_hash(record.send(id_method_name), record_type) || empty_case
60+
data: ids_hash_from_record_and_relationship(record, relationship) || empty_case
4861
}
4962
end
5063
relationships_hash

spec/lib/object_serializer_performance_spec.rb

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
describe FastJsonapi::ObjectSerializer, performance: true do
44
include_context 'movie class'
55
include_context 'ams movie class'
6+
include_context 'group class'
7+
include_context 'ams group class'
68

79
before(:all) { GC.disable }
810
after(:all) { GC.enable }
@@ -107,4 +109,27 @@ def run_json_benchmark(message, movie_count, our_serializer, ams_serializer)
107109
end
108110
end
109111
end
112+
113+
context 'when comparing with AMS 0.10.x and with polymorphic has_many' do
114+
[1, 25, 250, 1000].each do |group_count|
115+
speed_factor = 5
116+
it "should serialize #{group_count} records at least #{speed_factor} times faster than AMS" do
117+
ams_groups = build_ams_groups(group_count)
118+
groups = build_groups(group_count)
119+
options = {}
120+
our_serializer = GroupSerializer.new(groups, options)
121+
ams_serializer = ActiveModelSerializers::SerializableResource.new(ams_groups)
122+
123+
message = "Serialize to JSON string #{group_count} with polymorphic has_many"
124+
our_json, ams_json = run_json_benchmark(message, group_count, our_serializer, ams_serializer)
125+
126+
message = "Serialize to Ruby Hash #{group_count} with polymorphic has_many"
127+
run_hash_benchmark(message, group_count, our_serializer, ams_serializer)
128+
129+
expect(our_json.length).to eq ams_json.length
130+
expect { our_serializer.serialized_json }.to perform_faster_than { ams_serializer.to_json }.at_least(speed_factor).times
131+
expect { our_serializer.serializable_hash }.to perform_faster_than { ams_serializer.as_json }.at_least(speed_factor).times
132+
end
133+
end
134+
end
110135
end

spec/lib/serialization_core_spec.rb

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
describe FastJsonapi::ObjectSerializer do
44
include_context "movie class"
5+
include_context 'group class'
56

67
context 'when testing class methods of serialization core' do
78
it 'returns correct hash when id_hash is called' do
@@ -16,6 +17,12 @@
1617
expect(result_hash).to be nil
1718
end
1819

20+
it 'returns the correct hash when ids_hash_from_record_and_relationship is called for a polymorphic association' do
21+
relationship = { name: :groupees, relationship_type: :has_many, polymorphic: true }
22+
results = GroupSerializer.send :ids_hash_from_record_and_relationship, group, relationship
23+
expect(results).to include({ id: "1", type: :person }, { id: "2", type: :group })
24+
end
25+
1926
it 'returns correct hash when ids_hash is called' do
2027
inputs = [{ids: %w(1 2 3), record_type: :movie}, {ids: %w(x y z), record_type: 'person'}]
2128
inputs.each do |hash|
@@ -80,5 +87,4 @@
8087
expect(included_records.size).to eq 3
8188
end
8289
end
83-
8490
end

spec/shared/contexts/ams_context.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,14 @@ class AMSMovieTypeSerializer < ActiveModel::Serializer
3737
type 'movie_type'
3838
attributes :name
3939
end
40+
41+
class AMSRole < ActiveModelSerializers::Model
42+
attr_accessor :id, :name, :actor, :target
43+
end
44+
45+
class AMSGroup < ActiveModelSerializers::Model
46+
attr_accessor :id, :name
47+
end
4048
end
4149

4250
after(:context) do
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
RSpec.shared_context 'ams group class' do
2+
before(:context) do
3+
# models
4+
class AMSPerson < ActiveModelSerializers::Model
5+
attr_accessor :id, :first_name, :last_name
6+
end
7+
8+
class AMSGroup < ActiveModelSerializers::Model
9+
attr_accessor :id, :name, :groupees
10+
end
11+
12+
# serializers
13+
class AMSPersonSerializer < ActiveModel::Serializer
14+
type 'person'
15+
attributes :first_name, :last_name
16+
end
17+
18+
class AMSGroupSerializer < ActiveModel::Serializer
19+
type 'group'
20+
attributes :name
21+
has_many :groupees
22+
end
23+
end
24+
25+
after(:context) do
26+
classes_to_remove = %i[AMSPerson AMSGroup AMSPersonSerializer AMSGroupSerializer]
27+
classes_to_remove.each do |klass_name|
28+
Object.send(:remove_const, klass_name) if Object.constants.include?(klass_name)
29+
end
30+
end
31+
32+
let(:ams_groups) do
33+
group_count = 0
34+
person_count = 0
35+
3.times.map do |i|
36+
group = AMSGroup.new
37+
group.id = group_count + 1
38+
group.name = "Test Group #{group.id}"
39+
group_count = group.id
40+
41+
person = AMSPerson.new
42+
person.id = person_count + 1
43+
person.last_name = "Last Name #{person.id}"
44+
person.first_name = "First Name #{person.id}"
45+
person_count = person.id
46+
47+
child_group = AMSGroup.new
48+
child_group.id = group_count + 1
49+
child_group.name = "Test Group #{child_group.id}"
50+
group_count = child_group.id
51+
52+
group.groupees = [person, child_group]
53+
group
54+
end
55+
end
56+
57+
let(:ams_person) do
58+
ams_person = AMSPerson.new
59+
ams_person.id = 3
60+
ams_person
61+
end
62+
63+
def build_ams_groups(count)
64+
group_count = 0
65+
person_count = 0
66+
count.times.map do |i|
67+
group = AMSGroup.new
68+
group.id = group_count + 1
69+
group.name = "Test Group #{group.id}"
70+
group_count = group.id
71+
72+
person = AMSPerson.new
73+
person.id = person_count + 1
74+
person.last_name = "Last Name #{person.id}"
75+
person.first_name = "First Name #{person.id}"
76+
person_count = person.id
77+
78+
child_group = AMSGroup.new
79+
child_group.id = group_count + 1
80+
child_group.name = "Test Group #{child_group.id}"
81+
group_count = child_group.id
82+
83+
group.groupees = [person, child_group]
84+
group
85+
end
86+
end
87+
end

spec/shared/contexts/group_context.rb

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
RSpec.shared_context 'group class' do
2+
3+
# Person, Group Classes and serializers
4+
before(:context) do
5+
# models
6+
class Person
7+
attr_accessor :id, :first_name, :last_name
8+
end
9+
10+
class Group
11+
attr_accessor :id, :name, :groupees # Let's assume groupees can be Person or Group objects
12+
end
13+
14+
# serializers
15+
class PersonSerializer
16+
include FastJsonapi::ObjectSerializer
17+
set_type :person
18+
attributes :first_name, :last_name
19+
end
20+
21+
class GroupSerializer
22+
include FastJsonapi::ObjectSerializer
23+
set_type :group
24+
attributes :name
25+
has_many :groupees, polymorphic: true
26+
end
27+
end
28+
29+
30+
# Namespaced PersonSerializer
31+
before(:context) do
32+
# namespaced model stub
33+
module AppName
34+
module V1
35+
class PersonSerializer
36+
include FastJsonapi::ObjectSerializer
37+
# to test if compute_serializer_name works
38+
end
39+
end
40+
end
41+
end
42+
43+
44+
# Hyphenated keys for the serializer
45+
before(:context) do
46+
class HyphenPersonSerializer
47+
include FastJsonapi::ObjectSerializer
48+
use_hyphen
49+
set_type :person
50+
attributes :first_name, :last_name
51+
end
52+
53+
class HyphenGroupSerializer
54+
include FastJsonapi::ObjectSerializer
55+
use_hyphen
56+
set_type :group
57+
attributes :name
58+
has_many :groupees
59+
end
60+
end
61+
62+
63+
# Movie and Actor struct
64+
before(:context) do
65+
PersonStruct = Struct.new(
66+
:id, :first_name, :last_name
67+
)
68+
69+
GroupStruct = Struct.new(
70+
:id, :name, :groupees, :groupee_ids
71+
)
72+
end
73+
74+
after(:context) do
75+
classes_to_remove = %i[
76+
Person
77+
PersonSerializer
78+
Group
79+
GroupSerializer
80+
AppName::V1::PersonSerializer
81+
PersonStruct
82+
GroupStruct
83+
HyphenPersonSerializer
84+
HyphenGroupSerializer
85+
]
86+
classes_to_remove.each do |klass_name|
87+
Object.send(:remove_const, klass_name) if Object.constants.include?(klass_name)
88+
end
89+
end
90+
91+
let(:group_struct) do
92+
group = GroupStruct.new
93+
group[:id] = 1
94+
group[:name] = 'Group 1'
95+
group[:groupees] = []
96+
97+
person = PersonStruct.new
98+
person[:id] = 1
99+
person[:last_name] = "Last Name 1"
100+
person[:first_name] = "First Name 1"
101+
102+
child_group = GroupStruct.new
103+
child_group[:id] = 2
104+
child_group[:name] = 'Group 2'
105+
106+
group.groupees = [person, child_group]
107+
group
108+
end
109+
110+
let(:group) do
111+
group = Group.new
112+
group.id = 1
113+
group.name = 'Group 1'
114+
115+
person = Person.new
116+
person.id = 1
117+
person.last_name = "Last Name 1"
118+
person.first_name = "First Name 1"
119+
120+
child_group = Group.new
121+
child_group.id = 2
122+
child_group.name = 'Group 2'
123+
124+
group.groupees = [person, child_group]
125+
group
126+
end
127+
128+
def build_groups(count)
129+
group_count = 0
130+
person_count = 0
131+
132+
count.times.map do |i|
133+
group = Group.new
134+
group.id = group_count + 1
135+
group.name = "Test Group #{group.id}"
136+
group_count = group.id
137+
138+
person = Person.new
139+
person.id = person_count + 1
140+
person.last_name = "Last Name #{person.id}"
141+
person.first_name = "First Name #{person.id}"
142+
person_count = person.id
143+
144+
child_group = Group.new
145+
child_group.id = group_count + 1
146+
child_group.name = "Test Group #{child_group.id}"
147+
group_count = child_group.id
148+
149+
group.groupees = [person, child_group]
150+
group
151+
end
152+
end
153+
end

0 commit comments

Comments
 (0)