jsonapi-resources 0.9.12 → 0.10.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) 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-resources.rb +8 -3
  7. data/lib/jsonapi/active_relation_resource_finder.rb +640 -0
  8. data/lib/jsonapi/active_relation_resource_finder/join_tree.rb +126 -0
  9. data/lib/jsonapi/acts_as_resource_controller.rb +121 -106
  10. data/lib/jsonapi/{cached_resource_fragment.rb → cached_response_fragment.rb} +13 -30
  11. data/lib/jsonapi/compiled_json.rb +11 -1
  12. data/lib/jsonapi/configuration.rb +44 -18
  13. data/lib/jsonapi/error.rb +27 -0
  14. data/lib/jsonapi/exceptions.rb +43 -40
  15. data/lib/jsonapi/formatter.rb +3 -3
  16. data/lib/jsonapi/include_directives.rb +2 -45
  17. data/lib/jsonapi/link_builder.rb +87 -80
  18. data/lib/jsonapi/operation.rb +16 -5
  19. data/lib/jsonapi/operation_result.rb +74 -16
  20. data/lib/jsonapi/processor.rb +233 -112
  21. data/lib/jsonapi/relationship.rb +77 -53
  22. data/lib/jsonapi/request_parser.rb +378 -423
  23. data/lib/jsonapi/resource.rb +224 -524
  24. data/lib/jsonapi/resource_controller_metal.rb +2 -2
  25. data/lib/jsonapi/resource_fragment.rb +47 -0
  26. data/lib/jsonapi/resource_id_tree.rb +112 -0
  27. data/lib/jsonapi/resource_identity.rb +42 -0
  28. data/lib/jsonapi/resource_serializer.rb +133 -301
  29. data/lib/jsonapi/resource_set.rb +108 -0
  30. data/lib/jsonapi/resources/version.rb +1 -1
  31. data/lib/jsonapi/response_document.rb +100 -88
  32. data/lib/jsonapi/routing_ext.rb +21 -43
  33. metadata +29 -45
  34. data/lib/jsonapi/operation_dispatcher.rb +0 -88
  35. data/lib/jsonapi/operation_results.rb +0 -35
  36. data/lib/jsonapi/relationship_builder.rb +0 -167
@@ -5,10 +5,10 @@ module JSONAPI
5
5
  ActionController::Rendering,
6
6
  ActionController::Renderers::All,
7
7
  ActionController::StrongParameters,
8
- Gem::Requirement.new('< 6.1').satisfied_by?(ActionPack.gem_version) ? ActionController::ForceSSL : nil,
8
+ ActionController::ForceSSL,
9
9
  ActionController::Instrumentation,
10
10
  JSONAPI::ActsAsResourceController
11
- ].compact.freeze
11
+ ].freeze
12
12
 
13
13
  MODULES.each do |mod|
14
14
  include mod
