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,5 @@
1
+ module JSONAPI
2
+ class ResourceController < ActionController::Base
3
+ include JSONAPI::ActsAsResourceController
4
+ end
5
+ end
@@ -0,0 +1,16 @@
1
+ module JSONAPI
2
+ class ResourceControllerMetal < ActionController::Metal
3
+ MODULES = [
4
+ AbstractController::Rendering,
5
+ ActionController::Rendering,
6
+ ActionController::Renderers::All,
7
+ ActionController::StrongParameters,
8
+ ActionController::Instrumentation,
9
+ JSONAPI::ActsAsResourceController
10
+ ].freeze
11
+
12
+ MODULES.each do |mod|
13
+ include mod
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,531 @@
1
+ module JSONAPI
2
+ class ResourceSerializer
3
+
4
+ attr_reader :link_builder, :key_formatter, :serialization_options, :primary_class_name,
5
+ :fields, :include_directives, :always_include_to_one_linkage_data,
6
+ :always_include_to_many_linkage_data
7
+
8
+ # initialize
9
+ # Options can include
10
+ # include:
11
+ # Purpose: determines which objects will be side loaded with the source objects in a linked section
12
+ # Example: ['comments','author','comments.tags','author.posts']
13
+ # fields:
14
+ # Purpose: determines which fields are serialized for a resource type. This encompasses both attributes and
15
+ # relationship ids in the links section for a resource. Fields are global for a resource type.
16
+ # Example: { people: [:id, :email, :comments], posts: [:id, :title, :author], comments: [:id, :body, :post]}
17
+ # key_formatter: KeyFormatter instance to override the default configuration
18
+ # serialization_options: additional options that will be passed to resource meta and links lambdas
19
+
20
+ def initialize(primary_resource_klass, options = {})
21
+ @primary_resource_klass = primary_resource_klass
22
+ @primary_class_name = primary_resource_klass._type
23
+ @fields = options.fetch(:fields, {})
24
+ @include = options.fetch(:include, [])
25
+ @include_directives = options[:include_directives]
26
+ @include_directives ||= JSONAPI::IncludeDirectives.new(@primary_resource_klass, @include)
27
+ @key_formatter = options.fetch(:key_formatter, JSONAPI.configuration.key_formatter)
28
+ @id_formatter = ValueFormatter.value_formatter_for(:id)
29
+ @link_builder = generate_link_builder(primary_resource_klass, options)
30
+ @always_include_to_one_linkage_data = options.fetch(:always_include_to_one_linkage_data,
31
+ JSONAPI.configuration.always_include_to_one_linkage_data)
32
+ @always_include_to_many_linkage_data = options.fetch(:always_include_to_many_linkage_data,
33
+ JSONAPI.configuration.always_include_to_many_linkage_data)
34
+ @serialization_options = options.fetch(:serialization_options, {})
35
+
36
+ # Warning: This makes ResourceSerializer non-thread-safe. That's not a problem with the
37
+ # request-specific way it's currently used, though.
38
+ @value_formatter_type_cache = NaiveCache.new{|arg| ValueFormatter.value_formatter_for(arg) }
39
+
40
+ @_config_keys = {}
41
+ @_supplying_attribute_fields = {}
42
+ @_supplying_relationship_fields = {}
43
+ end
44
+
45
+ # Converts a single resource, or an array of resources to a hash, conforming to the JSONAPI structure
46
+ def serialize_to_hash(source)
47
+ @top_level_sources = Set.new([source].flatten(1).compact.map {|s| top_level_source_key(s) })
48
+
49
+ is_resource_collection = source.respond_to?(:to_ary)
50
+
51
+ @included_objects = {}
52
+
53
+ process_source_objects(source, @include_directives.include_directives)
54
+
55
+ primary_objects = []
56
+
57
+ # pull the processed objects corresponding to the source objects. Ensures we preserve order.
58
+ if is_resource_collection
59
+ source.each do |primary|
60
+ if primary.id
61
+ case primary
62
+ when CachedResourceFragment then primary_objects.push(@included_objects[primary.type][primary.id][:object_hash])
63
+ when Resource then primary_objects.push(@included_objects[primary.class._type][primary.id][:object_hash])
64
+ else raise "Unknown source type #{primary.inspect}"
65
+ end
66
+ end
67
+ end
68
+ else
69
+ if source.try(:id)
70
+ case source
71
+ when CachedResourceFragment then primary_objects.push(@included_objects[source.type][source.id][:object_hash])
72
+ when Resource then primary_objects.push(@included_objects[source.class._type][source.id][:object_hash])
73
+ else raise "Unknown source type #{source.inspect}"
74
+ end
75
+ end
76
+ end
77
+
78
+ included_objects = []
79
+ @included_objects.each_value do |objects|
80
+ objects.each_value do |object|
81
+ unless object[:primary]
82
+ included_objects.push(object[:object_hash])
83
+ end
84
+ end
85
+ end
86
+
87
+ primary_hash = { data: is_resource_collection ? primary_objects : primary_objects[0] }
88
+
89
+ primary_hash[:included] = included_objects if included_objects.size > 0
90
+ primary_hash
91
+ end
92
+
93
+ def serialize_to_links_hash(source, requested_relationship)
94
+ if requested_relationship.is_a?(JSONAPI::Relationship::ToOne)
95
+ data = to_one_linkage(source, requested_relationship)
96
+ else
97
+ data = to_many_linkage(source, requested_relationship)
98
+ end
99
+
100
+ {
101
+ links: {
102
+ self: self_link(source, requested_relationship),
103
+ related: related_link(source, requested_relationship)
104
+ },
105
+ data: data
106
+ }
107
+ end
108
+
109
+ def query_link(query_params)
110
+ link_builder.query_link(query_params)
111
+ end
112
+
113
+ def format_key(key)
114
+ @key_formatter.format(key)
115
+ end
116
+
117
+ def format_value(value, format)
118
+ @value_formatter_type_cache.get(format).format(value)
119
+ end
120
+
121
+ def config_key(resource_klass)
122
+ @_config_keys.fetch resource_klass do
123
+ desc = self.config_description(resource_klass).map(&:inspect).join(",")
124
+ key = JSONAPI.configuration.resource_cache_digest_function.call(desc)
125
+ @_config_keys[resource_klass] = "SRLZ-#{key}"
126
+ end
127
+ end
128
+
129
+ def config_description(resource_klass)
130
+ {
131
+ class_name: self.class.name,
132
+ seriserialization_options: serialization_options.sort.map(&:as_json),
133
+ supplying_attribute_fields: supplying_attribute_fields(resource_klass).sort,
134
+ supplying_relationship_fields: supplying_relationship_fields(resource_klass).sort,
135
+ link_builder_base_url: link_builder.base_url,
136
+ route_formatter_class: link_builder.route_formatter.uncached.class.name,
137
+ key_formatter_class: key_formatter.uncached.class.name,
138
+ always_include_to_one_linkage_data: always_include_to_one_linkage_data,
139
+ always_include_to_many_linkage_data: always_include_to_many_linkage_data
140
+ }
141
+ end
142
+
143
+ # Returns a serialized hash for the source model
144
+ def object_hash(source, include_directives = {})
145
+ obj_hash = {}
146
+
147
+ if source.is_a?(JSONAPI::CachedResourceFragment)
148
+ obj_hash['id'] = source.id
149
+ obj_hash['type'] = source.type
150
+
151
+ obj_hash['links'] = source.links_json if source.links_json
152
+ obj_hash['attributes'] = source.attributes_json if source.attributes_json
153
+
154
+ relationships = cached_relationships_hash(source, include_directives)
155
+ obj_hash['relationships'] = relationships unless relationships.empty?
156
+
157
+ obj_hash['meta'] = source.meta_json if source.meta_json
158
+ else
159
+ fetchable_fields = Set.new(source.fetchable_fields)
160
+
161
+ # TODO Should this maybe be using @id_formatter instead, for consistency?
162
+ id_format = source.class._attribute_options(:id)[:format]
163
+ # protect against ids that were declared as an attribute, but did not have a format set.
164
+ id_format = 'id' if id_format == :default
165
+ obj_hash['id'] = format_value(source.id, id_format)
166
+
167
+ obj_hash['type'] = format_key(source.class._type.to_s)
168
+
169
+ links = links_hash(source)
170
+ obj_hash['links'] = links unless links.empty?
171
+
172
+ attributes = attributes_hash(source, fetchable_fields)
173
+ obj_hash['attributes'] = attributes unless attributes.empty?
174
+
175
+ relationships = relationships_hash(source, fetchable_fields, include_directives)
176
+ obj_hash['relationships'] = relationships unless relationships.nil? || relationships.empty?
177
+
178
+ meta = meta_hash(source)
179
+ obj_hash['meta'] = meta unless meta.empty?
180
+ end
181
+
182
+ obj_hash
183
+ end
184
+
185
+ private
186
+
187
+ # Process the primary source object(s). This will then serialize associated object recursively based on the
188
+ # requested includes. Fields are controlled fields option for each resource type, such
189
+ # as fields: { people: [:id, :email, :comments], posts: [:id, :title, :author], comments: [:id, :body, :post]}
190
+ # The fields options controls both fields and included links references.
191
+ def process_source_objects(source, include_directives)
192
+ if source.respond_to?(:to_ary)
193
+ source.each { |resource| process_source_objects(resource, include_directives) }
194
+ else
195
+ return {} if source.nil?
196
+ add_resource(source, include_directives, true)
197
+ end
198
+ end
199
+
200
+ def supplying_attribute_fields(resource_klass)
201
+ @_supplying_attribute_fields.fetch resource_klass do
202
+ attrs = Set.new(resource_klass._attributes.keys.map(&:to_sym))
203
+ cur = resource_klass
204
+ while cur != JSONAPI::Resource
205
+ if @fields.has_key?(cur._type)
206
+ attrs &= @fields[cur._type]
207
+ break
208
+ end
209
+ cur = cur.superclass
210
+ end
211
+ @_supplying_attribute_fields[resource_klass] = attrs
212
+ end
213
+ end
214
+
215
+ def supplying_relationship_fields(resource_klass)
216
+ @_supplying_relationship_fields.fetch resource_klass do
217
+ relationships = Set.new(resource_klass._relationships.keys.map(&:to_sym))
218
+ cur = resource_klass
219
+ while cur != JSONAPI::Resource
220
+ if @fields.has_key?(cur._type)
221
+ relationships &= @fields[cur._type]
222
+ break
223
+ end
224
+ cur = cur.superclass
225
+ end
226
+ @_supplying_relationship_fields[resource_klass] = relationships
227
+ end
228
+ end
229
+
230
+ def attributes_hash(source, fetchable_fields)
231
+ fields = fetchable_fields & supplying_attribute_fields(source.class)
232
+ fields.each_with_object({}) do |name, hash|
233
+ unless name == :id
234
+ format = source.class._attribute_options(name)[:format]
235
+ hash[format_key(name)] = format_value(source.public_send(name), format)
236
+ end
237
+ end
238
+ end
239
+
240
+ def custom_generation_options
241
+ {
242
+ serializer: self,
243
+ serialization_options: @serialization_options
244
+ }
245
+ end
246
+
247
+ def meta_hash(source)
248
+ meta = source.meta(custom_generation_options)
249
+ (meta.is_a?(Hash) && meta) || {}
250
+ end
251
+
252
+ def links_hash(source)
253
+ links = custom_links_hash(source)
254
+ links[:self] = link_builder.self_link(source) unless links.key?(:self)
255
+ links.compact
256
+ end
257
+
258
+ def custom_links_hash(source)
259
+ custom_links = source.custom_links(custom_generation_options)
260
+ (custom_links.is_a?(Hash) && custom_links) || {}
261
+ end
262
+
263
+ def top_level_source_key(source)
264
+ case source
265
+ when CachedResourceFragment then "#{source.resource_klass}_#{source.id}"
266
+ when Resource then "#{source.class}_#{@id_formatter.format(source.id)}"
267
+ else raise "Unknown source type #{source.inspect}"
268
+ end
269
+ end
270
+
271
+ def self_referential_and_already_in_source(resource)
272
+ resource && @top_level_sources.include?(top_level_source_key(resource))
273
+ end
274
+
275
+ def relationships_hash(source, fetchable_fields, include_directives = {})
276
+ if source.is_a?(CachedResourceFragment)
277
+ return cached_relationships_hash(source, include_directives)
278
+ end
279
+
280
+ include_directives[:include_related] ||= {}
281
+
282
+ relationships = source.class._relationships.select{|k,v| fetchable_fields.include?(k) }
283
+ field_set = supplying_relationship_fields(source.class) & relationships.keys
284
+
285
+ relationships.each_with_object({}) do |(name, relationship), hash|
286
+ ia = include_directives[:include_related][name]
287
+ include_linkage = ia && ia[:include]
288
+ include_linked_children = ia && !ia[:include_related].empty?
289
+
290
+ if field_set.include?(name)
291
+ hash[format_key(name)] = link_object(source, relationship, include_linkage)
292
+ end
293
+
294
+ # If the object has been serialized once it will be in the related objects list,
295
+ # but it's possible all children won't have been captured. So we must still go
296
+ # through the relationships.
297
+ if include_linkage || include_linked_children
298
+ resources = if source.preloaded_fragments.has_key?(format_key(name))
299
+ source.preloaded_fragments[format_key(name)].values
300
+ else
301
+ [source.public_send(name)].flatten(1).compact
302
+ end
303
+ resources.each do |resource|
304
+ next if self_referential_and_already_in_source(resource)
305
+ id = resource.id
306
+ relationships_only = already_serialized?(relationship.type, id)
307
+ if include_linkage && !relationships_only
308
+ add_resource(resource, ia)
309
+ elsif include_linked_children || relationships_only
310
+ relationships_hash(resource, fetchable_fields, ia)
311
+ end
312
+ end
313
+ end
314
+ end
315
+ end
316
+
317
+ def cached_relationships_hash(source, include_directives)
318
+ h = source.relationships || {}
319
+ return h unless include_directives.has_key?(:include_related)
320
+
321
+ relationships = source.resource_klass._relationships.select do |k,v|
322
+ source.fetchable_fields.include?(k)
323
+ end
324
+
325
+ real_res = nil
326
+ relationships.each do |rel_name, relationship|
327
+ key = @key_formatter.format(rel_name)
328
+ to_many = relationship.is_a? JSONAPI::Relationship::ToMany
329
+
330
+ ia = include_directives[:include_related][rel_name]
331
+ if ia
332
+ if h.has_key?(key)
333
+ h[key][:data] = to_many ? [] : nil
334
+ end
335
+
336
+ fragments = source.preloaded_fragments[key]
337
+ if fragments.nil?
338
+ # The resources we want were not preloaded, we'll have to bypass the cache.
339
+ # This happens when including through belongs_to polymorphic relationships
340
+ if real_res.nil?
341
+ real_res = source.to_real_resource
342
+ end
343
+ relation_resources = [real_res.public_send(rel_name)].flatten(1).compact
344
+ fragments = relation_resources.map{|r| [r.id, r]}.to_h
345
+ end
346
+ fragments.each do |id, f|
347
+ add_resource(f, ia)
348
+
349
+ if h.has_key?(key)
350
+ # The hash already has everything we need except the :data field
351
+ data = {
352
+ type: format_key(f.is_a?(Resource) ? f.class._type : f.type),
353
+ id: @id_formatter.format(id)
354
+ }
355
+
356
+ if to_many
357
+ h[key][:data] << data
358
+ else
359
+ h[key][:data] = data
360
+ end
361
+ end
362
+ end
363
+ end
364
+ end
365
+
366
+ return h
367
+ end
368
+
369
+ def already_serialized?(type, id)
370
+ type = format_key(type)
371
+ id = @id_formatter.format(id)
372
+ @included_objects.key?(type) && @included_objects[type].key?(id)
373
+ end
374
+
375
+ def self_link(source, relationship)
376
+ link_builder.relationships_self_link(source, relationship)
377
+ end
378
+
379
+ def related_link(source, relationship)
380
+ link_builder.relationships_related_link(source, relationship)
381
+ end
382
+
383
+ def to_one_linkage(source, relationship)
384
+ linkage_id = foreign_key_value(source, relationship)
385
+ linkage_type = format_key(relationship.type_for_source(source))
386
+ return unless linkage_id.present? && linkage_type.present?
387
+
388
+ {
389
+ type: linkage_type,
390
+ id: linkage_id,
391
+ }
392
+ end
393
+
394
+ def to_many_linkage(source, relationship)
395
+ linkage = []
396
+ linkage_types_and_values = if source.preloaded_fragments.has_key?(format_key(relationship.name))
397
+ source.preloaded_fragments[format_key(relationship.name)].map do |_, resource|
398
+ [relationship.type, resource.id]
399
+ end
400
+ elsif relationship.polymorphic?
401
+ assoc = source._model.public_send(relationship.name)
402
+ # Avoid hitting the database again for values already pre-loaded
403
+ if assoc.respond_to?(:loaded?) and assoc.loaded?
404
+ assoc.map do |obj|
405
+ [obj.type.underscore.pluralize, obj.id]
406
+ end
407
+ else
408
+ assoc.pluck(:type, :id).map do |type, id|
409
+ [type.underscore.pluralize, id]
410
+ end
411
+ end
412
+ else
413
+ source.public_send(relationship.name).map do |value|
414
+ [relationship.type, value.id]
415
+ end
416
+ end
417
+
418
+ linkage_types_and_values.each do |type, value|
419
+ if type && value
420
+ linkage.append({type: format_key(type), id: @id_formatter.format(value)})
421
+ end
422
+ end
423
+ linkage
424
+ end
425
+
426
+ def link_object_to_one(source, relationship, include_linkage)
427
+ include_linkage = include_linkage | @always_include_to_one_linkage_data | relationship.always_include_linkage_data
428
+ link_object_hash = {}
429
+ link_object_hash[:links] = {}
430
+ link_object_hash[:links][:self] = self_link(source, relationship)
431
+ link_object_hash[:links][:related] = related_link(source, relationship)
432
+ link_object_hash[:data] = to_one_linkage(source, relationship) if include_linkage
433
+ link_object_hash
434
+ end
435
+
436
+ def link_object_to_many(source, relationship, include_linkage)
437
+ include_linkage = include_linkage | relationship.always_include_linkage_data
438
+ link_object_hash = {}
439
+ link_object_hash[:links] = {}
440
+ link_object_hash[:links][:self] = self_link(source, relationship)
441
+ link_object_hash[:links][:related] = related_link(source, relationship)
442
+ link_object_hash[:data] = to_many_linkage(source, relationship) if include_linkage
443
+ link_object_hash
444
+ end
445
+
446
+ def link_object(source, relationship, include_linkage = false)
447
+ if relationship.is_a?(JSONAPI::Relationship::ToOne)
448
+ link_object_to_one(source, relationship, include_linkage)
449
+ elsif relationship.is_a?(JSONAPI::Relationship::ToMany)
450
+ link_object_to_many(source, relationship, include_linkage)
451
+ end
452
+ end
453
+
454
+ # Extracts the foreign key value for a to_one relationship.
455
+ def foreign_key_value(source, relationship)
456
+ related_resource_id = if source.preloaded_fragments.has_key?(format_key(relationship.name))
457
+ source.preloaded_fragments[format_key(relationship.name)].values.first.try(:id)
458
+ elsif source.respond_to?("#{relationship.name}_id")
459
+ # If you have direct access to the underlying id, you don't have to load the relationship
460
+ # which can save quite a lot of time when loading a lot of data.
461
+ # This does not apply to e.g. has_one :through relationships.
462
+ source.public_send("#{relationship.name}_id")
463
+ else
464
+ source.public_send(relationship.name).try(:id)
465
+ end
466
+ return nil unless related_resource_id
467
+ @id_formatter.format(related_resource_id)
468
+ end
469
+
470
+ def foreign_key_types_and_values(source, relationship)
471
+ if relationship.is_a?(JSONAPI::Relationship::ToMany)
472
+ if relationship.polymorphic?
473
+ assoc = source._model.public_send(relationship.name)
474
+ # Avoid hitting the database again for values already pre-loaded
475
+ if assoc.respond_to?(:loaded?) and assoc.loaded?
476
+ assoc.map do |obj|
477
+ [obj.type.underscore.pluralize, @id_formatter.format(obj.id)]
478
+ end
479
+ else
480
+ assoc.pluck(:type, :id).map do |type, id|
481
+ [type.underscore.pluralize, @id_formatter.format(id)]
482
+ end
483
+ end
484
+ else
485
+ source.public_send(relationship.name).map do |value|
486
+ [relationship.type, @id_formatter.format(value.id)]
487
+ end
488
+ end
489
+ end
490
+ end
491
+
492
+ # Sets that an object should be included in the primary document of the response.
493
+ def set_primary(type, id)
494
+ type = format_key(type)
495
+ @included_objects[type][id][:primary] = true
496
+ end
497
+
498
+ def add_resource(source, include_directives, primary = false)
499
+ type = source.is_a?(JSONAPI::CachedResourceFragment) ? source.type : source.class._type
500
+ id = source.id
501
+
502
+ @included_objects[type] ||= {}
503
+ existing = @included_objects[type][id]
504
+
505
+ if existing.nil?
506
+ obj_hash = object_hash(source, include_directives)
507
+ @included_objects[type][id] = {
508
+ primary: primary,
509
+ object_hash: obj_hash,
510
+ includes: Set.new(include_directives[:include_related].keys)
511
+ }
512
+ else
513
+ include_related = Set.new(include_directives[:include_related].keys)
514
+ unless existing[:includes].superset?(include_related)
515
+ obj_hash = object_hash(source, include_directives)
516
+ @included_objects[type][id][:object_hash].deep_merge!(obj_hash)
517
+ @included_objects[type][id][:includes].add(include_related)
518
+ @included_objects[type][id][:primary] = existing[:primary] | primary
519
+ end
520
+ end
521
+ end
522
+
523
+ def generate_link_builder(primary_resource_klass, options)
524
+ LinkBuilder.new(
525
+ base_url: options.fetch(:base_url, ''),
526
+ route_formatter: options.fetch(:route_formatter, JSONAPI.configuration.route_formatter),
527
+ primary_resource_klass: primary_resource_klass,
528
+ )
529
+ end
530
+ end
531
+ end
@@ -0,0 +1,5 @@
1
+ module JSONAPI
2
+ module Resources
3
+ VERSION = '0.1.0'
4
+ end
5
+ end
@@ -0,0 +1,135 @@
1
+ module JSONAPI
2
+ class ResponseDocument
3
+ def initialize(operation_results, serializer, options = {})
4
+ @operation_results = operation_results
5
+ @serializer = serializer
6
+ @options = options
7
+
8
+ @key_formatter = @options.fetch(:key_formatter, JSONAPI.configuration.key_formatter)
9
+ end
10
+
11
+ def contents
12
+ hash = results_to_hash
13
+
14
+ meta = top_level_meta
15
+ hash.merge!(meta: meta) unless meta.empty?
16
+
17
+ links = top_level_links
18
+ hash.merge!(links: links) unless links.empty?
19
+
20
+ hash
21
+ end
22
+
23
+ def status
24
+ if @operation_results.has_errors?
25
+ @operation_results.all_errors[0].status
26
+ else
27
+ @operation_results.results[0].code
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ # Rolls up the top level meta data from the base_meta, the set of operations,
34
+ # and the result of each operation. The keys are then formatted.
35
+ def top_level_meta
36
+ meta = @options.fetch(:base_meta, {})
37
+
38
+ meta.merge!(@operation_results.meta)
39
+
40
+ @operation_results.results.each do |result|
41
+ meta.merge!(result.meta)
42
+
43
+ if JSONAPI.configuration.top_level_meta_include_record_count && result.respond_to?(:record_count)
44
+ meta[JSONAPI.configuration.top_level_meta_record_count_key] = result.record_count
45
+ end
46
+
47
+ if JSONAPI.configuration.top_level_meta_include_page_count && result.respond_to?(:page_count)
48
+ meta[JSONAPI.configuration.top_level_meta_page_count_key] = result.page_count
49
+ end
50
+ end
51
+
52
+ meta.as_json.deep_transform_keys { |key| @key_formatter.format(key) }
53
+ end
54
+
55
+ # Rolls up the top level links from the base_links, the set of operations,
56
+ # and the result of each operation. The keys are then formatted.
57
+ def top_level_links
58
+ links = @options.fetch(:base_links, {})
59
+
60
+ links.merge!(@operation_results.links)
61
+
62
+ @operation_results.results.each do |result|
63
+ links.merge!(result.links)
64
+
65
+ # Build pagination links
66
+ if result.is_a?(JSONAPI::ResourcesOperationResult) || result.is_a?(JSONAPI::RelatedResourcesOperationResult)
67
+ result.pagination_params.each_pair do |link_name, params|
68
+ if result.is_a?(JSONAPI::RelatedResourcesOperationResult)
69
+ relationship = result.source_resource.class._relationships[result._type.to_sym]
70
+ links[link_name] = @serializer.link_builder.relationships_related_link(result.source_resource, relationship, query_params(params))
71
+ else
72
+ links[link_name] = @serializer.query_link(query_params(params))
73
+ end
74
+ end
75
+ end
76
+ end
77
+
78
+ links.deep_transform_keys { |key| @key_formatter.format(key) }
79
+ end
80
+
81
+ def query_params(params)
82
+ query_params = {}
83
+ query_params[:page] = params
84
+
85
+ request = @options[:request]
86
+ if request.params[:fields]
87
+ query_params[:fields] = request.params[:fields].respond_to?(:to_unsafe_hash) ? request.params[:fields].to_unsafe_hash : request.params[:fields]
88
+ end
89
+
90
+ query_params[:include] = request.params[:include] if request.params[:include]
91
+ query_params[:sort] = request.params[:sort] if request.params[:sort]
92
+
93
+ if request.params[:filter]
94
+ query_params[:filter] = request.params[:filter].respond_to?(:to_unsafe_hash) ? request.params[:filter].to_unsafe_hash : request.params[:filter]
95
+ end
96
+
97
+ query_params
98
+ end
99
+
100
+ def results_to_hash
101
+ if @operation_results.has_errors?
102
+ { errors: @operation_results.all_errors }
103
+ else
104
+ if @operation_results.results.length == 1
105
+ result = @operation_results.results[0]
106
+
107
+ case result
108
+ when JSONAPI::ResourceOperationResult
109
+ @serializer.serialize_to_hash(result.resource)
110
+ when JSONAPI::ResourcesOperationResult
111
+ @serializer.serialize_to_hash(result.resources)
112
+ when JSONAPI::LinksObjectOperationResult
113
+ @serializer.serialize_to_links_hash(result.parent_resource,
114
+ result.relationship)
115
+ when JSONAPI::OperationResult
116
+ {}
117
+ end
118
+
119
+ elsif @operation_results.results.length > 1
120
+ resources = []
121
+ @operation_results.results.each do |result|
122
+ case result
123
+ when JSONAPI::ResourceOperationResult
124
+ resources.push(result.resource)
125
+ when JSONAPI::ResourcesOperationResult
126
+ resources.concat(result.resources)
127
+ end
128
+ end
129
+
130
+ @serializer.serialize_to_hash(resources)
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end