jsonapi-resources 0.9.0 → 0.10.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +5 -5
  2. data/LICENSE.txt +1 -1
  3. data/README.md +34 -11
  4. data/lib/bug_report_templates/rails_5_latest.rb +125 -0
  5. data/lib/bug_report_templates/rails_5_master.rb +140 -0
  6. data/lib/jsonapi/active_relation/adapters/join_left_active_record_adapter.rb +27 -0
  7. data/lib/jsonapi/active_relation/join_manager.rb +303 -0
  8. data/lib/jsonapi/active_relation_resource.rb +884 -0
  9. data/lib/jsonapi/acts_as_resource_controller.rb +122 -105
  10. data/lib/jsonapi/basic_resource.rb +1162 -0
  11. data/lib/jsonapi/cached_response_fragment.rb +127 -0
  12. data/lib/jsonapi/compiled_json.rb +11 -1
  13. data/lib/jsonapi/configuration.rb +71 -8
  14. data/lib/jsonapi/error.rb +27 -0
  15. data/lib/jsonapi/error_codes.rb +2 -0
  16. data/lib/jsonapi/exceptions.rb +80 -50
  17. data/lib/jsonapi/formatter.rb +3 -3
  18. data/lib/jsonapi/include_directives.rb +18 -65
  19. data/lib/jsonapi/link_builder.rb +74 -80
  20. data/lib/jsonapi/operation.rb +16 -5
  21. data/lib/jsonapi/operation_result.rb +74 -16
  22. data/lib/jsonapi/path.rb +43 -0
  23. data/lib/jsonapi/path_segment.rb +76 -0
  24. data/lib/jsonapi/processor.rb +239 -111
  25. data/lib/jsonapi/relationship.rb +153 -15
  26. data/lib/jsonapi/request_parser.rb +430 -367
  27. data/lib/jsonapi/resource.rb +3 -1253
  28. data/lib/jsonapi/resource_controller_metal.rb +5 -2
  29. data/lib/jsonapi/resource_fragment.rb +47 -0
  30. data/lib/jsonapi/resource_id_tree.rb +112 -0
  31. data/lib/jsonapi/resource_identity.rb +42 -0
  32. data/lib/jsonapi/resource_serializer.rb +143 -285
  33. data/lib/jsonapi/resource_set.rb +176 -0
  34. data/lib/jsonapi/resources/railtie.rb +9 -0
  35. data/lib/jsonapi/resources/version.rb +1 -1
  36. data/lib/jsonapi/response_document.rb +105 -83
  37. data/lib/jsonapi/routing_ext.rb +48 -26
  38. data/lib/jsonapi-resources.rb +20 -4
  39. data/lib/tasks/check_upgrade.rake +52 -0
  40. metadata +50 -20
  41. data/lib/jsonapi/cached_resource_fragment.rb +0 -127
  42. data/lib/jsonapi/operation_dispatcher.rb +0 -88
  43. data/lib/jsonapi/operation_results.rb +0 -35
  44. data/lib/jsonapi/relationship_builder.rb +0 -167
@@ -1,121 +1,261 @@
1
- require 'jsonapi/operation'
2
- require 'jsonapi/paginator'
3
-
4
1
  module JSONAPI
5
2
  class RequestParser
6
- attr_accessor :fields, :include, :filters, :sort_criteria, :errors, :operations,
7
- :resource_klass, :context, :paginator, :source_klass, :source_id,
3
+ attr_accessor :fields, :include, :filters, :sort_criteria, :errors, :controller_module_path,
4
+ :context, :paginator, :source_klass, :source_id,
8
5
  :include_directives, :params, :warnings, :server_error_callbacks
9
6
 
10
7
  def initialize(params = nil, options = {})
11
8
  @params = params
9
+ if params
10
+ controller_path = params.fetch(:controller, '')
11
+ @controller_module_path = controller_path.include?('/') ? controller_path.rpartition('/').first + '/' : ''
12
+ else
13
+ @controller_module_path = ''
14
+ end
15
+
12
16
  @context = options[:context]
13
17
  @key_formatter = options.fetch(:key_formatter, JSONAPI.configuration.key_formatter)
14
18
  @errors = []
15
19
  @warnings = []
16
- @operations = []
17
- @fields = {}
18
- @filters = {}
19
- @sort_criteria = nil
20
- @source_klass = nil
21
- @source_id = nil
22
- @include_directives = nil
23
- @paginator = nil
24
- @id = nil
25
20
  @server_error_callbacks = options.fetch(:server_error_callbacks, [])
21
+ end
26
22
 
27
- setup_action(@params)
23
+ def error_object_overrides
24
+ {}
28
25
  end
29
26
 
30
- def setup_action(params)
27
+ def each(_response_document)
28
+ operation = setup_base_op(params)
29
+ if @errors.any?
30
+ fail JSONAPI::Exceptions::Errors.new(@errors)
31
+ else
32
+ yield operation
33
+ end
34
+ rescue ActionController::ParameterMissing => e
35
+ fail JSONAPI::Exceptions::ParameterMissing.new(e.param, error_object_overrides)
36
+ end
37
+
38
+ def transactional?
39
+ case params[:action]
40
+ when 'index', 'show_related_resource', 'index_related_resources', 'show', 'show_relationship'
41
+ return false
42
+ else
43
+ return true
44
+ end
45
+ end
46
+
47
+ def setup_base_op(params)
31
48
  return if params.nil?
32
49
 
33
- @resource_klass ||= Resource.resource_for(params[:controller]) if params[:controller]
50
+ resource_klass = Resource.resource_klass_for(params[:controller]) if params[:controller]
34
51
 
35
52
  setup_action_method_name = "setup_#{params[:action]}_action"
36
53
  if respond_to?(setup_action_method_name)
37
54
  raise params[:_parser_exception] if params[:_parser_exception]
38
- send(setup_action_method_name, params)
55
+ send(setup_action_method_name, params, resource_klass)
39
56
  end
40
57
  rescue ActionController::ParameterMissing => e
41
- @errors.concat(JSONAPI::Exceptions::ParameterMissing.new(e.param).errors)
58
+ @errors.concat(JSONAPI::Exceptions::ParameterMissing.new(e.param, error_object_overrides).errors)
59
+ rescue JSONAPI::Exceptions::Error => e
60
+ e.error_object_overrides.merge! error_object_overrides
61
+ @errors.concat(e.errors)
42
62
  end