@@ -0,0 +1,47 @@
1
+ module JSONAPI
2
+
3
+ # A ResourceFragment holds a ResourceIdentity and associated partial resource data.
4
+ #
5
+ # The following partial resource data may be stored
6
+ # cache - the value of the cache field for the resource instance
7
+ # related - a hash of arrays of related resource identities, grouped by relationship name
8
+ # related_from - a set of related resource identities that loaded the fragment
9
+ #
10
+ # Todo: optionally use these for faster responses by bypassing model instantiation)
11
+ # attributes - resource attributes
12
+
13
+ class ResourceFragment
14
+ attr_reader :identity, :attributes, :related_from, :related
15
+
16
+ attr_accessor :primary, :cache
17
+
18
+ alias :cache_field :cache #ToDo: Rename one or the other
19
+
20
+ def initialize(identity)
21
+ @identity = identity
22
+ @cache = nil
23
+ @attributes = {}
24
+ @related = {}
25
+ @primary = false
26
+ @related_from = Set.new
27
+ end
28
+
29
+ def initialize_related(relationship_name)
30
+ @related ||= {}
31
+ @related[relationship_name.to_sym] ||= Set.new
32
+ end
33
+
34
+ def add_related_identity(relationship_name, identity)
35
+ initialize_related(relationship_name)
36
+ @related[relationship_name.to_sym] << identity
37
+ end
38
+
39
+ def add_related_from(identity)
40
+ @related_from << identity
41
+ end
42
+
43
+ def add_attribute(name, value)
44
+ @attributes[name] = value
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,112 @@
1
+ module JSONAPI
2
+
3
+ # A tree structure representing the resource structure of the requested resource(s). This is an intermediate structure
4
+ # used to keep track of the resources, by identity, found at different included relationships. It will be flattened and
5
+ # the resource instances will be fetched from the cache or the record store.
6
+ class ResourceIdTree
7
+
8
+ attr_reader :fragments, :related_resource_id_trees
9
+
10
+ # Gets the related Resource Id Tree for a relationship, and creates it first if it does not exist
11
+ #
12
+ # @param relationship [JSONAPI::Relationship]
13
+ #
14
+ # @return [JSONAPI::RelatedResourceIdTree] the new or existing resource id tree for the requested relationship
15
+ def fetch_related_resource_id_tree(relationship)
16
+ relationship_name = relationship.name.to_sym
17
+ @related_resource_id_trees[relationship_name] ||= RelatedResourceIdTree.new(relationship, self)
18
+ end
19
+
20
+ private
21
+
22
+ def init_included_relationships(fragment, include_related)
23
+ include_related && include_related.each_key do |relationship_name|
24
+ fragment.initialize_related(relationship_name)
25
+ end
26
+ end
27
+ end
28
+
29
+ class PrimaryResourceIdTree < ResourceIdTree
30
+
31
+ # Creates a PrimaryResourceIdTree with no resources and no related ResourceIdTrees
32
+ def initialize
33
+ @fragments ||= {}
34
+ @related_resource_id_trees ||= {}
35
+ end
36
+
37
+ # Adds each Resource Fragment to the Resources hash
38
+ #
39
+ # @param fragments [Hash]
40
+ # @param include_related [Hash]
41
+ #
42
+ # @return [null]
43
+ def add_resource_fragments(fragments, include_related)
44
+ fragments.each_value do |fragment|
45
+ add_resource_fragment(fragment, include_related)
46
+ end
47
+ end
48
+
49
+ # Adds a Resource Fragment to the Resources hash
50
+ #
51
+ # @param fragment [JSONAPI::ResourceFragment]
52
+ # @param include_related [Hash]
53
+ #
54
+ # @return [null]
55
+ def add_resource_fragment(fragment, include_related)
56
+ fragment.primary = true
57
+
58
+ init_included_relationships(fragment, include_related)
59
+
60
+ @fragments[fragment.identity] = fragment
61
+ end
62
+ end
63
+
64
+ class RelatedResourceIdTree < ResourceIdTree
65
+
66
+ attr_reader :parent_relationship, :source_resource_id_tree
67
+
68
+ # Creates a RelatedResourceIdTree with no resources and no related ResourceIdTrees. A connection to the parent
69
+ # ResourceIdTree is maintained.
70
+ #
71
+ # @param parent_relationship [JSONAPI::Relationship]
72
+ # @param source_resource_id_tree [JSONAPI::ResourceIdTree]
73
+ #
74
+ # @return [JSONAPI::RelatedResourceIdTree] the new or existing resource id tree for the requested relationship
75
+ def initialize(parent_relationship, source_resource_id_tree)
76
+ @fragments ||= {}
77
+ @related_resource_id_trees ||= {}
78
+
79
+ @parent_relationship = parent_relationship
80
+ @parent_relationship_name = parent_relationship.name.to_sym
81
+ @source_resource_id_tree = source_resource_id_tree
82
+ end
83
+
84
+ # Adds each Resource Fragment to the Resources hash
85
+ #
86
+ # @param fragments [Hash]
87
+ # @param include_related [Hash]
88
+ #
89
+ # @return [null]
90
+ def add_resource_fragments(fragments, include_related)
91
+ fragments.each_value do |fragment|
92
+ add_resource_fragment(fragment, include_related)
93
+ end
94
+ end
95
+
96
+ # Adds a Resource Fragment to the fragments hash
97
+ #
98
+ # @param fragment [JSONAPI::ResourceFragment]
99
+ # @param include_related [Hash]
100
+ #
101
+ # @return [null]
102
+ def add_resource_fragment(fragment, include_related)
103
+ init_included_relationships(fragment, include_related)
104
+
105
+ fragment.related_from.each do |rid|
106
+ @source_resource_id_tree.fragments[rid].add_related_identity(parent_relationship.name, fragment.identity)
107
+ end
108
+
109
+ @fragments[fragment.identity] = fragment
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,42 @@
1
+ module JSONAPI
2
+
3
+ # ResourceIdentity describes a unique identity of a resource in the system.
4
+ # This consists of a Resource class and an identifier that is unique within
5
+ # that Resource class. ResourceIdentities are intended to be used as hash
6
+ # keys to provide ordered mixing of resource types in result sets.
7
+ #
8
+ #
9
+ # == Creating a ResourceIdentity
10
+ #
11
+ # rid = ResourceIdentity.new(PostResource, 12)
12
+ #
13
+ class ResourceIdentity
14
+ attr_reader :resource_klass, :id
15
+
16
+ def initialize(resource_klass, id)
17
+ @resource_klass = resource_klass
18
+ @id = id
19
+ end
20
+
21
+ def ==(other)
22
+ # :nocov:
23
+ eql?(other)
24
+ # :nocov:
25
+ end
26
+
27
+ def eql?(other)
28
+ other.is_a?(ResourceIdentity) && other.resource_klass == @resource_klass && other.id == @id
29
+ end
30
+
31
+ def hash
32
+ [@resource_klass, @id].hash
33
+ end
34
+
35
+ # Creates a string representation of the identifier.
36
+ def to_s
37
+ # :nocov:
38
+ "#{resource_klass}:#{id}"
39
+ # :nocov:
40
+ end
41
+ end
42
+ end
@@ -1,7 +1,7 @@
1
1
  module JSONAPI
