Skip to content

Commit fb35962

Browse files
authored
Merge pull request #1096 from cerebris/includes_config
Allow more control over allowing includes
2 parents b52ef84 + 617aedb commit fb35962

File tree

9 files changed

+295
-18
lines changed

9 files changed

+295
-18
lines changed

lib/jsonapi/configuration.rb

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ class Configuration
1010
:route_format,
1111
:raise_if_parameters_not_allowed,
1212
:warn_on_route_setup_issues,
13-
:allow_include,
13+
:default_allow_include_to_one,
14+
:default_allow_include_to_many,
1415
:allow_sort,
1516
:allow_filter,
1617
:default_paginator,
@@ -50,7 +51,8 @@ def initialize
5051
self.resource_key_type = :integer
5152

5253
# optional request features
53-
self.allow_include = true
54+
self.default_allow_include_to_one = true
55+
self.default_allow_include_to_many = true
5456
self.allow_sort = true
5557
self.allow_filter = true
5658

@@ -227,7 +229,13 @@ def resource_finder=(resource_finder)
227229
@resource_finder = resource_finder
228230
end
229231

230-
attr_writer :allow_include, :allow_sort, :allow_filter
232+
def allow_include=(allow_include)
233+
ActiveSupport::Deprecation.warn('`allow_include` has been replaced by `default_allow_include_to_one` and `default_allow_include_to_many` options.')
234+
@default_allow_include_to_one = allow_include
235+
@default_allow_include_to_many = allow_include
236+
end
237+
238+
attr_writer :allow_sort, :allow_filter, :default_allow_include_to_one, :default_allow_include_to_many
231239

232240
attr_writer :default_paginator
233241

lib/jsonapi/exceptions.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -342,7 +342,7 @@ def errors
342342
title: I18n.translate('jsonapi-resources.exceptions.invalid_include.title',
343343
default: 'Invalid field'),
344344
detail: I18n.translate('jsonapi-resources.exceptions.invalid_include.detail',
345-
default: "#{relationship} is not a valid relationship of #{resource}",
345+
default: "#{relationship} is not a valid includable relationship of #{resource}",
346346
relationship: relationship, resource: resource))]
347347
end
348348
end

lib/jsonapi/relationship.rb

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ class Relationship
33
attr_reader :acts_as_set, :foreign_key, :options, :name,
44
:class_name, :polymorphic, :always_include_linkage_data,
55
:parent_resource, :eager_load_on_include, :custom_methods,
6-
:inverse_relationship
6+
:inverse_relationship, :allow_include
7+
8+
attr_writer :allow_include
79

810
def initialize(name, options = {})
911
@name = name.to_s
@@ -17,6 +19,7 @@ def initialize(name, options = {})
1719
@polymorphic_relations = options[:polymorphic_relations]
1820
@always_include_linkage_data = options.fetch(:always_include_linkage_data, false) == true
1921
@eager_load_on_include = options.fetch(:eager_load_on_include, true) == true
22+
@allow_include = options[:allow_include]
2023
end
2124

2225
alias_method :polymorphic?, :polymorphic
@@ -100,6 +103,22 @@ def belongs_to?
100103
def polymorphic_type
101104
"#{name}_type" if polymorphic?
102105
end
106+
107+
def allow_include?(context = nil)
108+
strategy = if @allow_include.nil?
109+
JSONAPI.configuration.default_allow_include_to_one
110+
else
111+
@allow_include
112+
end
113+
114+
if !!strategy == strategy #check for boolean
115+
return strategy
116+
elsif strategy.is_a?(Symbol) || strategy.is_a?(String)
117+
parent_resource.send(strategy, context)
118+
else
119+
strategy.call(context)
120+
end
121+
end
103122
end
104123

