sanger-jsonapi-resources 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +22 -0
  3. data/README.md +53 -0
  4. data/lib/generators/jsonapi/USAGE +13 -0
  5. data/lib/generators/jsonapi/controller_generator.rb +14 -0
  6. data/lib/generators/jsonapi/resource_generator.rb +14 -0
  7. data/lib/generators/jsonapi/templates/jsonapi_controller.rb +4 -0
  8. data/lib/generators/jsonapi/templates/jsonapi_resource.rb +4 -0
  9. data/lib/jsonapi/acts_as_resource_controller.rb +320 -0
  10. data/lib/jsonapi/cached_resource_fragment.rb +127 -0
  11. data/lib/jsonapi/callbacks.rb +51 -0
  12. data/lib/jsonapi/compiled_json.rb +36 -0
  13. data/lib/jsonapi/configuration.rb +258 -0
  14. data/lib/jsonapi/error.rb +47 -0
  15. data/lib/jsonapi/error_codes.rb +60 -0
  16. data/lib/jsonapi/exceptions.rb +563 -0
  17. data/lib/jsonapi/formatter.rb +169 -0
  18. data/lib/jsonapi/include_directives.rb +100 -0
  19. data/lib/jsonapi/link_builder.rb +152 -0
  20. data/lib/jsonapi/mime_types.rb +41 -0
  21. data/lib/jsonapi/naive_cache.rb +30 -0
  22. data/lib/jsonapi/operation.rb +24 -0
  23. data/lib/jsonapi/operation_dispatcher.rb +88 -0
  24. data/lib/jsonapi/operation_result.rb +65 -0
  25. data/lib/jsonapi/operation_results.rb +35 -0
  26. data/lib/jsonapi/paginator.rb +209 -0
  27. data/lib/jsonapi/processor.rb +328 -0
  28. data/lib/jsonapi/relationship.rb +94 -0
  29. data/lib/jsonapi/relationship_builder.rb +167 -0
  30. data/lib/jsonapi/request_parser.rb +678 -0
  31. data/lib/jsonapi/resource.rb +1255 -0
  32. data/lib/jsonapi/resource_controller.rb +5 -0
  33. data/lib/jsonapi/resource_controller_metal.rb +16 -0
  34. data/lib/jsonapi/resource_serializer.rb +531 -0
  35. data/lib/jsonapi/resources/version.rb +5 -0
  36. data/lib/jsonapi/response_document.rb +135 -0
  37. data/lib/jsonapi/routing_ext.rb +262 -0
  38. data/lib/jsonapi-resources.rb +27 -0
  39. metadata +223 -0
