Skip to content

Commit 3ebf349

Browse files
jshowshishirmk
authored andcommitted
Serialize nested includes (#152)
1 parent 966b350 commit 3ebf349

7 files changed

Lines changed: 251 additions & 37 deletions

File tree

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ end
207207

208208
### Compound Document
209209

210-
Support for top-level included member through ` options[:include] `.
210+
Support for top-level and nested included associations through ` options[:include] `.
211211

212212
```ruby
213213
options = {}
@@ -217,7 +217,7 @@ options[:links] = {
217217
next: '...',
218218
prev: '...'
219219
}
220-
options[:include] = [:actors]
220+
options[:include] = [:actors, :'actors.agency', :'actors.agency.state']
221221
MovieSerializer.new([movie, movie], options).serialized_json
222222
```
223223

lib/fast_jsonapi/object_serializer.rb

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -77,17 +77,7 @@ def process_options(options)
7777

7878
if options[:include].present?
7979
@includes = options[:include].delete_if(&:blank?).map(&:to_sym)
80-
validate_includes!(@includes)
81-
end
82-
end
83-
84-
def validate_includes!(includes)
85-
return if includes.blank?
86-
87-
existing_relationships = self.class.relationships_to_serialize.keys.to_set
88-
89-
unless existing_relationships.superset?(includes.to_set)
90-
raise ArgumentError, "One of keys from #{includes} is not specified as a relationship on the serializer"
80+
self.class.validate_includes!(@includes)
9181
end
9282
end
9383

@@ -193,7 +183,11 @@ def has_one(relationship_name, options = {}, &block)
193183
add_relationship(name, hash)
194184
end
195185

196-
alias belongs_to has_one
186+
def belongs_to(relationship_name, options = {}, &block)
187+
name = relationship_name.to_sym
188+
hash = create_relationship_hash(relationship_name, :belongs_to, options, block)
189+
add_relationship(name, hash)
190+
end
197191

198192
def create_relationship_hash(base_key, relationship_type, options, block)
199193
name = base_key.to_sym
@@ -234,6 +228,20 @@ def fetch_polymorphic_option(options)
234228
return option if option.respond_to? :keys
235229
{}
236230
end
231+
232+
def validate_includes!(includes)
233+
return if includes.blank?
234+
235+
includes.detect do |include_item|
236+
klass = self
237+
parse_include_item(include_item).each do |parsed_include|
238+
relationship_to_include = klass.relationships_to_serialize[parsed_include]
239+
raise ArgumentError, "#{parsed_include} is not specified as a relationship on #{klass.name}" unless relationship_to_include
240+
241+
klass = relationship_to_include[:serializer].to_s.constantize
242+
end
243+
end
244+
end
237245
end
238246
end
239247
end

lib/fast_jsonapi/serialization_core.rb

Lines changed: 45 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -113,22 +113,48 @@ def to_json(payload)
113113
FastJsonapi::MultiToJson.to_json(payload) if payload.present?
114114
end
115115

116-
# includes handler
116+
def parse_include_item(include_item)
117+
return [include_item.to_sym] unless include_item.to_s.include?('.')
118+
include_item.to_s.split('.').map { |item| item.to_sym }
119+
end
117120

121+
def remaining_items(items)
122+
return unless items.size > 1
123+
124+
items_copy = items.dup
125+
items_copy.delete_at(0)
126+
[items_copy.join('.').to_sym]
127+
end
128+
129+
# includes handler
118130
def get_included_records(record, includes_list, known_included_objects, params = {})
119-
includes_list.each_with_object([]) do |item, included_records|
120-
included_objects = fetch_associated_object(record, @relationships_to_serialize[item], params)
121-
next if included_objects.blank?
122-
123-
record_type = @relationships_to_serialize[item][:record_type]
124-
serializer = @relationships_to_serialize[item][:serializer].to_s.constantize
125-
relationship_type = @relationships_to_serialize[item][:relationship_type]
126-
included_objects = [included_objects] unless relationship_type == :has_many
127-
included_objects.each do |inc_obj|
128-
code = "#{record_type}_#{inc_obj.id}"
129-
next if known_included_objects.key?(code)
130-
known_included_objects[code] = inc_obj
131-
included_records << serializer.record_hash(inc_obj, params)
131+
return unless includes_list.present?
132+
133+
includes_list.sort.each_with_object([]) do |include_item, included_records|
134+
items = parse_include_item(include_item)
135+
items.each do |item|
136+
next unless relationships_to_serialize && relationships_to_serialize[item]
137+
138+
record_type = @relationships_to_serialize[item][:record_type]
139+
serializer = @relationships_to_serialize[item][:serializer].to_s.constantize
140+
relationship_type = @relationships_to_serialize[item][:relationship_type]
141+
142+
included_objects = fetch_associated_object(record, @relationships_to_serialize[item], params)
143+
included_objects = [included_objects] unless relationship_type == :has_many
144+
next if included_objects.blank?
145+
146+
included_objects.each do |inc_obj|
147+
if remaining_items(items)
148+
serializer_records = serializer.get_included_records(inc_obj, remaining_items(items), known_included_objects)
149+
included_records.concat(serializer_records) unless serializer_records.empty?
150+
end
151+
152+
code = "#{record_type}_#{inc_obj.id}"
153+
next if known_included_objects.key?(code)
154+
155+
known_included_objects[code] = inc_obj
156+
included_records << serializer.record_hash(inc_obj, params)
157+
end
132158
end
133159
end
134160
end
@@ -146,7 +172,11 @@ def fetch_id(record, relationship, params)
146172
return object.id
147173
end
148174

149-
record.public_send(relationship[:id_method_name])
175+
if relationship[:relationship_type] == :has_one
176+
record.public_send(relationship[:object_method_name])&.id
177+
else
178+
record.public_send(relationship[:id_method_name])
179+
end
150180
end
151181
end
152182
end

spec/lib/object_serializer_inheritance_spec.rb

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
CountrySerializer
1111
Employee
1212
EmployeeSerializer
13+
Photo
14+
PhotoSerializer
15+
EmployeeAccount
1316
]
1417
classes_to_remove.each do |klass_name|
1518
Object.send(:remove_const, klass_name) if Object.constants.include?(klass_name)
@@ -19,7 +22,14 @@
1922
class User
2023
attr_accessor :id, :first_name, :last_name
2124