105124
class ToMany < Relationship
@@ -114,6 +133,23 @@ def initialize(name, options = {})
114133
@inverse_relationship = options.fetch(:inverse_relationship, parent_resource._type.to_s.singularize.to_sym)
115134
end
116135
end
136+
137+
def allow_include?(context = nil)
138+
strategy = if @allow_include.nil?
139+
JSONAPI.configuration.default_allow_include_to_many
140+
else
141+
@allow_include
142+
end
143+
144+
if !!strategy == strategy #check for boolean
145+
return strategy
146+
elsif strategy.is_a?(Symbol) || strategy.is_a?(String)
147+
parent_resource.send(strategy, context)
148+
else
149+
strategy.call(context)
150+
end
151+
152+
end
117153
end
118154
end
119155
end

lib/jsonapi/request_parser.rb

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -330,22 +330,23 @@ def check_include(resource_klass, include_parts)
330330

331331
relationship = resource_klass._relationship(relationship_name)
332332
if relationship && format_key(relationship_name) == include_parts.first
333+
unless relationship.allow_include?(context)
334+
fail JSONAPI::Exceptions::InvalidInclude.new(format_key(resource_klass._type), include_parts.first)
335+
end
336+
333337
unless include_parts.last.empty?
334338
check_include(Resource.resource_klass_for(resource_klass.module_path + relationship.class_name.to_s.underscore),
335339
include_parts.last.partition('.'))
336340
end
337341
else
338342
fail JSONAPI::Exceptions::InvalidInclude.new(format_key(resource_klass._type), include_parts.first)
339343
end
344+
true
340345
end
341346

342347
def parse_include_directives(resource_klass, raw_include)
343348
return unless raw_include
344349

345-
unless JSONAPI.configuration.allow_include
346-
fail JSONAPI::Exceptions::ParameterNotAllowed.new(:include)
347-
end
348-
349350
included_resources = []
350351
begin
351352
included_resources += raw_include.is_a?(Array) ? raw_include : CSV.parse_line(raw_include) || []

locales/en.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ en:
4848
detail: "%{field} is not a valid field for %{type}."
4949
invalid_include:
5050
title: 'Invalid include'
51-
detail: "%{relationship} is not a valid relationship of %{resource}"
51+
detail: "%{relationship} is not a valid includable relationship of %{resource}"
5252
invalid_sort_criteria:
5353
title: 'Invalid sort criteria'
5454
detail: "%{sort_criteria} is not a valid sort criteria for %{resource}"

test/controllers/controller_test.rb

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -551,11 +551,12 @@ def test_show_single_with_includes
551551
end
552552

553553
def test_show_single_with_include_disallowed
554+
original_config = JSONAPI.configuration.dup
554555
JSONAPI.configuration.allow_include = false
555556
assert_cacheable_get :show, params: {id: '1', include: 'comments'}
556557
assert_response :bad_request
557558
ensure
558-
JSONAPI.configuration.allow_include = true
559+
JSONAPI.configuration = original_config
559560
end
560561

561562
def test_show_single_with_fields
@@ -2122,25 +2123,25 @@ def test_expense_entries_show_include
21222123
def test_expense_entries_show_bad_include_missing_relationship
21232124
assert_cacheable_get :show, params: {id: 1, include: 'isoCurrencies,employees'}
21242125
assert_response :bad_request
2125-
assert_match /isoCurrencies is not a valid relationship of expenseEntries/, json_response['errors'][0]['detail']
2126+
assert_match /isoCurrencies is not a valid includable relationship of expenseEntries/, json_response['errors'][0]['detail']
21262127
end
21272128

21282129
def test_expense_entries_show_bad_include_missing_sub_relationship
21292130
assert_cacheable_get :show, params: {id: 1, include: 'isoCurrency,employee.post'}
21302131
assert_response :bad_request
2131-
assert_match /post is not a valid relationship of employees/, json_response['errors'][0]['detail']
2132+
assert_match /post is not a valid includable relationship of employees/, json_response['errors'][0]['detail']
21322133
end
21332134

21342135
def test_invalid_include
21352136
assert_cacheable_get :index, params: {include: 'invalid../../../../'}
21362137
assert_response :bad_request
2137-
assert_match /invalid is not a valid relationship of expenseEntries/, json_response['errors'][0]['detail']
2138+
assert_match /invalid is not a valid includable relationship of expenseEntries/, json_response['errors'][0]['detail']
21382139
end
21392140