43
63
 
44
- def setup_index_action(params)
45
- parse_fields(params[:fields])
46
- parse_include_directives(params[:include])
47
- set_default_filters
48
- parse_filters(params[:filter])
49
- parse_sort_criteria(params[:sort])
50
- parse_pagination(params[:page])
51
- add_find_operation
64
+ def setup_index_action(params, resource_klass)
65
+ fields = parse_fields(resource_klass, params[:fields])
66
+ include_directives = parse_include_directives(resource_klass, params[:include])
67
+ filters = parse_filters(resource_klass, params[:filter])
68
+ sort_criteria = parse_sort_criteria(resource_klass, params[:sort])
69
+ paginator = parse_pagination(resource_klass, params[:page])
70
+
71
+ JSONAPI::Operation.new(
72
+ :find,
73
+ resource_klass,
74
+ context: context,
75
+ filters: filters,
76
+ include_directives: include_directives,
77
+ sort_criteria: sort_criteria,
78
+ paginator: paginator,
79
+ fields: fields
80
+ )
52
81
  end
53
82
 
54
- def setup_get_related_resource_action(params)
55
- initialize_source(params)
56
- parse_fields(params[:fields])
57
- parse_include_directives(params[:include])
58
- set_default_filters
59
- parse_filters(params[:filter])
60
- parse_sort_criteria(params[:sort])
61
- parse_pagination(params[:page])
62
- add_show_related_resource_operation(params[:relationship])
83
+ def setup_show_related_resource_action(params, resource_klass)
84
+ resolve_singleton_id(params, resource_klass)
85
+ source_klass = Resource.resource_klass_for(params.require(:source))
86
+ source_id = source_klass.verify_key(params.require(source_klass._as_parent_key), @context)
87
+
88
+ fields = parse_fields(resource_klass, params[:fields])
89
+ include_directives = parse_include_directives(resource_klass, params[:include])
90
+
91
+ relationship_type = params[:relationship].to_sym
92
+
93
+ JSONAPI::Operation.new(
94
+ :show_related_resource,
95
+ resource_klass,
96
+ context: @context,
97
+ relationship_type: relationship_type,
98
+ source_klass: source_klass,
99
+ source_id: source_id,
100
+ fields: fields,
101
+ include_directives: include_directives
102
+ )
63
103
  end
64
104
 
65
- def setup_get_related_resources_action(params)
66
- initialize_source(params)
67
- parse_fields(params[:fields])
68
- parse_include_directives(params[:include])
69
- set_default_filters
70
- parse_filters(params[:filter])
71
- parse_sort_criteria(params[:sort])
72
- parse_pagination(params[:page])
73
- add_show_related_resources_operation(params[:relationship])
105
+ def setup_index_related_resources_action(params, resource_klass)
106
+ resolve_singleton_id(params, resource_klass)
107
+ source_klass = Resource.resource_klass_for(params.require(:source))
108
+ source_id = source_klass.verify_key(params.require(source_klass._as_parent_key), @context)
109
+
110
+ fields = parse_fields(resource_klass, params[:fields])
111
+ include_directives = parse_include_directives(resource_klass, params[:include])
112
+ filters = parse_filters(resource_klass, params[:filter])
113
+ sort_criteria = parse_sort_criteria(resource_klass, params[:sort])
114
+ paginator = parse_pagination(resource_klass, params[:page])
115
+ relationship_type = params[:relationship]
116
+
117
+ JSONAPI::Operation.new(
118
+ :show_related_resources,
119
+ resource_klass,
120
+ context: @context,
121
+ relationship_type: relationship_type,
122
+ source_klass: source_klass,
123
+ source_id: source_id,
124
+ filters: filters,
125
+ sort_criteria: sort_criteria,
126
+ paginator: paginator,
127
+ fields: fields,
128
+ include_directives: include_directives
129
+ )
74
130
  end
75
131
 
76
- def setup_show_action(params)
77
- parse_fields(params[:fields])
78
- parse_include_directives(params[:include])
79
- @id = params[:id]
80
- add_show_operation
132
+ def setup_show_action(params, resource_klass)
133
+ resolve_singleton_id(params, resource_klass)
134
+ fields = parse_fields(resource_klass, params[:fields])
135
+ include_directives = parse_include_directives(resource_klass, params[:include])
136
+ id = params[:id]
137
+
138
+ JSONAPI::Operation.new(
139
+ :show,
140
+ resource_klass,
141
+ context: @context,
142
+ id: id,
143
+ include_directives: include_directives,
144
+ fields: fields,
145
+ allowed_resources: params[:allowed_resources]
146
+ )
81
147
  end
82
148
 
83
- def setup_show_relationship_action(params)
84
- add_show_relationship_operation(params[:relationship], params.require(@resource_klass._as_parent_key))
149
+ def setup_show_relationship_action(params, resource_klass)
150
+ resolve_singleton_id(params, resource_klass)
151
+ relationship_type = params[:relationship]
152
+ parent_key = params.require(resource_klass._as_parent_key)
153
+ include_directives = parse_include_directives(resource_klass, params[:include])
154
+ filters = parse_filters(resource_klass, params[:filter])
155
+ sort_criteria = parse_sort_criteria(resource_klass, params[:sort])
156
+ paginator = parse_pagination(resource_klass, params[:page])
157
+
158
+ JSONAPI::Operation.new(
159
+ :show_relationship,
160
+ resource_klass,
161
+ context: @context,
162
+ relationship_type: relationship_type,
163
+ parent_key: resource_klass.verify_key(parent_key),
164
+ filters: filters,
165
+ sort_criteria: sort_criteria,
166
+ paginator: paginator,
167
+ fields: fields,
168
+ include_directives: include_directives
169
+ )
85
170
  end
86
171
 