@@ -0,0 +1,678 @@
1
+ require 'jsonapi/operation'
2
+ require 'jsonapi/paginator'
3
+
4
+ module JSONAPI
5
+ class RequestParser
6
+ attr_accessor :fields, :include, :filters, :sort_criteria, :errors, :operations,
7
+ :resource_klass, :context, :paginator, :source_klass, :source_id,
8
+ :include_directives, :params, :warnings, :server_error_callbacks
9
+
10
+ def initialize(params = nil, options = {})
11
+ @params = params
12
+ @context = options[:context]
13
+ @key_formatter = options.fetch(:key_formatter, JSONAPI.configuration.key_formatter)
14
+ @errors = []
15
+ @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
+ @server_error_callbacks = options.fetch(:server_error_callbacks, [])
26
+
27
+ setup_action(@params)
28
+ end
29
+
30
+ def setup_action(params)
31
+ return if params.nil?
32
+
33
+ @resource_klass ||= Resource.resource_for(params[:controller]) if params[:controller]
34
+
35
+ setup_action_method_name = "setup_#{params[:action]}_action"
36
+ if respond_to?(setup_action_method_name)
37
+ raise params[:_parser_exception] if params[:_parser_exception]
38
+ send(setup_action_method_name, params)
39
+ end
40
+ rescue ActionController::ParameterMissing => e
41
+ @errors.concat(JSONAPI::Exceptions::ParameterMissing.new(e.param).errors)
42
+ end
43
+
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
52
+ end
53
+
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])
63
+ end
64
+
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])
74
+ end
75
+
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
81
+ end
82
+
83
+ def setup_show_relationship_action(params)
84
+ add_show_relationship_operation(params[:relationship], params.require(@resource_klass._as_parent_key))
85
+ end
86
+
87
+ def setup_create_action(params)
88
+ parse_fields(params[:fields])
89
+ parse_include_directives(params[:include])
90
+ parse_add_operation(params.require(:data))
91
+ end
92
+
93
+ def setup_create_relationship_action(params)
94
+ parse_modify_relationship_action(params, :add)
95
+ end
96
+
97
+ def setup_update_relationship_action(params)
98
+ parse_modify_relationship_action(params, :update)
99
+ end
100
+
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])
105
+ end
106
+
107
+ def setup_destroy_action(params)
108
+ parse_remove_operation(params)
109
+ end
110
+
111
+ def setup_destroy_relationship_action(params)
112
+ parse_modify_relationship_action(params, :remove)
113
+ end
114
+
115
+ def parse_modify_relationship_action(params, modification_type)
116
+ relationship_type = params.require(:relationship)
117
+ parent_key = params.require(@resource_klass._as_parent_key)
118
+ relationship = @resource_klass._relationship(relationship_type)
119
+
120
+ # Removals of to-one relationships are done implicitly and require no specification of data
121
+ data_required = !(modification_type == :remove && relationship.is_a?(JSONAPI::Relationship::ToOne))
122
+
123
+ if data_required
124
+ data = params.fetch(:data)
125
+ object_params = { relationships: { format_key(relationship.name) => { data: data } } }
126
+ verified_params = parse_params(object_params, @resource_klass.updatable_fields(@context))
127
+
128
+ parse_arguments = [verified_params, relationship, parent_key]
129
+ else
130
+ parse_arguments = [params, relationship, parent_key]
131
+ end
132
+
133
+ send(:"parse_#{modification_type}_relationship_operation", *parse_arguments)
134
+ end
135
+
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)
139
+ end
140
+
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
+
151
+ extracted_fields = {}
152
+
153
+ # Extract the fields for each type from the fields parameters
154
+ if fields.is_a?(ActionController::Parameters)
155
+ fields.each do |field, value|
156
+ resource_fields = value.split(',') unless value.nil? || value.empty?
157
+ extracted_fields[field] = resource_fields
158
+ end
159
+ else
160
+ fail JSONAPI::Exceptions::InvalidFieldFormat.new
161
+ end
162
+
163
+ # Validate the fields
164
+ extracted_fields.each do |type, values|
165
+ underscored_type = unformat_key(type)
166
+ extracted_fields[type] = []
167
+ begin
168
+ if type != format_key(type)
169
+ fail JSONAPI::Exceptions::InvalidResource.new(type)
170
+ end
171
+ type_resource = Resource.resource_for(@resource_klass.module_path + underscored_type.to_s)
172
+ rescue NameError
173
+ @errors.concat(JSONAPI::Exceptions::InvalidResource.new(type).errors)
174
+ rescue JSONAPI::Exceptions::InvalidResource => e
175
+ @errors.concat(e.errors)
176
+ end
177
+
178
+ if type_resource.nil?
179
+ @errors.concat(JSONAPI::Exceptions::InvalidResource.new(type).errors)
180
+ else
181
+ unless values.nil?
182
+ valid_fields = type_resource.fields.collect { |key| format_key(key) }
183
+ values.each do |field|
184
+ if valid_fields.include?(field)
185
+ extracted_fields[type].push unformat_key(field)
186
+ else
187
+ @errors.concat(JSONAPI::Exceptions::InvalidField.new(type, field).errors)
188
+ end
189
+ end
190
+ else
191
+ @errors.concat(JSONAPI::Exceptions::InvalidField.new(type, 'nil').errors)
192
+ end
193
+ end
194
+ end
195
+
196
+ @fields = extracted_fields.deep_transform_keys { |key| unformat_key(key) }
197
+ end
198
+
199
+ def check_include(resource_klass, include_parts)
200
+ relationship_name = unformat_key(include_parts.first)
201
+
202
+ relationship = resource_klass._relationship(relationship_name)
203
+ if relationship && format_key(relationship_name) == include_parts.first
204
+ 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('.'))
206
+ end
207
+ else
208
+ @errors.concat(JSONAPI::Exceptions::InvalidInclude.new(format_key(resource_klass._type),
209
+ include_parts.first).errors)
210
+ end
211
+ end
212
+
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
219
+
220
+ included_resources = []
221
+ begin
222
+ included_resources += CSV.parse_line(raw_include)
223
+ rescue CSV::MalformedCSVError
224
+ fail JSONAPI::Exceptions::InvalidInclude.new(format_key(@resource_klass._type), raw_include)
225
+ end
226
+
227
+ return if included_resources.empty?
228
+
229
+ result = included_resources.compact.map do |included_resource|
230
+ check_include(@resource_klass, included_resource.partition('.'))
231
+ unformat_key(included_resource).to_s
232
+ end
233
+
234
+ @include_directives = JSONAPI::IncludeDirectives.new(@resource_klass, result)
235
+ end
236
+
237
+ def parse_filters(filters)
238
+ return unless filters
239
+
240
+ unless JSONAPI.configuration.allow_filter
241
+ fail JSONAPI::Exceptions::ParametersNotAllowed.new([:filter])
242
+ end
243
+
244
+ unless filters.class.method_defined?(:each)
245
+ @errors.concat(JSONAPI::Exceptions::InvalidFiltersSyntax.new(filters).errors)
246
+ return
247
+ end
248
+
249
+ filters.each do |key, value|
250
+ filter = unformat_key(key)
251
+ if @resource_klass._allowed_filter?(filter)
252
+ @filters[filter] = value
253
+ else
254
+ @errors.concat(JSONAPI::Exceptions::FilterNotAllowed.new(filter).errors)
255
+ end
256
+ end
257
+ end
258
+
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
264
+ end
265
+
266
+ def parse_sort_criteria(sort_criteria)
267
+ return unless sort_criteria.present?
268
+
269
+ unless JSONAPI.configuration.allow_sort
270
+ fail JSONAPI::Exceptions::ParametersNotAllowed.new([:sort])
271
+ end
272
+
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)
279
+ end
280
+
281
+ @sort_criteria = sorts.collect do |sort|
282
+ if sort.start_with?('-')
283
+ sort_criteria = { field: unformat_key(sort[1..-1]).to_s }
284
+ sort_criteria[:direction] = :desc
285
+ else
286
+ sort_criteria = { field: unformat_key(sort).to_s }
287
+ sort_criteria[:direction] = :asc
288
+ end
289
+
290
+ check_sort_criteria(@resource_klass, sort_criteria)
291
+ sort_criteria
292
+ end
293
+ end
294
+
295
+ def check_sort_criteria(resource_klass, sort_criteria)
296
+ sort_field = sort_criteria[:field]
297
+ sortable_fields = resource_klass.sortable_fields(context)
298
+
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)
302
+ end
303
+ end
304
+
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)
381
+ if type.nil?
382
+ fail JSONAPI::Exceptions::ParameterMissing.new(:type)
383
+ elsif unformat_key(type).to_sym != @resource_klass._type
384
+ fail JSONAPI::Exceptions::InvalidResource.new(type)
385
+ end
386
+ end
387
+
388
+ def parse_to_one_links_object(raw)
389
+ if raw.nil?
390
+ return {
391
+ type: nil,
392
+ id: nil
393
+ }
394
+ end
395
+
396
+ 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
399
+ end
400
+
401
+ {
402
+ type: unformat_key(raw['type']).to_s,
403
+ id: raw['id']
404
+ }
405
+ end
406
+
407
+ def parse_to_many_links_object(raw)
408
+ fail JSONAPI::Exceptions::InvalidLinksObject.new if raw.nil?
409
+
410
+ links_object = {}
411
+ if raw.is_a?(Array)
412
+ raw.each do |link|
413
+ link_object = parse_to_one_links_object(link)
414
+ links_object[link_object[:type]] ||= []
415
+ links_object[link_object[:type]].push(link_object[:id])
416
+ end
417
+ else
418
+ fail JSONAPI::Exceptions::InvalidLinksObject.new
419
+ end
420
+ links_object
421
+ end
422
+
423
+ def parse_params(params, allowed_fields)
424
+ verify_permitted_params(params, allowed_fields)
425
+
426
+ checked_attributes = {}
427
+ checked_to_one_relationships = {}
428
+ checked_to_many_relationships = {}
429
+
430
+ params.each do |key, value|
431
+ 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
442
+ end
443
+ 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
452
+ end
453
+ end
454
+
455
+ return {
456
+ 'attributes' => checked_attributes,
457
+ 'to_one' => checked_to_one_relationships,
458
+ 'to_many' => checked_to_many_relationships
459
+ }.deep_transform_keys { |key| unformat_key(key) }
460
+ end
461
+
462
+ def parse_to_one_relationship(link_value, relationship)
463
+ if link_value.nil?
464
+ linkage = nil
465
+ else
466
+ linkage = link_value[:data]
467
+ end
468
+
469
+ links_object = parse_to_one_links_object(linkage)
470
+ if !relationship.polymorphic? && links_object[:type] && (links_object[:type].to_s != relationship.type.to_s)
471
+ fail JSONAPI::Exceptions::TypeMismatch.new(links_object[:type])
472
+ end
473
+
474
+ unless links_object[:id].nil?
475
+ resource = self.resource_klass || Resource
476
+ relationship_resource = resource.resource_for(unformat_key(links_object[:type]).to_s)
477
+ relationship_id = relationship_resource.verify_key(links_object[:id], @context)
478
+ if relationship.polymorphic?
479
+ { id: relationship_id, type: unformat_key(links_object[:type].to_s) }
480
+ else
481
+ relationship_id
482
+ end
483
+ else
484
+ nil
485
+ end
486
+ end
487
+
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))
492
+ linkage = link_value[:data]
493
+ else
494
+ fail JSONAPI::Exceptions::InvalidLinksObject.new
495
+ end
496
+
497
+ links_object = parse_to_many_links_object(linkage)
498
+
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
+ if links_object.length == 0
504
+ add_result.call([])
505
+ 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
509
+
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)
513
+ end
514
+ end
515
+ end
516
+
517
+ def unformat_value(attribute, value)
518
+ value_formatter = JSONAPI::ValueFormatter.value_formatter_for(@resource_klass._attribute_options(attribute)[:format])
519
+ value_formatter.unformat(value)
520
+ end
521
+
522
+ def verify_permitted_params(params, allowed_fields)
523
+ formatted_allowed_fields = allowed_fields.collect { |field| format_key(field).to_sym }
524
+ params_not_allowed = []
525
+
526
+ params.each do |key, value|
527
+ 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
534
+ end
535
+ 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
543
+ end
544
+ 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
552
+ end
553
+ end
554
+ else
555
+ params_not_allowed.push(key)
556
+ end
557
+ end
558
+
559
+ 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)
569
+ end
570
+ end
571
+ end
572
+
573
+ def parse_add_relationship_operation(verified_params, relationship, parent_key)
574
+ 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]
581
+ )
582
+ end
583
+ end
584
+
585
+ def parse_update_relationship_operation(verified_params, relationship, parent_key)
586
+ options = {
587
+ context: @context,
588
+ resource_id: parent_key,
589
+ relationship_type: relationship.name
590
+ }
591
+
592
+ if relationship.is_a?(JSONAPI::Relationship::ToOne)
593
+ if relationship.polymorphic?
594
+ options[:key_value] = verified_params[:to_one].values[0][:id]
595
+ options[:key_type] = verified_params[:to_one].values[0][:type]
596
+
597
+ operation_type = :replace_polymorphic_to_one_relationship
598
+ else
599
+ options[:key_value] = verified_params[:to_one].values[0]
600
+ operation_type = :replace_to_one_relationship
601
+ end
602
+ elsif relationship.is_a?(JSONAPI::Relationship::ToMany)
603
+ unless relationship.acts_as_set
604
+ fail JSONAPI::Exceptions::ToManySetReplacementForbidden.new
605
+ end
606
+ options[:data] = verified_params[:to_many].values[0]
607
+ operation_type = :replace_to_many_relationships
608
+ end
609
+
610
+ @operations.push JSONAPI::Operation.new(operation_type, resource_klass, options)
611
+ end
612
+
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)
653
+ operation_base_args = [resource_klass].push(
654
+ context: @context,
655
+ resource_id: parent_key,
656
+ relationship_type: relationship.name
657
+ )
658
+
659
+ if relationship.is_a?(JSONAPI::Relationship::ToMany)
660
+ operation_args = operation_base_args.dup
661
+ keys = params[:to_many].values[0]
662
+ operation_args[1] = operation_args[1].merge(associated_keys: keys)
663
+ @operations.push JSONAPI::Operation.new(:remove_to_many_relationships, *operation_args)
664
+ else
665
+ @operations.push JSONAPI::Operation.new(:remove_to_one_relationship, *operation_base_args)
666
+ end
667
+ end
668
+
669
+ def format_key(key)
670
+ @key_formatter.format(key)
671
+ end
672
+
673
+ def unformat_key(key)
674
+ unformatted_key = @key_formatter.unformat(key)
675
+ unformatted_key.nil? ? nil : unformatted_key.to_sym
676
+ end
677
+ end
678
+ end