jsonapi-resources 0.9.12 → 0.10.0.beta1

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 (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