87
- def setup_create_action(params)
88
- parse_fields(params[:fields])
89
- parse_include_directives(params[:include])
90
- parse_add_operation(params.require(:data))
172
+ def setup_create_action(params, resource_klass)
173
+ fields = parse_fields(resource_klass, params[:fields])
174
+ include_directives = parse_include_directives(resource_klass, params[:include])
175
+
176
+ data = params.require(:data)
177
+
178
+ unless data.respond_to?(:each_pair)
179
+ fail JSONAPI::Exceptions::InvalidDataFormat.new(error_object_overrides)
180
+ end
181
+
182
+ verify_type(data[:type], resource_klass)
183
+
184
+ data = parse_params(resource_klass, data, resource_klass.creatable_fields(@context))
185
+
186
+ JSONAPI::Operation.new(
187
+ :create_resource,
188
+ resource_klass,
189
+ context: @context,
190
+ data: data,
191
+ fields: fields,
192
+ include_directives: include_directives,
193
+ warnings: @warnings
194
+ )
91
195
  end
92
196
 
93
- def setup_create_relationship_action(params)
94
- parse_modify_relationship_action(params, :add)
197
+ def setup_create_relationship_action(params, resource_klass)
198
+ resolve_singleton_id(params, resource_klass)
199
+ parse_modify_relationship_action(:add, params, resource_klass)
95
200
  end
96
201
 
97
- def setup_update_relationship_action(params)
98
- parse_modify_relationship_action(params, :update)
202
+ def setup_update_relationship_action(params, resource_klass)
203
+ parse_modify_relationship_action(:update, params, resource_klass)
99
204
  end
100
205
 
101
- def setup_update_action(params)
102
- parse_fields(params[:fields])
103
- parse_include_directives(params[:include])
104
- parse_replace_operation(params.require(:data), params[:id])
206
+ def setup_update_action(params, resource_klass)
207
+ resolve_singleton_id(params, resource_klass)
208
+ fields = parse_fields(resource_klass, params[:fields])
209
+ include_directives = parse_include_directives(resource_klass, params[:include])
210
+
211
+ data = params.require(:data)
212
+ key = params[:id]
213
+
214
+ fail JSONAPI::Exceptions::InvalidDataFormat.new(error_object_overrides) unless data.respond_to?(:each_pair)
215
+
216
+ fail JSONAPI::Exceptions::MissingKey.new(error_object_overrides) if data[:id].nil?
217
+
218
+ resource_id = data.require(:id)
219
+ # Singleton resources may not have the ID set in the URL
220
+ if key
221
+ fail JSONAPI::Exceptions::KeyNotIncludedInURL.new(resource_id) if key.to_s != resource_id.to_s
222
+ end
223
+
224
+ data.delete(:id)
225
+
226
+ verify_type(data[:type], resource_klass)
227
+
228
+ JSONAPI::Operation.new(
229
+ :replace_fields,
230
+ resource_klass,
231
+ context: @context,
232
+ resource_id: resource_id,
233
+ data: parse_params(resource_klass, data, resource_klass.updatable_fields(@context)),
234
+ fields: fields,
235
+ include_directives: include_directives,
236
+ warnings: @warnings
237
+ )
105
238
  end
106
239
 
107
- def setup_destroy_action(params)
108
- parse_remove_operation(params)
240
+ def setup_destroy_action(params, resource_klass)
241
+ resolve_singleton_id(params, resource_klass)
242
+ JSONAPI::Operation.new(
243
+ :remove_resource,
244
+ resource_klass,
245
+ context: @context,
246
+ resource_id: resource_klass.verify_key(params.require(:id), @context))
109
247
  end
110
248
 
111
- def setup_destroy_relationship_action(params)
112
- parse_modify_relationship_action(params, :remove)
249
+ def setup_destroy_relationship_action(params, resource_klass)
250
+ resolve_singleton_id(params, resource_klass)
251
+ parse_modify_relationship_action(:remove, params, resource_klass)
113
252
  end
114
253
 
115
- def parse_modify_relationship_action(params, modification_type)
254
+ def parse_modify_relationship_action(modification_type, params, resource_klass)
116
255
  relationship_type = params.require(:relationship)
117
- parent_key = params.require(@resource_klass._as_parent_key)
118
- relationship = @resource_klass._relationship(relationship_type)
256
+
257
+ parent_key = params.require(resource_klass._as_parent_key)
258
+ relationship = resource_klass._relationship(relationship_type)
119
259
 
120
260
  # Removals of to-one relationships are done implicitly and require no specification of data
121
261
  data_required = !(modification_type == :remove && relationship.is_a?(JSONAPI::Relationship::ToOne))
@@ -123,77 +263,74 @@ module JSONAPI
123
263
  if data_required
124
264
  data = params.fetch(:data)
125
265
  object_params = { relationships: { format_key(relationship.name) => { data: data } } }
126
- verified_params = parse_params(object_params, @resource_klass.updatable_fields(@context))
127
266
 
128
- parse_arguments = [verified_params, relationship, parent_key]
267
+ verified_params = parse_params(resource_klass, object_params, resource_klass.updatable_fields(@context))
268
+
269
+ parse_arguments = [resource_klass, verified_params, relationship, parent_key]
129
270
  else
130
- parse_arguments = [params, relationship, parent_key]
271
+ parse_arguments = [resource_klass, params, relationship, parent_key]
131
272
  end
132
273
 
133
274
  send(:"parse_#{modification_type}_relationship_operation", *parse_arguments)
134
275
  end
135
276
 
136
- def initialize_source(params)
137
- @source_klass = Resource.resource_for(params.require(:source))
138
- @source_id = @source_klass.verify_key(params.require(@source_klass._as_parent_key), @context)
277
+ def parse_pagination(resource_klass, page)
278
+ paginator_name = resource_klass._paginator
279
+ JSONAPI::Paginator.paginator_for(paginator_name).new(page) unless paginator_name == :none
139
280
  end
140
281
 
141
- def parse_pagination(page)
142
- paginator_name = @resource_klass._paginator
143
- @paginator = JSONAPI::Paginator.paginator_for(paginator_name).new(page) unless paginator_name == :none
144
- rescue JSONAPI::Exceptions::Error => e
145
- @errors.concat(e.errors)
146
- end
147
-
148
- def parse_fields(fields)
149
- return if fields.nil?
150
-
282
+ def parse_fields(resource_klass, fields)
151
283
  extracted_fields = {}
152
284
 
285
+ return extracted_fields if fields.nil?
286
+
153
287
  # Extract the fields for each type from the fields parameters