21402141
def test_invalid_include_long_garbage_string
21412142
assert_cacheable_get :index, params: {include: 'invalid.foo.bar.dfsdfs,dfsdfs.sdfwe.ewrerw.erwrewrew'}
21422143
assert_response :bad_request
2143-
assert_match /invalid is not a valid relationship of expenseEntries/, json_response['errors'][0]['detail']
2144+
assert_match /invalid is not a valid includable relationship of expenseEntries/, json_response['errors'][0]['detail']
21442145
end
21452146

21462147
def test_expense_entries_show_fields

test/integration/requests/request_test.rb

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1125,14 +1125,53 @@ def test_include_parameter_allowed
11251125
assert_cacheable_jsonapi_get '/api/v2/books/1/book_comments?include=author'
11261126
end
11271127

1128-
def test_include_parameter_not_allowed
1128+
def test_deprecated_include_parameter_not_allowed
1129+
original_config = JSONAPI.configuration.dup
11291130
JSONAPI.configuration.allow_include = false
11301131
get '/api/v2/books/1/book_comments?include=author', headers: {
11311132
'Accept' => JSONAPI::MEDIA_TYPE
11321133
}
11331134
assert_jsonapi_response 400
11341135
ensure
1135-
JSONAPI.configuration.allow_include = true
1136+
JSONAPI.configuration = original_config
1137+
end
1138+
1139+
def test_deprecated_include_message
1140+
ActiveSupport::Deprecation.silenced = false
1141+
original_config = JSONAPI.configuration.dup
1142+
_out, err = capture_io do
1143+
eval <<-CODE
1144+
JSONAPI.configuration.allow_include = false
1145+
CODE
1146+
end
1147+
assert_match /DEPRECATION WARNING: `allow_include` has been replaced by `default_allow_include_to_one` and `default_allow_include_to_many` options./, err
1148+
ensure
1149+
JSONAPI.configuration = original_config
1150+
ActiveSupport::Deprecation.silenced = true
1151+
end
1152+
1153+
1154+
def test_to_one_include_parameter_not_allowed
1155+
original_config = JSONAPI.configuration.dup
1156+
JSONAPI.configuration.default_allow_include_to_one = false
1157+
get '/api/v2/books/1/book_comments?include=author', headers: {
1158+
'Accept' => JSONAPI::MEDIA_TYPE
1159+
}
1160+
assert_jsonapi_response 400
1161+
ensure
1162+
JSONAPI.configuration = original_config
1163+
end
1164+
1165+
def test_to_one_include_parameter_allowed
1166+
original_config = JSONAPI.configuration.dup
1167+
JSONAPI.configuration.default_allow_include_to_one = true
1168+
get '/api/v2/books/1/book_comments?include=author', headers: {
1169+
'Accept' => JSONAPI::MEDIA_TYPE
1170+
}
1171+
assert_jsonapi_response 200
1172+
assert_equal 1, json_response['included'].size
1173+
ensure
1174+
JSONAPI.configuration = original_config
11361175
end
11371176

11381177
def test_filter_parameter_not_allowed

test/unit/jsonapi_request/jsonapi_request_test.rb

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,92 @@ def test_parse_blank_includes
4747
assert_empty include_directives.model_includes
4848
end
4949

