Skip to content

Commit b80e021

Browse files
author
Lee Richmond
committed
Improve recursive sideloading logic
This allows us to re-use the same resource multiple times, and configure a variable depth (improving performance).
1 parent 9bf1b41 commit b80e021

File tree

3 files changed

+144
-38
lines changed

3 files changed

+144
-38
lines changed

lib/jsonapi_compliable/sideload.rb

Lines changed: 38 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,21 @@ def initialize(name, type: nil, resource: nil, polymorphic: false, primary_key:
4343
extend @resource_class.config[:adapter].sideloading_module
4444
end
4545

46+
# @api private
47+
def self.max_depth
48+
@max_depth || 2
49+
end
50+
51+
# Set maximum levels of sideload recursion
52+
# /authors?comments.authors would be one level
53+
# /authors?comments.authors.comments.authors would be two levels
54+
# etc
55+
#
56+
# Default max depth is 2
57+
def self.max_depth=(val)
58+
@max_depth = val
59+
end
60+
4661
# @see #resource_class
4762
# @return [Resource] an instance of +#resource_class+
4863
def resource
@@ -331,6 +346,19 @@ def sideload(name)
331346
@sideloads[name]
332347
end
333348

349+
# @api private
350+
def all_sideloads
351+
{}.tap do |all|
352+
if polymorphic?
353+
polymorphic_groups.each_pair do |type, sl|
354+
all.merge!(sl.resource.sideloading.all_sideloads)
355+
end
356+
else
357+
all.merge!(@sideloads.merge(resource.sideloading.sideloads))
358+
end
359+
end
360+
end
361+
334362
# Looks at all nested sideload, and all nested sideloads for the
335363
# corresponding Resources, and returns an Include Directive hash
336364
#
@@ -354,27 +382,20 @@ def sideload(name)
354382
#
355383
# @return [Hash] The nested include hash
356384
# @api private
357-
def to_hash(processed = [])
358-
# Cut off at 5 recursions
359-
if processed.select { |p| p == self }.length == 5
360-
return { name => {} }
361-
end
362-
processed << self
385+
def to_hash(depth_chain = [], parent = nil)
386+
depth = depth_chain.select { |arr| arr == [parent, self] }.length
387+
return {} if depth >= self.class.max_depth
363388

364-
result = { name => {} }.tap do |hash|
365-
@sideloads.each_pair do |key, sideload|
366-
hash[name][key] = sideload.to_hash(processed)[key] || {}
389+
unless (parent && parent.name == :base) || name == :base
390+
depth_chain += [[parent, self]]
391+
end
367392

368-
if sideload.polymorphic?
369-
sideload.polymorphic_groups.each_pair do |type, sl|
370-
hash[name][key].merge!(nested_sideload_hash(sl, processed))
371-
end
372-
else
373-
hash[name][key].merge!(nested_sideload_hash(sideload, processed))
374-
end
393+
{ name => {} }.tap do |hash|
394+
all_sideloads.each_pair do |key, sl|
395+
sideload_hash = sl.to_hash(depth_chain, self)
396+
hash[name].merge!(sideload_hash)
375397
end
376398
end
377-
result
378399
end
379400

380401
# @api private

lib/jsonapi_compliable/util/relationship_payload.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ def payload_for(sideload, relationship_payload)
4747
type = relationship_payload[:meta][:jsonapi_type]
4848
sideload = sideload.polymorphic_child_for_type(type)
4949
end
50+
relationship_payload[:meta][:method] ||= :update
5051

5152
{
5253
sideload: sideload,

spec/sideload_spec.rb

Lines changed: 105 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -172,34 +172,118 @@ def foo
172172
end
173173

174174
describe '#to_hash' do
175-
# Set this up to catch any recursive trap
176-
let(:resource) do
177-
resource = JsonapiCompliable::Resource
178-
resource.allow_sideload :bing, resource: JsonapiCompliable::Resource
179-
resource
175+
before do
176+
stub_const('ResourceA', Class.new(JsonapiCompliable::Resource))
177+
stub_const('ResourceB', Class.new(JsonapiCompliable::Resource))
178+
stub_const('ResourceC', Class.new(JsonapiCompliable::Resource))
179+
stub_const('ResourceD', Class.new(JsonapiCompliable::Resource))
180+
stub_const('ResourceE', Class.new(JsonapiCompliable::Resource))
180181
end
181182

182-
before do
183-
resource = JsonapiCompliable::Resource
184-
instance.allow_sideload :bar, resource: resource do
185-
allow_sideload :baz, resource: resource do
186-
allow_sideload :bazoo, resource: resource
183+
around do |e|
184+
original = JsonapiCompliable::Sideload.max_depth
185+
JsonapiCompliable::Sideload.max_depth = 5
186+
e.run
187+
JsonapiCompliable::Sideload.max_depth = original
188+
end
189+
190+
subject { ResourceA.new.sideloading.to_hash[:base] }
191+
192+
context 'when simple' do
193+
before do
194+
ResourceA.allow_sideload :b, resource: ResourceB
195+
ResourceB.allow_sideload :c, resource: ResourceC
196+
ResourceC.allow_sideload :d, resource: ResourceD
197+
end
198+
199+
it 'returns all sideloads in a nested hash' do
200+
expect(subject).to eq(b: { c: { d: {} } })
201+
end
202+
end
203+
204+
context 'when recursive' do
205+
before do
206+
ResourceA.allow_sideload :b, resource: ResourceB
207+
ResourceB.allow_sideload :a, resource: ResourceA
208+
end
209+
210+
it 'allows 5 levels of recursion' do
211+
expect(subject).to eq({
212+
b: {
213+
a: { # one
214+
b: {
215+
a: { # two
216+
b: {
217+
a: { # three
218+
b: {
219+
a: { # four
220+
b: {
221+
a: { # five
222+
b: {}
223+
}
224+
}
225+
}
226+
}
227+
}
228+
}
229+
}
230+
}
231+
}
232+
}
233+
})
234+
end
235+
end
236+
237+
context 'when polymorphic' do
238+
before do
239+
ResourceA.allow_sideload :polly, polymorphic: true do
240+
allow_sideload 'WhenTypeB', resource: ResourceB
241+
allow_sideload 'WhenTypeC', resource: ResourceC
187242
end
243+
244+
ResourceB.allow_sideload :d, resource: ResourceD
245+
ResourceC.allow_sideload :e, resource: ResourceE
246+
end
247+
248+
it 'returns the correct nested hash' do
249+
expect(subject[:polly]).to eq({
250+
d: {},
251+
e: {}
252+
})
188253
end
189-
instance.allow_sideload :blah, resource: resource
190254
end
191255

192-
it 'recursively builds a hash of sideloads' do
193-
expect(instance.to_hash).to eq({
194-
foo: {
195-
bar: {
196-
baz: {
197-
bazoo: {}
256+
context 'when polymorphic AND recursive' do
257+
before do
258+
ResourceA.allow_sideload :polly, polymorphic: true do
259+
allow_sideload 'WhenTypeB', resource: ResourceB
260+
end
261+
ResourceB.allow_sideload :a, resource: ResourceA
262+
end
263+
264+
it 'allows 5 levels of recursion' do
265+
expect(subject[:polly]).to eq({
266+
a: { # one
267+
polly: {
268+
a: { # two
269+
polly: {
270+
a: { # three
271+
polly: {
272+
a: { # four
273+
polly: {
274+
a: { # five
275+
polly: {}
276+
}
277+
}
278+
}
279+
}
280+
}
281+
}
282+
}
198283
}
199-
},
200-
blah: {}
201-
}
202-
})
284+
}
285+
})
286+
end
203287
end
204288
end
205289
end

0 commit comments

Comments
 (0)