154
288
  if fields.is_a?(ActionController::Parameters)
155
289
  fields.each do |field, value|
156
- resource_fields = value.split(',') unless value.nil? || value.empty?
290
+ if value.is_a?(Array)
291
+ resource_fields = value
292
+ else
293
+ resource_fields = value.split(',') unless value.nil? || value.empty?
294
+ end
157
295
  extracted_fields[field] = resource_fields
158
296
  end
159
297
  else
160
- fail JSONAPI::Exceptions::InvalidFieldFormat.new
298
+ fail JSONAPI::Exceptions::InvalidFieldFormat.new(error_object_overrides)
161
299
  end
162
300
 
163
301
  # Validate the fields
302
+ validated_fields = {}
164
303
  extracted_fields.each do |type, values|
165
304
  underscored_type = unformat_key(type)
166
- extracted_fields[type] = []
305
+ validated_fields[type] = []
167
306
  begin
168
307
  if type != format_key(type)
169
- fail JSONAPI::Exceptions::InvalidResource.new(type)
308
+ fail JSONAPI::Exceptions::InvalidResource.new(type, error_object_overrides)
170
309
  end
171
- type_resource = Resource.resource_for(@resource_klass.module_path + underscored_type.to_s)
310
+ type_resource = Resource.resource_klass_for(resource_klass.module_path + underscored_type.to_s)
172
311
  rescue NameError
173
- @errors.concat(JSONAPI::Exceptions::InvalidResource.new(type).errors)
174
- rescue JSONAPI::Exceptions::InvalidResource => e
175
- @errors.concat(e.errors)
312
+ fail JSONAPI::Exceptions::InvalidResource.new(type, error_object_overrides)
176
313
  end
177
314
 
178
315
  if type_resource.nil?
179
- @errors.concat(JSONAPI::Exceptions::InvalidResource.new(type).errors)
316
+ fail JSONAPI::Exceptions::InvalidResource.new(type, error_object_overrides)
180
317
  else
181
318
  unless values.nil?
182
319
  valid_fields = type_resource.fields.collect { |key| format_key(key) }
183
320
  values.each do |field|
184
321
  if valid_fields.include?(field)
185
- extracted_fields[type].push unformat_key(field)
322
+ validated_fields[type].push unformat_key(field)
186
323
  else
187
- @errors.concat(JSONAPI::Exceptions::InvalidField.new(type, field).errors)
324
+ fail JSONAPI::Exceptions::InvalidField.new(type, field, error_object_overrides)
188
325
  end
189
326
  end
190
327
  else
191
- @errors.concat(JSONAPI::Exceptions::InvalidField.new(type, 'nil').errors)
328
+ fail JSONAPI::Exceptions::InvalidField.new(type, 'nil', error_object_overrides)
192
329
  end
193
330
  end
194
331
  end
195
332
 
196
- @fields = extracted_fields.deep_transform_keys { |key| unformat_key(key) }
333
+ validated_fields.deep_transform_keys { |key| unformat_key(key) }
197
334
  end
198
335
 
199
336
  def check_include(resource_klass, include_parts)
@@ -201,211 +338,144 @@ module JSONAPI
201
338
 
202
339
  relationship = resource_klass._relationship(relationship_name)
203
340
  if relationship && format_key(relationship_name) == include_parts.first
341
+ unless relationship.allow_include?(context)
342
+ fail JSONAPI::Exceptions::InvalidInclude.new(format_key(resource_klass._type), include_parts.first)
343
+ end
344
+
204
345
  unless include_parts.last.empty?
205
- check_include(Resource.resource_for(resource_klass.module_path + relationship.class_name.to_s.underscore), include_parts.last.partition('.'))
346
+ check_include(Resource.resource_klass_for(resource_klass.module_path + relationship.class_name.to_s.underscore),
347
+ include_parts.last.partition('.'))
206
348
  end
207
349
  else
208
- @errors.concat(JSONAPI::Exceptions::InvalidInclude.new(format_key(resource_klass._type),
209
- include_parts.first).errors)
350
+ fail JSONAPI::Exceptions::InvalidInclude.new(format_key(resource_klass._type), include_parts.first)
210
351
  end
352
+ true
211
353
  end
212
354
 
213
- def parse_include_directives(raw_include)
214
- return unless raw_include
215
-
216
- unless JSONAPI.configuration.allow_include
217
- fail JSONAPI::Exceptions::ParametersNotAllowed.new([:include])
218
- end
355
+ def parse_include_directives(resource_klass, raw_include)
356
+ raw_include ||= ''
219
357
 
220
358
  included_resources = []
221
359
  begin
222
- included_resources += CSV.parse_line(raw_include)
360
+ included_resources += raw_include.is_a?(Array) ? raw_include : CSV.parse_line(raw_include) || []
223
361
  rescue CSV::MalformedCSVError
224
- fail JSONAPI::Exceptions::InvalidInclude.new(format_key(@resource_klass._type), raw_include)
362
+ fail JSONAPI::Exceptions::InvalidInclude.new(format_key(resource_klass._type), raw_include)
225
363
  end
226
364
 
227
- return if included_resources.empty?
365
+ begin
366
+ result = included_resources.compact.map do |included_resource|
367
+ check_include(resource_klass, included_resource.partition('.'))
368
+ unformat_key(included_resource).to_s
369
+ end
228
370
 
229
- result = included_resources.compact.map do |included_resource|
230
- check_include(@resource_klass, included_resource.partition('.'))
231
- unformat_key(included_resource).to_s
371
+ return JSONAPI::IncludeDirectives.new(resource_klass, result)
372
+ rescue JSONAPI::Exceptions::InvalidInclude => e
373
+ @errors.concat(e.errors)
374
+ return {}
232
375
  end
233
-
234
- @include_directives = JSONAPI::IncludeDirectives.new(@resource_klass, result)
235
376
  end
236
377
 
237
- def parse_filters(filters)
238
- return unless filters
378
+ def parse_filters(resource_klass, filters)
379
+ parsed_filters = {}
239
380
 
240
- unless JSONAPI.configuration.allow_filter
241
- fail JSONAPI::Exceptions::ParametersNotAllowed.new([:filter])
381
+ # apply default filters
382
+ resource_klass._allowed_filters.each do |filter, opts|
383
+ next if opts[:default].nil? || !parsed_filters[filter].nil?
384
+ parsed_filters[filter] = opts[:default]
242
385
  end