2
2
  class ResourceSerializer
3
3
 
4
- attr_reader :link_builder, :key_formatter, :serialization_options, :primary_class_name,
4
+ attr_reader :link_builder, :key_formatter, :serialization_options,
5
5
  :fields, :include_directives, :always_include_to_one_linkage_data,
6
6
  :always_include_to_many_linkage_data
7
7
 
@@ -19,7 +19,6 @@ module JSONAPI
19
19
 
20
20
  def initialize(primary_resource_klass, options = {})
21
21
  @primary_resource_klass = primary_resource_klass
22
- @primary_class_name = primary_resource_klass._type
23
22
  @fields = options.fetch(:fields, {})
24
23
  @include = options.fetch(:include, [])
25
24
  @include_directives = options[:include_directives]
@@ -42,67 +41,72 @@ module JSONAPI
42
41
  @_supplying_relationship_fields = {}
43
42
  end
44
43
 
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)
44
+ # Converts a resource_set to a hash, conforming to the JSONAPI structure
45
+ def serialize_resource_set_to_hash_single(resource_set)
54
46
 
55
47
  primary_objects = []
48
+ included_objects = []
56
49
 
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}"
50
+ resource_set.resource_klasses.each_value do |resource_klass|
51
+ resource_klass.each_value do |resource|
52
+ serialized_resource = object_hash(resource[:resource], resource[:relationships])
53
+
54
+ if resource[:primary]
55
+ primary_objects.push(serialized_resource)
56
+ else
57
+ included_objects.push(serialized_resource)
74
58
  end
75
59
  end
76
60
  end
77
61
 
62
+ fail "To Many primary objects for show" if (primary_objects.count > 1)
63
+ primary_hash = { 'data' => primary_objects[0] }
64
+
65
+ primary_hash['included'] = included_objects if included_objects.size > 0
66
+ primary_hash
67
+ end
68
+
69
+ def serialize_resource_set_to_hash_plural(resource_set)
70
+
71
+ primary_objects = []
78
72
  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])
73
+
74
+ resource_set.resource_klasses.each_value do |resource_klass|
75
+ resource_klass.each_value do |resource|
76
+ serialized_resource = object_hash(resource[:resource], resource[:relationships])
77
+
78
+ if resource[:primary]
79
+ primary_objects.push(serialized_resource)
80
+ else
81
+ included_objects.push(serialized_resource)
83
82
  end
