Skip to content

Commit a7872b9

Browse files
author
Lee Richmond
committed
Ensure nested sideloads work, add multisort
1 parent 88425bb commit a7872b9

File tree

8 files changed

+119
-32
lines changed

8 files changed

+119
-32
lines changed

lib/jsonapi_compliable/base.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ def render_jsonapi(scope, opts = {})
4848
resolved = scoped.respond_to?(:resolve) ? scoped.resolve : scoped
4949

5050
options = default_jsonapi_render_options
51-
options[:include] = forced_includes || Util::IncludeParams.scrub(query_hash[:include], resource.allowed_sideloads)
51+
options[:include] = forced_includes || query_hash[:include]
5252
options[:jsonapi] = resolved
5353
options[:fields] = query_hash[:fields]
5454
options[:meta] ||= {}

lib/jsonapi_compliable/query.rb

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,23 @@ def initialize(resource, params)
2121
@params = params
2222
end
2323

24+
def include_directive
25+
@include_directive ||= JSONAPI::IncludeDirective.new(params[:include])
26+
end
27+
28+
def include_hash
29+
@include_hash ||= include_directive.to_hash
30+
end
31+
32+
def all_requested_association_names
33+
@all_requested_association_names ||= Util::Hash.keys(include_hash)
34+
end
35+
2436
def to_hash
2537
hash = { resource.type => self.class.default_hash }
26-
resource.association_names.each do |name|
27-
hash[name] = self.class.default_hash.except(:include)
38+
39+
all_requested_association_names.each do |name|
40+
hash[name] = self.class.default_hash
2841
end
2942

3043
fields = parse_fields({}, :fields)
@@ -37,7 +50,7 @@ def to_hash
3750
parse_filter(hash)
3851
parse_sort(hash)
3952
parse_pagination(hash)
40-
parse_include(hash)
53+
parse_include(hash, include_hash)
4154
parse_stats(hash)
4255

4356
hash
@@ -55,8 +68,13 @@ def association?(name)
5568
resource.association_names.include?(name)
5669
end
5770

58-
def parse_include(hash)
59-
hash[resource.type][:include] = JSONAPI::IncludeDirective.new(params[:include] || {}).to_hash
71+
def parse_include(memo, incl_hash, namespace = nil)
72+
namespace ||= resource.type
73+
74+
memo[namespace][:include] = incl_hash
75+
incl_hash.each_pair do |key, sub_hash|
76+
memo[key][:include] = parse_include(memo, sub_hash, key)
77+
end
6078
end
6179

6280
def parse_stats(hash)

lib/jsonapi_compliable/scoping/sort.rb

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,29 @@
1-
# TODO: multisort
2-
31
module JsonapiCompliable
42
class Scoping::Sort < Scoping::Base
53
def custom_scope
64
resource.sorting
75
end
86

97
def apply_standard_scope
10-
resource.adapter.order(@scope, attribute, direction)
8+
each_sort do |attribute, direction|
9+
@scope = resource.adapter.order(@scope, attribute, direction)
10+
end
11+
@scope
1112
end
1213

1314
def apply_custom_scope
14-
custom_scope.call(@scope, attribute, direction)
15+
each_sort do |attribute, direction|
16+
@scope = custom_scope.call(@scope, attribute, direction)
17+
end
18+
@scope
1519
end
1620

1721
private
1822

19-
def attribute
20-
sort_param[0].keys.first
21-
end
22-
23-
def direction
24-
sort_param[0].values.first
23+
def each_sort
24+
sort_param.each do |sort_hash|
25+
yield sort_hash.keys.first, sort_hash.values.first
26+
end
2527
end
2628

2729
def sort_param

lib/jsonapi_compliable/sideload.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,17 @@ def sideload(name)
5959
@sideloads[name]
6060
end
6161

62+
# Grab from nested sideloads, AND resource, recursively
6263
def to_hash
6364
{ name => {} }.tap do |hash|
6465
@sideloads.each_pair do |key, sideload|
6566
hash[name][key] = sideload.to_hash[key]
67+
68+
if sideloading = sideload.resource.sideloading
69+
sideloading.sideloads.each_pair do |k, s|
70+
hash[name][k] = s.to_hash[k]
71+
end
72+
end
6673
end
6774
end
6875
end

spec/integration/resources_spec.rb renamed to spec/integration/active_record_spec.rb

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,20 @@
22

33
RSpec.describe 'integrated resources and adapters', type: :controller do
44
module Integration
5+
class GenreResource < JsonapiCompliable::Resource
6+
type :genres
7+
use_adapter JsonapiCompliable::Adapters::ActiveRecord
8+
end
9+
510
class BookResource < JsonapiCompliable::Resource
611
type :books
712
use_adapter JsonapiCompliable::Adapters::ActiveRecord
813
allow_filter :id
14+
15+
belongs_to :genre,
16+
scope: -> { Genre.all },
17+
foreign_key: :genre_id,
18+
resource: GenreResource
919
end
1020

1121
class DwellingResource < JsonapiCompliable::Resource
@@ -82,14 +92,15 @@ def index
8292