243
386
 
387
+ return parsed_filters unless filters
388
+
244
389
  unless filters.class.method_defined?(:each)
245
390
  @errors.concat(JSONAPI::Exceptions::InvalidFiltersSyntax.new(filters).errors)
246
- return
391
+ return {}
392
+ end
393
+
394
+ unless JSONAPI.configuration.allow_filter
395
+ fail JSONAPI::Exceptions::ParameterNotAllowed.new(:filter)
247
396
  end
248
397
 
249
398
  filters.each do |key, value|
250
399
  filter = unformat_key(key)
251
- if @resource_klass._allowed_filter?(filter)
252
- @filters[filter] = value
400
+ if resource_klass._allowed_filter?(filter)
401
+ parsed_filters[filter] = value
253
402
  else
254
- @errors.concat(JSONAPI::Exceptions::FilterNotAllowed.new(filter).errors)
403
+ fail JSONAPI::Exceptions::FilterNotAllowed.new(key)
255
404
  end
256
405
  end
257
- end
258
406
 
259
- def set_default_filters
260
- @resource_klass._allowed_filters.each do |filter, opts|
261
- next if opts[:default].nil? || !@filters[filter].nil?
262
- @filters[filter] = opts[:default]
263
- end
407
+ parsed_filters
264
408
  end
265
409
 
266
- def parse_sort_criteria(sort_criteria)
410
+ def parse_sort_criteria(resource_klass, sort_criteria)
267
411
  return unless sort_criteria.present?
268
412
 
269
413
  unless JSONAPI.configuration.allow_sort
270
- fail JSONAPI::Exceptions::ParametersNotAllowed.new([:sort])
414
+ fail JSONAPI::Exceptions::ParameterNotAllowed.new(:sort)
271
415
  end
272
416
 
273
- sorts = []
274
- begin
275
- raw = URI.unescape(sort_criteria)
276
- sorts += CSV.parse_line(raw)
277
- rescue CSV::MalformedCSVError
278
- fail JSONAPI::Exceptions::InvalidSortCriteria.new(format_key(@resource_klass._type), raw)
417
+ if sort_criteria.is_a?(Array)
418
+ sorts = sort_criteria
419
+ elsif sort_criteria.is_a?(String)
420
+ begin
421
+ raw = URI.decode_www_form_component(sort_criteria)
422
+ sorts = CSV.parse_line(raw)
423
+ rescue CSV::MalformedCSVError
424
+ fail JSONAPI::Exceptions::InvalidSortCriteria.new(format_key(resource_klass._type), raw)
425
+ end
279
426
  end
280
427
 
281
428
  @sort_criteria = sorts.collect do |sort|
282
429
  if sort.start_with?('-')
283
- sort_criteria = { field: unformat_key(sort[1..-1]).to_s }
284
- sort_criteria[:direction] = :desc
430
+ criteria = { field: unformat_key(sort[1..-1]).to_s }
431
+ criteria[:direction] = :desc
285
432
  else
286
- sort_criteria = { field: unformat_key(sort).to_s }
287
- sort_criteria[:direction] = :asc
433
+ criteria = { field: unformat_key(sort).to_s }
434
+ criteria[:direction] = :asc
288
435
  end
289
436
 
290
- check_sort_criteria(@resource_klass, sort_criteria)
291
- sort_criteria
437
+ check_sort_criteria(resource_klass, criteria)
438
+ criteria
292
439
  end
293
440
  end
294
441
 
295
442
  def check_sort_criteria(resource_klass, sort_criteria)
296
443
  sort_field = sort_criteria[:field]
297
- sortable_fields = resource_klass.sortable_fields(context)
298
444
 
299
- unless sortable_fields.include? sort_field.to_sym
300
- @errors.concat(JSONAPI::Exceptions::InvalidSortCriteria
301
- .new(format_key(resource_klass._type), sort_field).errors)
445
+ unless resource_klass.sortable_field?(sort_field.to_sym, context)
446
+ fail JSONAPI::Exceptions::InvalidSortCriteria.new(format_key(resource_klass._type), sort_field)
302
447
  end
303
448
  end
304
449
 
305
- def add_find_operation
306
- @operations.push JSONAPI::Operation.new(:find,
307
- @resource_klass,
308
- context: @context,
309
- filters: @filters,
310
- include_directives: @include_directives,
311
- sort_criteria: @sort_criteria,
312
- paginator: @paginator,
313
- fields: @fields
314
- )
315
- end
316
-
317
- def add_show_operation
318
- @operations.push JSONAPI::Operation.new(:show,
319
- @resource_klass,
320
- context: @context,
321
- id: @id,
322
- include_directives: @include_directives,
323
- fields: @fields
324
- )
325
- end
326
-
327
- def add_show_relationship_operation(relationship_type, parent_key)
328
- @operations.push JSONAPI::Operation.new(:show_relationship,
329
- @resource_klass,
330
- context: @context,
331
- relationship_type: relationship_type,
332
- parent_key: @resource_klass.verify_key(parent_key)
333
- )
334
- end
335
-
336
- def add_show_related_resource_operation(relationship_type)
337
- @operations.push JSONAPI::Operation.new(:show_related_resource,
338
- @resource_klass,
339
- context: @context,
340
- relationship_type: relationship_type,
341
- source_klass: @source_klass,
342
- source_id: @source_id,
343
- fields: @fields,
344
- include_directives: @include_directives
345
- )
346
- end
347
-
348
- def add_show_related_resources_operation(relationship_type)
349
- @operations.push JSONAPI::Operation.new(:show_related_resources,
350
- @resource_klass,
351
- context: @context,
352
- relationship_type: relationship_type,
353
- source_klass: @source_klass,
354
- source_id: @source_id,
355
- filters: @source_klass.verify_filters(@filters, @context),
356
- sort_criteria: @sort_criteria,
357
- paginator: @paginator,
358
- fields: @fields,
359
- include_directives: @include_directives
360
- )
361
- end
362
-
363
- def parse_add_operation(params)
364
- fail JSONAPI::Exceptions::InvalidDataFormat unless params.respond_to?(:each_pair)
365
-
366
- verify_type(params[:type])
367
-
368
- data = parse_params(params, @resource_klass.creatable_fields(@context))
369
- @operations.push JSONAPI::Operation.new(:create_resource,
370
- @resource_klass,
371
- context: @context,
372
- data: data,
373
- fields: @fields,
374
- include_directives: @include_directives
375
- )
376
- rescue JSONAPI::Exceptions::Error => e
377
- @errors.concat(e.errors)
378
- end
379
-
380
- def verify_type(type)
450
+ def verify_type(type, resource_klass)
381
451
  if type.nil?