84
83
  end
85
84
  end
86
85
 
87
- primary_hash = { data: is_resource_collection ? primary_objects : primary_objects[0] }
86
+ primary_hash = { 'data' => primary_objects }
88
87
 
89
- primary_hash[:included] = included_objects if included_objects.size > 0
88
+ primary_hash['included'] = included_objects if included_objects.size > 0
90
89
  primary_hash
91
90
  end
92
91
 
93
- def serialize_to_relationship_hash(source, requested_relationship)
92
+ def serialize_related_resource_set_to_hash_plural(resource_set, _source_resource)
93
+ return serialize_resource_set_to_hash_plural(resource_set)
94
+ end
95
+
96
+ def serialize_to_links_hash(source, requested_relationship, resource_ids)
94
97
  if requested_relationship.is_a?(JSONAPI::Relationship::ToOne)
95
- data = to_one_linkage(source, requested_relationship)
98
+ data = to_one_linkage(resource_ids[0])
96
99
  else
97
- data = to_many_linkage(source, requested_relationship)
100
+ data = to_many_linkage(resource_ids)
98
101
  end
99
102
 
100
- rel_hash = { 'data': data }
101
-
102
- links = default_relationship_links(source, requested_relationship)
103
- rel_hash['links'] = links unless links.blank?
104
-
105
- rel_hash
103
+ {
104
+ 'links' => {
105
+ 'self' => self_link(source, requested_relationship),
106
+ 'related' => related_link(source, requested_relationship)
107
+ },
108
+ 'data' => data
109
+ }
106
110
  end
107
111
 
108
112
  def query_link(query_params)
@@ -113,6 +117,10 @@ module JSONAPI
113
117
  @key_formatter.format(key)
114
118
  end
115
119
 
120
+ def unformat_key(key)
121
+ @key_formatter.unformat(key)
122
+ end
123
+
116
124
  def format_value(value, format)
117
125
  @value_formatter_type_cache.get(format).format(value)
118
126
  end
@@ -132,30 +140,35 @@ module JSONAPI
132
140
  supplying_attribute_fields: supplying_attribute_fields(resource_klass).sort,
133
141
  supplying_relationship_fields: supplying_relationship_fields(resource_klass).sort,
134
142
  link_builder_base_url: link_builder.base_url,
143
+ route_formatter_class: link_builder.route_formatter.uncached.class.name,
135
144
  key_formatter_class: key_formatter.uncached.class.name,
136
145
  always_include_to_one_linkage_data: always_include_to_one_linkage_data,
137
146
  always_include_to_many_linkage_data: always_include_to_many_linkage_data
138
147
  }
139
148
  end
140
149
 
141
- # Returns a serialized hash for the source model
142
- def object_hash(source, include_directives = {})
150
+ def object_hash(source, relationship_data)
143
151
  obj_hash = {}
144
152
 
145
- if source.is_a?(JSONAPI::CachedResourceFragment)
146
- obj_hash['id'] = source.id
153
+ return obj_hash if source.nil?
154
+
155
+ fetchable_fields = Set.new(source.fetchable_fields)
156
+
157
+ if source.is_a?(JSONAPI::CachedResponseFragment)
158
+ id_format = source.resource_klass._attribute_options(:id)[:format]
159
+
160
+ id_format = 'id' if id_format == :default
161
+ obj_hash['id'] = format_value(source.id, id_format)
147
162
  obj_hash['type'] = source.type
148
163
 
149
164
  obj_hash['links'] = source.links_json if source.links_json
150
165
  obj_hash['attributes'] = source.attributes_json if source.attributes_json
151
166
 
152
- relationships = cached_relationships_hash(source, include_directives)
153
- obj_hash['relationships'] = relationships unless relationships.blank?
167
+ relationships = cached_relationships_hash(source, fetchable_fields, relationship_data)
168
+ obj_hash['relationships'] = relationships unless relationships.nil? || relationships.empty?
154
169
 
155
170
  obj_hash['meta'] = source.meta_json if source.meta_json