22-
attr_accessor :address_ids, :country_id, :photo_id
25+
attr_accessor :address_ids, :country_id
26+
27+
def photo
28+
p = Photo.new
29+
p.id = 1
30+
p.user_id = id
31+
p
32+
end
2333
end
2434

2535
class UserSerializer
@@ -36,6 +46,15 @@ class UserSerializer
3646
has_one :photo
3747
end
3848

49+
class Photo
50+
attr_accessor :id, :user_id
51+
end
52+
53+
class PhotoSerializer
54+
include FastJsonapi::ObjectSerializer
55+
attributes :id, :name
56+
end
57+
3958
class Country
4059
attr_accessor :id, :name
4160
end
@@ -45,9 +64,19 @@ class CountrySerializer
4564
attributes :name
4665
end
4766

67+
class EmployeeAccount
68+
attr_accessor :id, :employee_id
69+
end
70+
4871
class Employee < User
4972
attr_accessor :id, :location, :compensation
50-
attr_accessor :account_id
73+
74+
def account
75+
a = EmployeeAccount.new
76+
a.id = 1
77+
a.employee_id = id
78+
a
79+
end
5180
end
5281

5382
class EmployeeSerializer < UserSerializer

spec/lib/object_serializer_spec.rb

Lines changed: 81 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 instance methods of object serializer' do
78
it 'returns correct hash when serializable_hash is called' do
@@ -12,7 +13,7 @@
1213
serializable_hash = MovieSerializer.new([movie, movie], options).serializable_hash
1314

1415
expect(serializable_hash[:data].length).to eq 2
15-
expect(serializable_hash[:data][0][:relationships].length).to eq 3
16+
expect(serializable_hash[:data][0][:relationships].length).to eq 4
1617
expect(serializable_hash[:data][0][:attributes].length).to eq 2
1718

1819
expect(serializable_hash[:meta]).to be_instance_of(Hash)
@@ -126,6 +127,85 @@
126127
end
127128
end
128129