382
452
  fail JSONAPI::Exceptions::ParameterMissing.new(:type)
383
- elsif unformat_key(type).to_sym != @resource_klass._type
384
- fail JSONAPI::Exceptions::InvalidResource.new(type)
453
+ elsif unformat_key(type).to_sym != resource_klass._type
454
+ fail JSONAPI::Exceptions::InvalidResource.new(type, error_object_overrides)
385
455
  end
386
456
  end
387
457
 
388
458
  def parse_to_one_links_object(raw)
389
459
  if raw.nil?
390
460
  return {
391
- type: nil,
392
- id: nil
461
+ type: nil,
462
+ id: nil
393
463
  }
394
464
  end
395
465
 
396
466
  if !(raw.is_a?(Hash) || raw.is_a?(ActionController::Parameters)) ||
397
- raw.keys.length != 2 || !(raw.key?('type') && raw.key?('id'))
398
- fail JSONAPI::Exceptions::InvalidLinksObject.new
467
+ raw.keys.length != 2 || !(raw.key?('type') && raw.key?('id'))
468
+ fail JSONAPI::Exceptions::InvalidLinksObject.new(error_object_overrides)
399
469
  end
400
470
 
401
471
  {
402
- type: unformat_key(raw['type']).to_s,
403
- id: raw['id']
472
+ type: unformat_key(raw['type']).to_s,
473
+ id: raw['id']
404
474
  }
405
475
  end
406
476
 
407
477
  def parse_to_many_links_object(raw)
408
- fail JSONAPI::Exceptions::InvalidLinksObject.new if raw.nil?
478
+ fail JSONAPI::Exceptions::InvalidLinksObject.new(error_object_overrides) if raw.nil?
409
479
 
410
480
  links_object = {}
411
481
  if raw.is_a?(Array)
@@ -415,12 +485,12 @@ module JSONAPI
415
485
  links_object[link_object[:type]].push(link_object[:id])
416
486
  end
417
487
  else
418
- fail JSONAPI::Exceptions::InvalidLinksObject.new
488
+ fail JSONAPI::Exceptions::InvalidLinksObject.new(error_object_overrides)
419
489
  end
420
490
  links_object
421
491
  end
422
492
 
423
- def parse_params(params, allowed_fields)
493
+ def parse_params(resource_klass, params, allowed_fields)
424
494
  verify_permitted_params(params, allowed_fields)
425
495
 
426
496
  checked_attributes = {}
@@ -429,37 +499,37 @@ module JSONAPI
429
499
 
430
500
  params.each do |key, value|
431
501
  case key.to_s
432
- when 'relationships'
433
- value.each do |link_key, link_value|
434
- param = unformat_key(link_key)
435
- relationship = @resource_klass._relationship(param)
436
-
437
- if relationship.is_a?(JSONAPI::Relationship::ToOne)
438
- checked_to_one_relationships[param] = parse_to_one_relationship(link_value, relationship)
439
- elsif relationship.is_a?(JSONAPI::Relationship::ToMany)
440
- parse_to_many_relationship(link_value, relationship) do |result_val|
441
- checked_to_many_relationships[param] = result_val
502
+ when 'relationships'
503
+ value.each do |link_key, link_value|
504
+ param = unformat_key(link_key)
505
+ relationship = resource_klass._relationship(param)
506
+
507
+ if relationship.is_a?(JSONAPI::Relationship::ToOne)
508
+ checked_to_one_relationships[param] = parse_to_one_relationship(resource_klass, link_value, relationship)
509
+ elsif relationship.is_a?(JSONAPI::Relationship::ToMany)
510
+ parse_to_many_relationship(resource_klass, link_value, relationship) do |result_val|
511
+ checked_to_many_relationships[param] = result_val
512
+ end
442
513
  end
443
514
  end
444
- end
445
- when 'id'
446
- checked_attributes['id'] = unformat_value(:id, value)
447
- when 'attributes'
448
- value.each do |key, value|
449
- param = unformat_key(key)
450
- checked_attributes[param] = unformat_value(param, value)
451
- end
515
+ when 'id'
516
+ checked_attributes['id'] = unformat_value(resource_klass, :id, value)
517
+ when 'attributes'
518
+ value.each do |key, value|
519
+ param = unformat_key(key)
520
+ checked_attributes[param] = unformat_value(resource_klass, param, value)
521
+ end
452
522
  end
453
523
  end
454
524
 
455
525
  return {
456
- 'attributes' => checked_attributes,
457
- 'to_one' => checked_to_one_relationships,
458
- 'to_many' => checked_to_many_relationships
526
+ 'attributes' => checked_attributes,
527
+ 'to_one' => checked_to_one_relationships,
528
+ 'to_many' => checked_to_many_relationships
459
529
  }.deep_transform_keys { |key| unformat_key(key) }
460
530
  end
461
531
 
462
- def parse_to_one_relationship(link_value, relationship)
532
+ def parse_to_one_relationship(resource_klass, link_value, relationship)
463
533
  if link_value.nil?
464
534
  linkage = nil
465
535
  else
@@ -468,12 +538,12 @@ module JSONAPI
468
538
 
469
539
  links_object = parse_to_one_links_object(linkage)
470
540
  if !relationship.polymorphic? && links_object[:type] && (links_object[:type].to_s != relationship.type.to_s)
471
- fail JSONAPI::Exceptions::TypeMismatch.new(links_object[:type])
541
+ fail JSONAPI::Exceptions::TypeMismatch.new(links_object[:type], error_object_overrides)
472
542
  end
473
543
 
474
544
  unless links_object[:id].nil?