156
171
  else
157
- fetchable_fields = Set.new(source.fetchable_fields)
158
-
159
172
  # TODO Should this maybe be using @id_formatter instead, for consistency?
160
173
  id_format = source.class._attribute_options(:id)[:format]
161
174
  # protect against ids that were declared as an attribute, but did not have a format set.
@@ -170,8 +183,8 @@ module JSONAPI
170
183
  attributes = attributes_hash(source, fetchable_fields)
171
184
  obj_hash['attributes'] = attributes unless attributes.empty?
172
185
 
173
- relationships = relationships_hash(source, fetchable_fields, include_directives)
174
- obj_hash['relationships'] = relationships unless relationships.blank?
186
+ relationships = relationships_hash(source, fetchable_fields, relationship_data)
187
+ obj_hash['relationships'] = relationships unless relationships.nil? || relationships.empty?
175
188
 
176
189
  meta = meta_hash(source)
177
190
  obj_hash['meta'] = meta unless meta.empty?
@@ -182,19 +195,6 @@ module JSONAPI
182
195
 
183
196
  private
184
197
 
185
- # Process the primary source object(s). This will then serialize associated object recursively based on the
186
- # requested includes. Fields are controlled fields option for each resource type, such
187
- # as fields: { people: [:id, :email, :comments], posts: [:id, :title, :author], comments: [:id, :body, :post]}
188
- # The fields options controls both fields and included links references.
189
- def process_source_objects(source, include_directives)
190
- if source.respond_to?(:to_ary)
191
- source.each { |resource| process_source_objects(resource, include_directives) }
192
- else
193
- return {} if source.nil?
194
- add_resource(source, include_directives, true)
195
- end
196
- end
197
-
198
198
  def supplying_attribute_fields(resource_klass)
199
199
  @_supplying_attribute_fields.fetch resource_klass do
200
200
  attrs = Set.new(resource_klass._attributes.keys.map(&:to_sym))
@@ -249,9 +249,7 @@ module JSONAPI
249
249
 
250
250
  def links_hash(source)
251
251
  links = custom_links_hash(source)
252
- if !links.key?('self') && !source.class.exclude_link?(:self)
253
- links['self'] = link_builder.self_link(source)
254
- end
252
+ links['self'] = link_builder.self_link(source) unless links.key?('self')
255
253
  links.compact
256
254
  end
257
255
 
@@ -260,118 +258,61 @@ module JSONAPI
260
258
  (custom_links.is_a?(Hash) && custom_links) || {}
261
259
  end
262
260
 
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) }
261
+ def relationships_hash(source, fetchable_fields, relationship_data)
262
+ relationships = source.class._relationships.select{|k,_v| fetchable_fields.include?(k) }
283
263
  field_set = supplying_relationship_fields(source.class) & relationships.keys
284
264
 
285
265
  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
-
266
+ include_data = false
290
267
  if field_set.include?(name)
291
- ro = relationship_object(source, relationship, include_linkage)
292
- hash[format_key(name)] = ro unless ro.blank?
293
- end
294
-
295
- # If the object has been serialized once it will be in the related objects list,
296
- # but it's possible all children won't have been captured. So we must still go
297
- # through the relationships.
298
- if include_linkage || include_linked_children
299
- resources = if source.preloaded_fragments.has_key?(format_key(name))
300
- source.preloaded_fragments[format_key(name)].values
301
- else
302
- options = { filters: ia && ia[:include_filters] || {} }
303
- [source.public_send(name, options)].flatten(1).compact
304
- end
305
- resources.each do |resource|
306
- next if self_referential_and_already_in_source(resource)
307
- id = resource.id
308
- relationships_only = already_serialized?(relationship.type, id)
309
- if include_linkage && !relationships_only
310
- add_resource(resource, ia)
311
- elsif include_linked_children || relationships_only
312
- relationships_hash(resource, fetchable_fields, ia)
268
+ if relationship_data[name]
269
+ include_data = true
270
+ if relationship.is_a?(JSONAPI::Relationship::ToOne)
271
+ rids = relationship_data[name].first
272
+ else
273
+ rids = relationship_data[name]
313
274
  end