130+
context 'nested includes' do
131+
it 'has_many to belongs_to: returns correct nested includes when serializable_hash is called' do
132+
# 3 actors, 3 agencies
133+
include_object_total = 6
134+
135+
options = {}
136+
options[:include] = [:actors, :'actors.agency']
137+
serializable_hash = MovieSerializer.new([movie], options).serializable_hash
138+
139+
expect(serializable_hash[:included]).to be_instance_of(Array)
140+
expect(serializable_hash[:included].length).to eq include_object_total
141+
(0..include_object_total-1).each do |include|
142+
expect(serializable_hash[:included][include]).to be_instance_of(Hash)
143+
end
144+
145+
options[:include] = [:'actors.agency']
146+
serializable_hash = MovieSerializer.new([movie], options).serializable_hash
147+
148+
expect(serializable_hash[:included]).to be_instance_of(Array)
149+
expect(serializable_hash[:included].length).to eq include_object_total
150+
(0..include_object_total-1).each do |include|
151+
expect(serializable_hash[:included][include]).to be_instance_of(Hash)
152+
end
153+
end
154+
155+
it '`has_many` to `belongs_to` to `belongs_to` - returns correct nested includes when serializable_hash is called' do
156+
# 3 actors, 3 agencies, 1 state
157+
include_object_total = 7
158+
159+
options = {}
160+
options[:include] = [:actors, :'actors.agency', :'actors.agency.state']
161+
serializable_hash = MovieSerializer.new([movie], options).serializable_hash
162+
163+
expect(serializable_hash[:included]).to be_instance_of(Array)
164+
expect(serializable_hash[:included].length).to eq include_object_total
165+
166+
actors_serialized = serializable_hash[:included].find_all { |included| included[:type] == :actor }.map { |included| included[:id].to_i }
167+
agencies_serialized = serializable_hash[:included].find_all { |included| included[:type] == :agency }.map { |included| included[:id].to_i }
168+
states_serialized = serializable_hash[:included].find_all { |included| included[:type] == :state }.map { |included| included[:id].to_i }
169+
170+
movie.actors.each do |actor|
171+
expect(actors_serialized).to include(actor.id)
172+
end
173+
174+
agencies = movie.actors.map(&:agency).uniq
175+
agencies.each do |agency|
176+
expect(agencies_serialized).to include(agency.id)
177+
end
178+
179+
states = agencies.map(&:state).uniq
180+
states.each do |state|
181+
expect(states_serialized).to include(state.id)
182+
end
183+
end
184+
it 'has_many => has_one returns correct nested includes when serializable_hash is called' do
185+
options = {}
186+
options[:include] = [:movies, :'movies.advertising_campaign']
187+
serializable_hash = MovieTypeSerializer.new([movie_type], options).serializable_hash
188+
189+
movies_serialized = serializable_hash[:included].find_all { |included| included[:type] == :movie }.map { |included| included[:id].to_i }
190+
advertising_campaigns_serialized = serializable_hash[:included].find_all { |included| included[:type] == :advertising_campaign }.map { |included| included[:id].to_i }
191+
192+
movies = movie_type.movies
193+
movies.each do |movie|
194+
expect(movies_serialized).to include(movie.id)
195+
end
196+
197+
advertising_campaigns = movies.map(&:advertising_campaign)
198+
advertising_campaigns.each do |advertising_campaign|
199+
expect(advertising_campaigns_serialized).to include(advertising_campaign.id)
200+
end
201+
end
202+
it 'polymorphic' do
203+
options = {}
204+
options[:include] = [:groupees]
205+
serializable_hash = GroupSerializer.new([group], options).serializable_hash
206+
end
207+
end
208+
129209
context 'when testing included do block of object serializer' do
130210
it 'should set default_type based on serializer class name' do
131211
class BlahSerializer

spec/lib/object_serializer_struct_spec.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
serializable_hash = MovieSerializer.new([movie_struct, movie_struct], options).serializable_hash
1313

1414
expect(serializable_hash[:data].length).to eq 2
15-
expect(serializable_hash[:data][0][:relationships].length).to eq 3
15+
expect(serializable_hash[:data][0][:relationships].length).to eq 4
1616
expect(serializable_hash[:data][0][:attributes].length).to eq 2
1717

1818
expect(serializable_hash[:meta]).to be_instance_of(Hash)

0 commit comments

Comments
 (0)