475
- resource = self.resource_klass || Resource
476
- relationship_resource = resource.resource_for(unformat_key(links_object[:type]).to_s)
545
+ resource = resource_klass || Resource
546
+ relationship_resource = resource.resource_klass_for(unformat_key(relationship.options[:class_name] || links_object[:type]).to_s)
477
547
  relationship_id = relationship_resource.verify_key(links_object[:id], @context)
478
548
  if relationship.polymorphic?
479
549
  { id: relationship_id, type: unformat_key(links_object[:type].to_s) }
@@ -485,37 +555,54 @@ module JSONAPI
485
555
  end
486
556
  end
487
557
 
488
- def parse_to_many_relationship(link_value, relationship, &add_result)
489
- if link_value.is_a?(Array) && link_value.length == 0
490
- linkage = []
491
- elsif (link_value.is_a?(Hash) || link_value.is_a?(ActionController::Parameters))
558
+ def parse_to_many_relationship(resource_klass, link_value, relationship, &add_result)
559
+ if (link_value.is_a?(Hash) || link_value.is_a?(ActionController::Parameters))
492
560
  linkage = link_value[:data]
493
561
  else
494
- fail JSONAPI::Exceptions::InvalidLinksObject.new
562
+ fail JSONAPI::Exceptions::InvalidLinksObject.new(error_object_overrides)
495
563
  end
496
564
 
497
565
  links_object = parse_to_many_links_object(linkage)
498
566
 
499
- # Since we do not yet support polymorphic to_many relationships we will raise an error if the type does not match the
500
- # relationship's type.
501
- # ToDo: Support Polymorphic relationships
502
-
503
567
  if links_object.length == 0
504
568
  add_result.call([])
505
569
  else
506
- if links_object.length > 1 || !links_object.has_key?(unformat_key(relationship.type).to_s)
507
- fail JSONAPI::Exceptions::TypeMismatch.new(links_object[:type])
508
- end
570
+ if relationship.polymorphic?
571
+ polymorphic_results = []
572
+
573
+ links_object.each_pair do |type, keys|
574
+ type_name = unformat_key(type).to_s
575
+
576
+ relationship_resource_klass = resource_klass.resource_klass_for(relationship.class_name)
577
+ relationship_klass = relationship_resource_klass._model_class
578
+
579
+ linkage_object_resource_klass = resource_klass.resource_klass_for(type_name)
580
+ linkage_object_klass = linkage_object_resource_klass._model_class
581
+
582
+ unless linkage_object_klass == relationship_klass || linkage_object_klass.in?(relationship_klass.subclasses)
583
+ fail JSONAPI::Exceptions::TypeMismatch.new(type_name)
584
+ end
585
+
586
+ relationship_ids = relationship_resource_klass.verify_keys(keys, @context)
587
+ polymorphic_results << { type: type, ids: relationship_ids }
588
+ end
509
589
 
510
- links_object.each_pair do |type, keys|
511
- relationship_resource = Resource.resource_for(@resource_klass.module_path + unformat_key(type).to_s)
512
- add_result.call relationship_resource.verify_keys(keys, @context)
590
+ add_result.call polymorphic_results
591
+ else
592
+ relationship_type = unformat_key(relationship.type).to_s
593
+
594
+ if links_object.length > 1 || !links_object.has_key?(relationship_type)
595
+ fail JSONAPI::Exceptions::TypeMismatch.new(links_object[:type])
596
+ end
597
+
598
+ relationship_resource_klass = Resource.resource_klass_for(resource_klass.module_path + relationship_type)
599
+ add_result.call relationship_resource_klass.verify_keys(links_object[relationship_type], @context)
513
600
  end
514
601
  end
515
602
  end
516
603
 
517
- def unformat_value(attribute, value)
518
- value_formatter = JSONAPI::ValueFormatter.value_formatter_for(@resource_klass._attribute_options(attribute)[:format])
604
+ def unformat_value(resource_klass, attribute, value)
605
+ value_formatter = JSONAPI::ValueFormatter.value_formatter_for(resource_klass._attribute_options(attribute)[:format])
519
606
  value_formatter.unformat(value)
520
607
  end
521
608
 
@@ -525,68 +612,76 @@ module JSONAPI
525
612
 
526
613
  params.each do |key, value|
527
614
  case key.to_s
528
- when 'relationships'
529
- value.keys.each do |links_key|
530
- unless formatted_allowed_fields.include?(links_key.to_sym)
531
- params_not_allowed.push(links_key)
532
- unless JSONAPI.configuration.raise_if_parameters_not_allowed
533
- value.delete links_key
615
+ when 'relationships'
616
+ value.keys.each do |links_key|
617
+ unless formatted_allowed_fields.include?(links_key.to_sym)
618
+ if JSONAPI.configuration.raise_if_parameters_not_allowed
619
+ fail JSONAPI::Exceptions::ParameterNotAllowed.new(links_key, error_object_overrides)
620
+ else
621
+ params_not_allowed.push(links_key)
622
+ value.delete links_key
623
+ end
534
624
  end
535
625
  end
536
- end
537
- when 'attributes'
538
- value.each do |attr_key, attr_value|
539
- unless formatted_allowed_fields.include?(attr_key.to_sym)
540
- params_not_allowed.push(attr_key)
541
- unless JSONAPI.configuration.raise_if_parameters_not_allowed
542
- value.delete attr_key
626
+ when 'attributes'
627
+ value.each do |attr_key, _attr_value|
628
+ unless formatted_allowed_fields.include?(attr_key.to_sym)
629
+ if JSONAPI.configuration.raise_if_parameters_not_allowed
630
+ fail JSONAPI::Exceptions::ParameterNotAllowed.new(attr_key, error_object_overrides)
631
+ else
632
+ params_not_allowed.push(attr_key)
633
+ value.delete attr_key
634
+ end
543
635
  end
544
636
  end
545
- end
546
- when 'type'
547
- when 'id'
548
- unless formatted_allowed_fields.include?(:id)
549
- params_not_allowed.push(:id)
550
- unless JSONAPI.configuration.raise_if_parameters_not_allowed
551
- params.delete :id
637
+ when 'type'
638
+ when 'id'
639
+ unless formatted_allowed_fields.include?(:id)
640
+ if JSONAPI.configuration.raise_if_parameters_not_allowed
641
+ fail JSONAPI::Exceptions::ParameterNotAllowed.new(:id, error_object_overrides)
642
+ else
643
+ params_not_allowed.push(:id)
644
+ params.delete :id
645
+ end
646
+ end
647
+ else
648
+ if JSONAPI.configuration.raise_if_parameters_not_allowed
649
+ fail JSONAPI::Exceptions::ParameterNotAllowed.new(key, error_object_overrides)
650
+ else
651
+ params_not_allowed.push(key)
652
+ params.delete key
552
653
  end