314
275
  end
276
+
277
+ hash[format_key(name)] = link_object(source, relationship, rids, include_data)
315
278
  end
316
279
  end
317
280
  end
318
281
 
319
- def cached_relationships_hash(source, include_directives)
320
- h = source.relationships || {}
321
- return h unless include_directives.has_key?(:include_related)
282
+ def cached_relationships_hash(source, fetchable_fields, relationship_data)
283
+ relationships = {}
322
284
 
323
- relationships = source.resource_klass._relationships.select do |k,v|
324
- source.fetchable_fields.include?(k)
285
+ source.relationships.try(:each_pair) do |k,v|
286
+ if fetchable_fields.include?(unformat_key(k).to_sym)
287
+ relationships[k.to_sym] = v
288
+ end
325
289
  end
326
290
 
327
- real_res = nil
328
- relationships.each do |rel_name, relationship|
329
- key = @key_formatter.format(rel_name)
330
- to_many = relationship.is_a? JSONAPI::Relationship::ToMany
291
+ field_set = supplying_relationship_fields(source.resource_klass).collect {|k| format_key(k).to_sym } & relationships.keys
331
292
 
332
- ia = include_directives[:include_related][rel_name]
333
- if ia
334
- if h.has_key?(key)
335
- h[key][:data] = to_many ? [] : nil
336
- end
293
+ relationships.each_with_object({}) do |(name, relationship), hash|
294
+ if field_set.include?(name)
295
+
296
+ relationship_name = unformat_key(name).to_sym
297
+ relationship_klass = source.resource_klass._relationships[relationship_name]
337
298
 
338
- fragments = source.preloaded_fragments[key]
339
- if fragments.nil?
340
- # The resources we want were not preloaded, we'll have to bypass the cache.
341
- # This happens when including through belongs_to polymorphic relationships
342
- if real_res.nil?
343
- real_res = source.to_real_resource
299
+ if relationship_klass.is_a?(JSONAPI::Relationship::ToOne)
300
+ # include_linkage = @always_include_to_one_linkage_data | relationship_klass.always_include_linkage_data
301
+ if relationship_data[relationship_name]
302
+ rids = relationship_data[relationship_name].first
303
+ relationship['data'] = to_one_linkage(rids)
344
304
  end
345
- relation_resources = [real_res.public_send(rel_name)].flatten(1).compact
346
- fragments = relation_resources.map{|r| [r.id, r]}.to_h
347
- end
348
- fragments.each do |id, f|
349
- add_resource(f, ia)
350
-
351
- if h.has_key?(key)
352
- # The hash already has everything we need except the :data field
353
- data = {
354
- type: format_key(f.is_a?(Resource) ? f.class._type : f.type),
355
- id: @id_formatter.format(id)
356
- }
357
-
358
- if to_many
359
- h[key][:data] << data
360
- else
361
- h[key][:data] = data
362
- end
305
+ else
306
+ # include_linkage = relationship_klass.always_include_linkage_data
307
+ if relationship_data[relationship_name]
308
+ rids = relationship_data[relationship_name]
309
+ relationship['data'] = to_many_linkage(rids)
363
310
  end
364
311
  end
312
+
313
+ hash[format_key(name)] = relationship
365
314
  end
366
315
  end
367
-
368
- return h
369
- end
370
-
371
- def already_serialized?(type, id)
372
- type = format_key(type)
373
- id = @id_formatter.format(id)
374
- @included_objects.key?(type) && @included_objects[type].key?(id)
375
316
  end
376
317
 
377
318
  def self_link(source, relationship)
@@ -382,169 +323,60 @@ module JSONAPI
382
323
  link_builder.relationships_related_link(source, relationship)
383
324
  end
384
325
 
385
- def default_relationship_links(source, relationship)
386
- links = {}
387
- links['self'] = self_link(source, relationship) unless relationship.exclude_link?(:self)
388
- links['related'] = related_link(source, relationship) unless relationship.exclude_link?(:related)
389
- links.compact
390
- end
391
-
392
- def to_one_linkage(source, relationship)
393
- linkage_id = foreign_key_value(source, relationship)
394
- linkage_type = format_key(relationship.type_for_source(source))
395
- return unless linkage_id.present? && linkage_type.present?
396
-
397
- {
398
- type: linkage_type,
399
- id: linkage_id,
400
- }
401
- end
402
-
403
- def to_many_linkage(source, relationship)
326
+ def to_many_linkage(rids)
404
327
  linkage = []