50+
def test_check_include_allowed
51+
reset_includes
52+
assert JSONAPI::RequestParser.new.check_include(ExpenseEntryResource, "isoCurrency".partition('.'))
53+
ensure
54+
reset_includes
55+
end
56+
57+
def test_check_nested_include_allowed
58+
reset_includes
59+
assert JSONAPI::RequestParser.new.check_include(ExpenseEntryResource, "employee.expenseEntries".partition('.'))
60+
ensure
61+
reset_includes
62+
end
63+
64+
def test_check_include_relationship_does_not_exist
65+
reset_includes
66+
67+
assert_raises JSONAPI::Exceptions::InvalidInclude do
68+
assert JSONAPI::RequestParser.new.check_include(ExpenseEntryResource, "foo".partition('.'))
69+
end
70+
ensure
71+
reset_includes
72+
end
73+
74+
def test_check_nested_include_relationship_does_not_exist_wrong_format
75+
reset_includes
76+
77+
assert_raises JSONAPI::Exceptions::InvalidInclude do
78+
assert JSONAPI::RequestParser.new.check_include(ExpenseEntryResource, "employee.expense-entries".partition('.'))
79+
end
80+
ensure
81+
reset_includes
82+
end
83+
84+
def test_check_include_has_one_not_allowed_default
85+
reset_includes
86+
87+
assert JSONAPI::RequestParser.new.check_include(ExpenseEntryResource, "isoCurrency".partition('.'))
88+
JSONAPI.configuration.default_allow_include_to_one = false
89+
90+
assert_raises JSONAPI::Exceptions::InvalidInclude do
91+
JSONAPI::RequestParser.new.check_include(ExpenseEntryResource, "isoCurrency".partition('.'))
92+
end
93+
ensure
94+
reset_includes
95+
end
96+
97+
def test_check_include_has_one_not_allowed_resource
98+
reset_includes
99+
100+
assert JSONAPI::RequestParser.new.check_include(ExpenseEntryResource, "isoCurrency".partition('.'))
101+
ExpenseEntryResource._relationship(:iso_currency).allow_include = false
102+
103+
assert_raises JSONAPI::Exceptions::InvalidInclude do
104+
JSONAPI::RequestParser.new.check_include(ExpenseEntryResource, "isoCurrency".partition('.'))
105+
end
106+
ensure
107+
reset_includes
108+
end
109+
110+
def test_check_include_has_many_not_allowed_default
111+
reset_includes
112+
113+
assert JSONAPI::RequestParser.new.check_include(EmployeeResource, "expenseEntries".partition('.'))
114+
JSONAPI.configuration.default_allow_include_to_many = false
115+
116+
assert_raises JSONAPI::Exceptions::InvalidInclude do
117+
JSONAPI::RequestParser.new.check_include(EmployeeResource, "expenseEntries".partition('.'))
118+
end
119+
ensure
120+
reset_includes
121+
end
122+
123+
def test_check_include_has_many_not_allowed_resource
124+
reset_includes
125+
126+
assert JSONAPI::RequestParser.new.check_include(EmployeeResource, "expenseEntries".partition('.'))
127+
EmployeeResource._relationship(:expense_entries).allow_include = false
128+
129+
assert_raises JSONAPI::Exceptions::InvalidInclude do
130+
JSONAPI::RequestParser.new.check_include(EmployeeResource, "expenseEntries".partition('.'))
131+
end
132+
ensure
133+
reset_includes
134+
end
135+
50136
def test_parse_dasherized_with_dasherized_include
51137
params = ActionController::Parameters.new(
52138
{
@@ -87,7 +173,7 @@ def test_parse_dasherized_with_underscored_include
87173

88174
request.parse_include_directives(ExpenseEntryResource, params[:include])
89175
refute request.errors.empty?
90-
assert_equal 'iso_currency is not a valid relationship of expense-entries', request.errors[0].detail
176+
assert_equal 'iso_currency is not a valid includable relationship of expense-entries', request.errors[0].detail
91177
end
92178

93179
def test_parse_fields_underscored
@@ -242,4 +328,12 @@ def test_parse_sort_with_relationships
242328
def setup_request
243329
@request = JSONAPI::RequestParser.new
244330
end
331+
332+
def reset_includes
333+
JSONAPI.configuration.json_key_format = :camelized_key
334+
JSONAPI.configuration.default_allow_include_to_one = true
335+
JSONAPI.configuration.default_allow_include_to_many = true
336+
ExpenseEntryResource._relationship(:iso_currency).allow_include = nil
337+
EmployeeResource._relationship(:expense_entries).allow_include = nil
338+
end
245339
end

0 commit comments

Comments
 (0)