553
- end
554
- else
555
- params_not_allowed.push(key)
556
654
  end
557
655
  end
558
656
 
559
657
  if params_not_allowed.length > 0
560
- if JSONAPI.configuration.raise_if_parameters_not_allowed
561
- fail JSONAPI::Exceptions::ParametersNotAllowed.new(params_not_allowed)
562
- else
563
- params_not_allowed_warnings = params_not_allowed.map do |key|
564
- JSONAPI::Warning.new(code: JSONAPI::PARAM_NOT_ALLOWED,
565
- title: 'Param not allowed',
566
- detail: "#{key} is not allowed.")
567
- end
568
- self.warnings.concat(params_not_allowed_warnings)
658
+ params_not_allowed_warnings = params_not_allowed.map do |param|
659
+ JSONAPI::Warning.new(code: JSONAPI::PARAM_NOT_ALLOWED,
660
+ title: 'Param not allowed',
661
+ detail: "#{param} is not allowed.")
569
662
  end
663
+ self.warnings.concat(params_not_allowed_warnings)
570
664
  end
571
665
  end
572
666
 
573
- def parse_add_relationship_operation(verified_params, relationship, parent_key)
667
+ def parse_add_relationship_operation(resource_klass, verified_params, relationship, parent_key)
574
668
  if relationship.is_a?(JSONAPI::Relationship::ToMany)
575
- @operations.push JSONAPI::Operation.new(:create_to_many_relationships,
576
- resource_klass,
577
- context: @context,
578
- resource_id: parent_key,
579
- relationship_type: relationship.name,
580
- data: verified_params[:to_many].values[0]
669
+ return JSONAPI::Operation.new(
670
+ :create_to_many_relationships,
671
+ resource_klass,
672
+ context: @context,
673
+ resource_id: parent_key,
674
+ relationship_type: relationship.name,
675
+ data: verified_params[:to_many].values[0]
581
676
  )
582
677
  end
583
678
  end
584
679
 
585
- def parse_update_relationship_operation(verified_params, relationship, parent_key)
680
+ def parse_update_relationship_operation(resource_klass, verified_params, relationship, parent_key)
586
681
  options = {
587
- context: @context,
588
- resource_id: parent_key,
589
- relationship_type: relationship.name
682
+ context: @context,
683
+ resource_id: parent_key,
684
+ relationship_type: relationship.name
590
685
  }
591
686
 
592
687
  if relationship.is_a?(JSONAPI::Relationship::ToOne)
@@ -607,62 +702,30 @@ module JSONAPI
607
702
  operation_type = :replace_to_many_relationships
608
703
  end
609
704
 
610
- @operations.push JSONAPI::Operation.new(operation_type, resource_klass, options)
705
+ JSONAPI::Operation.new(operation_type, resource_klass, options)
611
706
  end
612
707
 
613
- def parse_single_replace_operation(data, keys, id_key_presence_check_required: true)
614
- fail JSONAPI::Exceptions::InvalidDataFormat unless data.respond_to?(:each_pair)
615
-
616
- fail JSONAPI::Exceptions::MissingKey.new if data[:id].nil?
617
-
618
- key = data[:id].to_s
619
- if id_key_presence_check_required && !keys.include?(key)
620
- fail JSONAPI::Exceptions::KeyNotIncludedInURL.new(key)
621
- end
622
-
623
- data.delete(:id) unless keys.include?(:id)
624
-
625
- verify_type(data[:type])
626
-
627
- @operations.push JSONAPI::Operation.new(:replace_fields,
628
- @resource_klass,
629
- context: @context,
630
- resource_id: key,
631
- data: parse_params(data, @resource_klass.updatable_fields(@context)),
632
- fields: @fields,
633
- include_directives: @include_directives
634
- )
635
- end
636
-
637
- def parse_replace_operation(data, keys)
638
- parse_single_replace_operation(data, [keys], id_key_presence_check_required: keys.present?)
639
- rescue JSONAPI::Exceptions::Error => e
640
- @errors.concat(e.errors)
641
- end
642
-
643
- def parse_remove_operation(params)
644
- @operations.push JSONAPI::Operation.new(:remove_resource,
645
- @resource_klass,
646
- context: @context,
647
- resource_id: @resource_klass.verify_key(params.require(:id), context))
648
- rescue JSONAPI::Exceptions::Error => e
649
- @errors.concat(e.errors)
650
- end
651
-
652
- def parse_remove_relationship_operation(params, relationship, parent_key)
708
+ def parse_remove_relationship_operation(resource_klass, params, relationship, parent_key)
653
709
  operation_base_args = [resource_klass].push(
654
- context: @context,
655
- resource_id: parent_key,
656
- relationship_type: relationship.name
710
+ context: @context,
711
+ resource_id: parent_key,
712
+ relationship_type: relationship.name
657
713
  )
658
714
 
659
715
  if relationship.is_a?(JSONAPI::Relationship::ToMany)
660
716
  operation_args = operation_base_args.dup
661
717
  keys = params[:to_many].values[0]
662
718
  operation_args[1] = operation_args[1].merge(associated_keys: keys)
663
- @operations.push JSONAPI::Operation.new(:remove_to_many_relationships, *operation_args)
719
+ JSONAPI::Operation.new(:remove_to_many_relationships, *operation_args)
664
720
  else
665
- @operations.push JSONAPI::Operation.new(:remove_to_one_relationship, *operation_base_args)
721
+ JSONAPI::Operation.new(:remove_to_one_relationship, *operation_base_args)
722
+ end
723
+ end
724
+
725
+ def resolve_singleton_id(params, resource_klass)
726
+ if resource_klass.singleton? && params[:id].nil?
727
+ key = resource_klass.singleton_key(context)
728
+ params[:id] = key
666
729
  end
667
730
  end
668
731