405
- include_config = include_directives.include_config(relationship.name.to_sym) if include_directives
406
- include_filters = include_config[:include_filters] if include_config
407
- options = { filters: include_filters || {} }
408
328
 
409
- linkage_types_and_values = if source.preloaded_fragments.has_key?(format_key(relationship.name))
410
- source.preloaded_fragments[format_key(relationship.name)].map do |_, resource|
411
- [relationship.type, resource.id]
412
- end
413
- elsif relationship.polymorphic?
414
- assoc = source.public_send("records_for_#{relationship.name}", options)
415
- # Avoid hitting the database again for values already pre-loaded
416
- if assoc.respond_to?(:loaded?) and assoc.loaded?
417
- assoc.map do |obj|
418
- [obj.type.underscore.pluralize, obj.id]
419
- end
420
- else
421
- assoc = assoc.unscope(:includes) if assoc.is_a?(ActiveRecord::Relation)
422
- assoc.pluck(:type, :id).map do |type, id|
423
- [type.underscore.pluralize, id]
424
- end
425
- end
426
- else
427
- source.public_send(relationship.name, options).map do |value|
428
- [relationship.type, value.id]
329
+ rids && rids.each do |details|
330
+ id = details.id
331
+ type = details.resource_klass.try(:_type)
332
+ if type && id
333
+ linkage.append({'type' => format_key(type), 'id' => @id_formatter.format(id)})
429
334
  end
430
335
  end
431
336
 
432
- linkage_types_and_values.each do |type, value|
433
- if type && value
434
- linkage.append({type: format_key(type), id: @id_formatter.format(value)})
435
- end
436
- end
437
337
  linkage
438
338
  end
439
339
 
440
- def relationship_object_to_one(source, relationship, include_linkage)
441
- include_linkage = include_linkage | @always_include_to_one_linkage_data | relationship.always_include_linkage_data
442
- relationship_object_hash = {}
443
-
444
- links = default_relationship_links(source, relationship)
340
+ def to_one_linkage(rid)
341
+ return unless rid
445
342
 
446
- relationship_object_hash['links'] = links unless links.blank?
447
- relationship_object_hash[:data] = to_one_linkage(source, relationship) if include_linkage
448
- relationship_object_hash
343
+ {
344
+ 'type' => format_key(rid.resource_klass._type),
345
+ 'id' => @id_formatter.format(rid.id),
346
+ }
449
347
  end
450
348
 
451
- def relationship_object_to_many(source, relationship, include_linkage)
452
- include_linkage = include_linkage | relationship.always_include_linkage_data
453
- relationship_object_hash = {}
349
+ def link_object_to_one(source, relationship, rid, include_data)
350
+ link_object_hash = {}
351
+ link_object_hash['links'] = {}
352
+ link_object_hash['links']['self'] = self_link(source, relationship)
353
+ link_object_hash['links']['related'] = related_link(source, relationship)
354
+ link_object_hash['data'] = to_one_linkage(rid) if include_data
355
+ link_object_hash
356
+ end
454
357
 
455
- links = default_relationship_links(source, relationship)
456
- relationship_object_hash['links'] = links unless links.blank?
457
- relationship_object_hash[:data] = to_many_linkage(source, relationship) if include_linkage
458
- relationship_object_hash
358
+ def link_object_to_many(source, relationship, rids, include_data)
359
+ link_object_hash = {}
360
+ link_object_hash['links'] = {}
361
+ link_object_hash['links']['self'] = self_link(source, relationship)
362
+ link_object_hash['links']['related'] = related_link(source, relationship)
363
+ link_object_hash['data'] = to_many_linkage(rids) if include_data
364
+ link_object_hash
459
365
  end
460
366
 