8393
let!(:author1) { Author.create!(first_name: 'Stephen', dwelling: house, state: state) }
8494
let!(:author2) { Author.create!(first_name: 'George', dwelling: condo) }
85-
let!(:book1) { Book.create!(author: author1, title: 'The Shining') }
86-
let!(:book2) { Book.create!(author: author1, title: 'The Stand') }
95+
let!(:book1) { Book.create!(author: author1, genre: genre, title: 'The Shining') }
96+
let!(:book2) { Book.create!(author: author1, genre: genre, title: 'The Stand') }
8797
let!(:state) { State.create!(name: 'Maine') }
8898
let!(:bio) { Bio.create!(author: author1, picture: 'imgur', description: 'author bio') }
8999
let!(:hobby1) { Hobby.create!(name: 'Fishing', authors: [author1]) }
90100
let!(:hobby2) { Hobby.create!(name: 'Woodworking', authors: [author1]) }
91101
let(:house) { House.new(name: 'Cozy') }
92102
let(:condo) { Condo.new(name: 'Modern') }
103+
let(:genre) { Genre.create!(name: 'Horror') }
93104

94105
def ids_for(type)
95106
json_includes(type).map { |b| b['id'].to_i }
@@ -115,6 +126,11 @@ def ids_for(type)
115126
expect(json_included_types).to match_array(%w(books))
116127
end
117128

129+
it 'allows nested sideloading' do
130+
get :index, params: { include: 'books.genre' }
131+
expect(json_included_types).to match_array(%w(books genres))
132+
end
133+
118134
context 'sideloading has_many' do
119135
# TODO: may want to blow up here, only for index action
120136
it 'allows pagination of sideloaded resource' do

spec/query_spec.rb

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
RSpec.describe JsonapiCompliable::Query do
44
let(:resource) { double(type: :authors, association_names: [:books]).as_null_object }
5-
let(:params) { {} }
5+
let(:params) { { include: 'books' } }
66
let(:instance) { described_class.new(resource, params) }
77

88
describe '#to_hash' do
@@ -13,10 +13,20 @@
1313
expect(subject[:authors][:filter]).to eq({})
1414
end
1515

16-
it 'defaults associations' do
16+
it 'does not default associations' do
1717
expect(subject[:books][:filter]).to eq({})
1818
end
1919

20+
context 'when association is not requested' do
21+
before do
22+
params.delete(:include)
23+
end
24+
25+
it 'does not default the association query' do
26+
expect(subject).to_not have_key(:books)
27+
end
28+
end
29+
2030
context 'when filter param present' do
2131
before do
2232
params[:filter] = { id: 1, books: { title: 'foo' } }
@@ -156,24 +166,32 @@
156166
end
157167

158168
describe 'include' do
159-
it 'defaults main entity' do
160-
expect(subject[:authors][:include]).to eq({})
169+
it 'sets main entity' do
170+
expect(subject[:authors][:include]).to eq(books: {})
161171
end
162172

163-
it 'does NOT default associations' do
164-
expect(subject[:books]).to_not have_key(:include)
173+
it 'sets associations' do
174+
expect(subject[:books][:include]).to eq({})
165175
end
166176

167177
context 'when include param present' do
168178
before do
169-
params[:include] = 'books.genre,state'
179+
params[:include] = 'books.genre.owner,state'
170180
end
171181

172182
it 'transforms to hash' do
173183
expect(subject[:authors][:include]).to eq({
174-
books: { genre: {} },
184+
books: { genre: { owner: {} } },
175185
state: {}
176186
})
187+
expect(subject[:books][:include]).to eq({
188+
genre: { owner: {} }
189+
})
190+
expect(subject[:genre][:include]).to eq({
191+
owner: {}
192+
})
193+
expect(subject[:owner][:include]).to eq({})
194+
expect(subject[:state][:include]).to eq({})
177195
end
178196
end
179197
end

spec/resource_spec.rb

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -128,12 +128,18 @@
128128
end
129129

130130
describe '#association_names' do
131-
it 'collects all sideloads' do
131+
it 'collects nested + resource sideloads' do
132132
klass.allow_sideload :books do
133-
allow_sideload :genre
133+
genre_resource = Class.new(JsonapiCompliable::Resource) do
134+
allow_sideload :from_genre_resource do
135+
allow_sideload :nested_from_genre_resource
136+
end
137+
end
138+
allow_sideload :genre, resource: genre_resource
134139
end
135-
klass.allow_sideload :state
136-
expect(instance.association_names).to eq([:books, :genre, :state])
140+
klass.allow_sideload :state, polymorphic: true
141+
expect(instance.association_names)
142+
.to match_array([:books, :genre, :state, :from_genre_resource, :nested_from_genre_resource])
137143
end
138144

139145
context 'when no whitelist' do

spec/sorting_spec.rb

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ def index
1212
end
1313

1414
before do
15-
Author.create!(first_name: 'Stephen')
16-
Author.create!(first_name: 'Philip')
15+
Author.create!(first_name: 'Stephen', last_name: 'King')
16+
Author.create!(first_name: 'Philip', last_name: 'Dick')
1717
end
1818

1919
it 'defaults sort to resource default_sort' do
@@ -43,6 +43,26 @@ def index
4343
it { is_expected.to eq(%w(Stephen Philip)) }
4444
end
4545

46+
context 'when prefixed with type' do
47+
let(:sort_param) { 'authors.first_name' }
48+
49+
it { is_expected.to eq(%w(Philip Stephen)) }
50+
end
51+
52+
context 'when passed multisort' do
53+
let(:sort_param) { 'first_name,last_name' }
54+
55+
before do
56+
Author.create(first_name: 'Stephen', last_name: 'Adams')
57+
end
58+
59+
it 'sorts correctly' do
60+
get :index, params: { sort: sort_param }
61+
last_names = json_items.map { |n| n['last_name'] }
62+
expect(last_names).to eq(%w(Dick Adams King))
63+
end
64+
end
65+
4666
context 'when given a custom sort function' do
4767
let(:sort_param) { 'first_name' }
4868

0 commit comments

Comments
 (0)