461
- def relationship_object(source, relationship, include_linkage = false)
367
+ def link_object(source, relationship, rid, include_data)
462
368
  if relationship.is_a?(JSONAPI::Relationship::ToOne)
463
- relationship_object_to_one(source, relationship, include_linkage)
369
+ link_object_to_one(source, relationship, rid, include_data)
464
370
  elsif relationship.is_a?(JSONAPI::Relationship::ToMany)
465
- relationship_object_to_many(source, relationship, include_linkage)
466
- end
467
- end
468
-
469
- # Extracts the foreign key value for a to_one relationship.
470
- def foreign_key_value(source, relationship)
471
- # If you have changed the key_name, don't even try to look at `"#{relationship.name}_id"`
472
- # just load the association and call the custom key_name
473
- foreign_key_type_changed = relationship.options[:foreign_key_type_changed] || false
474
- related_resource_id =
475
- if source.preloaded_fragments.has_key?(format_key(relationship.name))
476
- source.preloaded_fragments[format_key(relationship.name)].values.first.try(:id)
477
- elsif !foreign_key_type_changed && source.respond_to?("#{relationship.name}_id")
478
- # If you have direct access to the underlying id, you don't have to load the relationship
479
- # which can save quite a lot of time when loading a lot of data.
480
- # This does not apply to e.g. has_one :through relationships.
481
- source.public_send("#{relationship.name}_id")
482
- else
483
- source.public_send(relationship.name).try(:id)
484
- end
485
- return nil unless related_resource_id
486
- @id_formatter.format(related_resource_id)
487
- end
488
-
489
- def foreign_key_types_and_values(source, relationship)
490
- if relationship.is_a?(JSONAPI::Relationship::ToMany)
491
- if relationship.polymorphic?
492
- assoc = source._model.public_send(relationship.name)
493
- # Avoid hitting the database again for values already pre-loaded
494
- if assoc.respond_to?(:loaded?) and assoc.loaded?
495
- assoc.map do |obj|
496
- [obj.type.underscore.pluralize, @id_formatter.format(obj.id)]
497
- end
498
- else
499
- assoc.pluck(:type, :id).map do |type, id|
500
- [type.underscore.pluralize, @id_formatter.format(id)]
501
- end
502
- end
503
- else
504
- source.public_send(relationship.name).map do |value|
505
- [relationship.type, @id_formatter.format(value.id)]
506
- end
507
- end
508
- end
509
- end
510
-
511
- # Sets that an object should be included in the primary document of the response.
512
- def set_primary(type, id)
513
- type = format_key(type)
514
- @included_objects[type][id][:primary] = true
515
- end
516
-
517
- def add_resource(source, include_directives, primary = false)
518
- type = source.is_a?(JSONAPI::CachedResourceFragment) ? source.type : source.class._type
519
- id = source.id
520
-
521
- @included_objects[type] ||= {}
522
- existing = @included_objects[type][id]
523
-
524
- if existing.nil?
525
- obj_hash = object_hash(source, include_directives)
526
- @included_objects[type][id] = {
527
- primary: primary,
528
- object_hash: obj_hash,
529
- includes: Set.new(include_directives[:include_related].keys)
530
- }
531
- else
532
- include_related = Set.new(include_directives[:include_related].keys)
533
- unless existing[:includes].superset?(include_related)
534
- obj_hash = object_hash(source, include_directives)
535
- @included_objects[type][id][:object_hash].deep_merge!(obj_hash)
536
- @included_objects[type][id][:includes].add(include_related)
537
- @included_objects[type][id][:primary] = existing[:primary] | primary
538
- end
371
+ link_object_to_many(source, relationship, rid, include_data)
539
372
  end
540
373
  end
541
374
 
542
375
  def generate_link_builder(primary_resource_klass, options)
543
376
  LinkBuilder.new(
544
377
  base_url: options.fetch(:base_url, ''),
545
- primary_resource_klass: primary_resource_klass,
546
378
  route_formatter: options.fetch(:route_formatter, JSONAPI.configuration.route_formatter),
547
- url_helpers: options.fetch(:url_helpers, options[:controller]),
379
+ primary_resource_klass: primary_resource_klass,
548
380
  )
549
381
  end
550
382
  end