sanger-jsonapi-resources 0.1.1 → 0.2.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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +1 -1
  3. data/README.md +35 -12
  4. data/lib/bug_report_templates/rails_5_latest.rb +125 -0
  5. data/lib/bug_report_templates/rails_5_master.rb +140 -0
  6. data/lib/jsonapi/active_relation/adapters/join_left_active_record_adapter.rb +26 -0
  7. data/lib/jsonapi/active_relation/join_manager.rb +297 -0
  8. data/lib/jsonapi/active_relation_resource.rb +898 -0
  9. data/lib/jsonapi/acts_as_resource_controller.rb +130 -113
  10. data/lib/jsonapi/basic_resource.rb +1164 -0
  11. data/lib/jsonapi/cached_response_fragment.rb +129 -0
  12. data/lib/jsonapi/callbacks.rb +2 -0
  13. data/lib/jsonapi/compatibility_helper.rb +29 -0
  14. data/lib/jsonapi/compiled_json.rb +13 -1
  15. data/lib/jsonapi/configuration.rb +88 -21
  16. data/lib/jsonapi/error.rb +29 -0
  17. data/lib/jsonapi/error_codes.rb +4 -0
  18. data/lib/jsonapi/exceptions.rb +82 -50
  19. data/lib/jsonapi/formatter.rb +5 -3
  20. data/lib/jsonapi/include_directives.rb +22 -67
  21. data/lib/jsonapi/link_builder.rb +76 -80
  22. data/lib/jsonapi/mime_types.rb +6 -10
  23. data/lib/jsonapi/naive_cache.rb +2 -0
  24. data/lib/jsonapi/operation.rb +18 -5
  25. data/lib/jsonapi/operation_result.rb +76 -16
  26. data/lib/jsonapi/paginator.rb +2 -0
  27. data/lib/jsonapi/path.rb +45 -0
  28. data/lib/jsonapi/path_segment.rb +78 -0
  29. data/lib/jsonapi/processor.rb +193 -115
  30. data/lib/jsonapi/relationship.rb +145 -14
  31. data/lib/jsonapi/request.rb +734 -0
  32. data/lib/jsonapi/resource.rb +3 -1251
  33. data/lib/jsonapi/resource_controller.rb +2 -0
  34. data/lib/jsonapi/resource_controller_metal.rb +7 -1
  35. data/lib/jsonapi/resource_fragment.rb +56 -0
  36. data/lib/jsonapi/resource_identity.rb +44 -0
  37. data/lib/jsonapi/resource_serializer.rb +158 -284
  38. data/lib/jsonapi/resource_set.rb +196 -0
  39. data/lib/jsonapi/resource_tree.rb +236 -0
  40. data/lib/jsonapi/resources/railtie.rb +9 -0
  41. data/lib/jsonapi/resources/version.rb +1 -1
  42. data/lib/jsonapi/response_document.rb +107 -83
  43. data/lib/jsonapi/routing_ext.rb +50 -26
  44. data/lib/jsonapi-resources.rb +23 -5
  45. data/lib/tasks/check_upgrade.rake +52 -0
  46. metadata +43 -31
  47. data/lib/jsonapi/cached_resource_fragment.rb +0 -127
  48. data/lib/jsonapi/operation_dispatcher.rb +0 -88
  49. data/lib/jsonapi/operation_results.rb +0 -35
  50. data/lib/jsonapi/relationship_builder.rb +0 -167
  51. data/lib/jsonapi/request_parser.rb +0 -678
@@ -0,0 +1,196 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSONAPI
4
+ # Contains a hash of resource types which contain a hash of resources, relationships and primary status keyed by
5
+ # resource id.
6
+ class ResourceSet
7
+
8
+ attr_reader :resource_klasses, :populated
9
+
10
+ def initialize(source, include_related = nil, options = nil)
11
+ @populated = false
12
+ tree = if source.is_a?(JSONAPI::ResourceTree)
13
+ source
14
+ elsif source.class < JSONAPI::BasicResource
15
+ JSONAPI::PrimaryResourceTree.new(resource: source, include_related: include_related, options: options)
16
+ elsif source.is_a?(Array)
17
+ JSONAPI::PrimaryResourceTree.new(resources: source, include_related: include_related, options: options)
18
+ end
19
+
20
+ if tree
21
+ @resource_klasses = flatten_resource_tree(tree)
22
+ end
23
+ end
24
+
25
+ def populate!(serializer, context, options)
26
+ return if @populated
27
+
28
+ # For each resource klass we want to generate the caching key
29
+
30
+ # Hash for collecting types and ids
31
+ # @type [Hash<Class<Resource>, Id[]]]
32
+ missed_resource_ids = {}
33
+
34
+ # Array for collecting CachedResponseFragment::Lookups
35
+ # @type [Lookup[]]
36
+ lookups = []
37
+
38
+ # Step One collect all of the lookups for the cache, or keys that don't require cache access
39
+ @resource_klasses.each_key do |resource_klass|
40
+ missed_resource_ids[resource_klass] ||= []
41
+
42
+ serializer_config_key = serializer.config_key(resource_klass).gsub("/", "_")
43
+ context_json = resource_klass.attribute_caching_context(context).to_json
44
+ context_b64 = JSONAPI.configuration.resource_cache_digest_function.call(context_json)
45
+ context_key = "ATTR-CTX-#{context_b64.gsub("/", "_")}"
46
+
47
+ if resource_klass.caching?
48
+ cache_ids = @resource_klasses[resource_klass].map do |(k, v)|
49
+ # Store the hashcode of the cache_field to avoid storing objects and to ensure precision isn't lost
50
+ # on timestamp types (i.e. string conversions dropping milliseconds)
51
+ [k, resource_klass.hash_cache_field(v[:cache_id])]
52
+ end
53
+
54
+ lookups.push(
55
+ CachedResponseFragment::Lookup.new(
56
+ resource_klass,
57
+ serializer_config_key,
58
+ context,
59
+ context_key,
60
+ cache_ids
61
+ )
62
+ )
63
+ else
64
+ @resource_klasses[resource_klass].keys.each do |k|
65
+ if @resource_klasses[resource_klass][k][:resource].nil?
66
+ missed_resource_ids[resource_klass] << k
67
+ else
68
+ register_resource(resource_klass, @resource_klasses[resource_klass][k][:resource])
69
+ end
70
+ end
71
+ end
72
+ end
73
+
74
+ if lookups.any?
75
+ raise "You've declared some Resources as caching without providing a caching store" if JSONAPI.configuration.resource_cache.nil?
76
+
77
+ # Step Two execute the cache lookup
78
+ found_resources = CachedResponseFragment.lookup(lookups, context)
79
+ else
80
+ found_resources = {}
81
+ end
82
+
83
+ # Step Three collect the results and collect hit/miss stats
84
+ stats = {}
85
+ found_resources.each do |resource_klass, resources|
86
+ resources.each do |id, cached_resource|
87
+ stats[resource_klass] ||= {}
88
+
89
+ if cached_resource.nil?
90
+ stats[resource_klass][:misses] ||= 0
91
+ stats[resource_klass][:misses] += 1
92
+
93
+ # Collect misses
94
+ missed_resource_ids[resource_klass].push(id)
95
+ else
96
+ stats[resource_klass][:hits] ||= 0
97
+ stats[resource_klass][:hits] += 1
98
+
99
+ register_resource(resource_klass, cached_resource)
100
+ end
101
+ end
102
+ end
103
+
104
+ report_stats(stats)
105
+
106
+ writes = []
107
+
108
+ # Step Four find any of the missing resources and join them into the result
109
+ missed_resource_ids.each_pair do |resource_klass, ids|
110
+ next if ids.empty?
111
+
112
+ find_opts = {context: context, fields: options[:fields]}
113
+ found_resources = resource_klass.find_to_populate_by_keys(ids, find_opts)
114
+
115
+ found_resources.each do |resource|
116
+ relationship_data = @resource_klasses[resource_klass][resource.id][:relationships]
117
+
118
+ if resource_klass.caching?
119
+ serializer_config_key = serializer.config_key(resource_klass).gsub("/", "_")
120
+ context_json = resource_klass.attribute_caching_context(context).to_json
121
+ context_b64 = JSONAPI.configuration.resource_cache_digest_function.call(context_json)
122
+ context_key = "ATTR-CTX-#{context_b64.gsub("/", "_")}"
123
+
124
+ writes.push(CachedResponseFragment::Write.new(
125
+ resource_klass,
126
+ resource,
127
+ serializer,
128
+ serializer_config_key,
129
+ context,
130
+ context_key,
131
+ relationship_data
132
+ ))
133
+ end
134
+
135
+ register_resource(resource_klass, resource)
136
+ end
137
+ end
138
+
139
+ # Step Five conditionally write to the cache
140
+ CachedResponseFragment.write(writes) unless JSONAPI.configuration.resource_cache.nil?
141
+
142
+ mark_populated!
143
+ self
144
+ end
145
+
146
+ def mark_populated!
147
+ @populated = true
148
+ end
149
+
150
+ def register_resource(resource_klass, resource, primary = false)
151
+ @resource_klasses[resource_klass] ||= {}
152
+ @resource_klasses[resource_klass][resource.id] ||= {primary: resource.try(:primary) || primary, relationships: {}}
153
+ @resource_klasses[resource_klass][resource.id][:resource] = resource
154
+ end
155
+
156
+ private
157
+
158
+ def report_stats(stats)
159
+ return unless JSONAPI.configuration.resource_cache_usage_report_function || JSONAPI.configuration.resource_cache.nil?
160
+
161
+ stats.each_pair do |resource_klass, stat|
162
+ JSONAPI.configuration.resource_cache_usage_report_function.call(
163
+ resource_klass.name,
164
+ stat[:hits] || 0,
165
+ stat[:misses] || 0
166
+ )
167
+ end
168
+ end
169
+
170
+ def flatten_resource_tree(resource_tree, flattened_tree = {})
171
+ resource_tree.fragments.each_pair do |resource_rid, fragment|
172
+
173
+ resource_klass = resource_rid.resource_klass
174
+ id = resource_rid.id
175
+
176
+ flattened_tree[resource_klass] ||= {}
177
+
178
+ flattened_tree[resource_klass][id] ||= {primary: fragment.primary, relationships: {}}
179
+ flattened_tree[resource_klass][id][:cache_id] ||= fragment.cache if fragment.cache
180
+ flattened_tree[resource_klass][id][:resource] ||= fragment.resource if fragment.resource
181
+
182
+ fragment.related.try(:each_pair) do |relationship_name, related_rids|
183
+ flattened_tree[resource_klass][id][:relationships][relationship_name] ||= Set.new
184
+ flattened_tree[resource_klass][id][:relationships][relationship_name].merge(related_rids)
185
+ end
186
+ end
187
+
188
+ related_resource_trees = resource_tree.related_resource_trees
189
+ related_resource_trees.try(:each_value) do |related_resource_tree|
190
+ flatten_resource_tree(related_resource_tree, flattened_tree)
191
+ end
192
+
193
+ flattened_tree
194
+ end
195
+ end
196
+ end
@@ -0,0 +1,236 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSONAPI
4
+
5
+ # A tree structure representing the resource structure of the requested resource(s). This is an intermediate structure
6
+ # used to keep track of the resources, by identity, found at different included relationships. It will be flattened and
7
+ # the resource instances will be fetched from the cache or the record store.
8
+ class ResourceTree
9
+
10
+ attr_reader :fragments, :related_resource_trees
11
+
12
+ # Gets the related Resource Id Tree for a relationship, and creates it first if it does not exist
13
+ #
14
+ # @param relationship [JSONAPI::Relationship]
15
+ #
16
+ # @return [JSONAPI::RelatedResourceTree] the new or existing resource id tree for the requested relationship
17
+ def get_related_resource_tree(relationship)
18
+ relationship_name = relationship.name.to_sym
19
+ @related_resource_trees[relationship_name] ||= RelatedResourceTree.new(relationship, self)
20
+ end
21
+
22
+ # Adds each Resource Fragment to the Resources hash
23
+ #
24
+ # @param fragments [Hash]
25
+ # @param include_related [Hash]
26
+ #
27
+ # @return [null]
28
+ def add_resource_fragments(fragments, include_related)
29
+ fragments.each_value do |fragment|
30
+ add_resource_fragment(fragment, include_related)
31
+ end
32
+ end
33
+
34
+ # Adds a Resource Fragment to the fragments hash
35
+ #
36
+ # @param fragment [JSONAPI::ResourceFragment]
37
+ # @param include_related [Hash]
38
+ #
39
+ # @return [null]
40
+ def add_resource_fragment(fragment, include_related)
41
+ init_included_relationships(fragment, include_related)
42
+
43
+ @fragments[fragment.identity] = fragment
44
+ end
45
+
46
+ # Adds each Resource to the fragments hash
47
+ #
48
+ # @param resource [Hash]
49
+ # @param include_related [Hash]
50
+ #
51
+ # @return [null]
52
+ def add_resources(resources, include_related)
53
+ resources.each do |resource|
54
+ add_resource_fragment(JSONAPI::ResourceFragment.new(resource.identity, resource: resource), include_related)
55
+ end
56
+ end
57
+
58
+ # Adds a Resource to the fragments hash
59
+ #
60
+ # @param fragment [JSONAPI::ResourceFragment]
61
+ # @param include_related [Hash]
62
+ #
63
+ # @return [null]
64
+ def add_resource(resource, include_related)
65
+ add_resource_fragment(JSONAPI::ResourceFragment.new(resource.identity, resource: resource), include_related)
66
+ end
67
+
68
+ private
69
+
70
+ def init_included_relationships(fragment, include_related)
71
+ include_related && include_related.each_key do |relationship_name|
72
+ fragment.initialize_related(relationship_name)
73
+ end
74
+ end
75
+
76
+ def load_included(resource_klass, source_resource_tree, include_related, options)
77
+ include_related.try(:each_key) do |key|
78
+ relationship = resource_klass._relationship(key)
79
+ relationship_name = relationship.name.to_sym
80
+
81
+ find_related_resource_options = options.except(:filters, :sort_criteria, :paginator)
82
+ find_related_resource_options[:sort_criteria] = relationship.resource_klass.default_sort
83
+ find_related_resource_options[:cache] = resource_klass.caching?
84
+
85
+ related_fragments = resource_klass.find_included_fragments(source_resource_tree.fragments.values,
86
+ relationship_name,
87
+ find_related_resource_options)
88
+
89
+ related_resource_tree = source_resource_tree.get_related_resource_tree(relationship)
90
+ related_resource_tree.add_resource_fragments(related_fragments, include_related[key][:include_related])
91
+
92
+ # Now recursively get the related resources for the currently found resources
93
+ load_included(relationship.resource_klass,
94
+ related_resource_tree,
95
+ include_related[relationship_name][:include_related],
96
+ options)
97
+ end
98
+ end
99
+
100
+ def add_resources_to_tree(resource_klass,
101
+ tree,
102
+ resources,
103
+ include_related,
104
+ source_rid: nil,
105
+ source_relationship_name: nil,
106
+ connect_source_identity: true)
107
+ fragments = {}
108
+
109
+ resources.each do |resource|
110
+ next unless resource
111
+
112
+ # fragments[resource.identity] ||= ResourceFragment.new(resource.identity, resource: resource)
113
+ # resource_fragment = fragments[resource.identity]
114
+ # ToDo: revert when not needed for testing
115
+ resource_fragment = if fragments[resource.identity]
116
+ fragments[resource.identity]
117
+ else
118
+ fragments[resource.identity] = ResourceFragment.new(resource.identity, resource: resource)
119
+ fragments[resource.identity]
120
+ end
121
+
122
+ if resource.class.caching?
123
+ resource_fragment.cache = resource.cache_field_value
124
+ end
125
+
126
+ linkage_relationships = resource_klass.to_one_relationships_for_linkage(resource.class, include_related)
127
+ linkage_relationships.each do |relationship_name|
128
+ related_resource = resource.send(relationship_name)
129
+ resource_fragment.add_related_identity(relationship_name, related_resource&.identity)
130
+ end
131
+
132
+ if source_rid && connect_source_identity
133
+ resource_fragment.add_related_from(source_rid)
134
+ source_klass = source_rid.resource_klass
135
+ related_relationship_name = source_klass._relationships[source_relationship_name].inverse_relationship
136
+ if related_relationship_name
137
+ resource_fragment.add_related_identity(related_relationship_name, source_rid)
138
+ end
139
+ end
140
+ end
141
+
142
+ tree.add_resource_fragments(fragments, include_related)
143
+ end
144
+ end
145
+
146
+ class PrimaryResourceTree < ResourceTree
147
+
148
+ # Creates a PrimaryResourceTree with no resources and no related ResourceTrees
149
+ def initialize(fragments: nil, resources: nil, resource: nil, include_related: nil, options: nil)
150
+ @fragments ||= {}
151
+ @related_resource_trees ||= {}
152
+ if fragments || resources || resource
153
+ if fragments
154
+ add_resource_fragments(fragments, include_related)
155
+ end
156
+
157
+ if resources
158
+ add_resources(resources, include_related)
159
+ end
160
+
161
+ if resource
162
+ add_resource(resource, include_related)
163
+ end
164
+
165
+ complete_includes!(include_related, options)
166
+ end
167
+ end
168
+
169
+ # Adds a Resource Fragment to the fragments hash
170
+ #
171
+ # @param fragment [JSONAPI::ResourceFragment]
172
+ # @param include_related [Hash]
173
+ #
174
+ # @return [null]
175
+ def add_resource_fragment(fragment, include_related)
176
+ fragment.primary = true
177
+ super(fragment, include_related)
178
+ end
179
+
180
+ def complete_includes!(include_related, options)
181
+ # ToDo: can we skip if more than one resource_klass found?
182
+ resource_klasses = Set.new
183
+ @fragments.each_key { |identity| resource_klasses << identity.resource_klass }
184
+
185
+ resource_klasses.each { |resource_klass| load_included(resource_klass, self, include_related, options)}
186
+
187
+ self
188
+ end
189
+ end
190
+
191
+ class RelatedResourceTree < ResourceTree
192
+
193
+ attr_reader :parent_relationship, :source_resource_tree
194
+
195
+ # Creates a RelatedResourceTree with no resources and no related ResourceTrees. A connection to the parent
196
+ # ResourceTree is maintained.
197
+ #
198
+ # @param parent_relationship [JSONAPI::Relationship]
199
+ # @param source_resource_tree [JSONAPI::ResourceTree]
200
+ #
201
+ # @return [JSONAPI::RelatedResourceTree] the new or existing resource id tree for the requested relationship
202
+ def initialize(parent_relationship, source_resource_tree)
203
+ @fragments ||= {}
204
+ @related_resource_trees ||= {}
205
+
206
+ @parent_relationship = parent_relationship
207
+ @parent_relationship_name = parent_relationship.name.to_sym
208
+ @source_resource_tree = source_resource_tree
209
+ end
210
+
211
+ # Adds a Resource Fragment to the fragments hash
212
+ #
213
+ # @param fragment [JSONAPI::ResourceFragment]
214
+ # @param include_related [Hash]
215
+ #
216
+ # @return [null]
217
+ def add_resource_fragment(fragment, include_related)
218
+ init_included_relationships(fragment, include_related)
219
+
220
+ fragment.related_from.each do |rid|
221
+ @source_resource_tree.fragments[rid].add_related_identity(parent_relationship.name, fragment.identity)
222
+ end
223
+
224
+ if @fragments[fragment.identity]
225
+ @fragments[fragment.identity].related_from.merge(fragment.related_from)
226
+ fragment.related.each_pair do |relationship_name, rids|
227
+ if rids
228
+ @fragments[fragment.identity].merge_related_identities(relationship_name, rids)
229
+ end
230
+ end
231
+ else
232
+ @fragments[fragment.identity] = fragment
233
+ end
234
+ end
235
+ end
236
+ end
@@ -0,0 +1,9 @@
1
+ module JSONAPI
2
+ module Resources
3
+ class Railtie < Rails::Railtie
4
+ rake_tasks do
5
+ load 'tasks/check_upgrade.rake'
6
+ end
7
+ end
8
+ end
9
+ end
@@ -1,5 +1,5 @@
1
1
  module JSONAPI
2
2
  module Resources
3
- VERSION = '0.1.1'
3
+ VERSION = '0.2.0'
4
4
  end
5
5
  end
@@ -1,81 +1,140 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module JSONAPI
2
4
  class ResponseDocument
3
- def initialize(operation_results, serializer, options = {})
4
- @operation_results = operation_results
5
- @serializer = serializer
5
+ attr_reader :serialized_results
6
+
7
+ def initialize(options = {})
8
+ @serialized_results = []
9
+ @result_codes = []
10
+ @error_results = []
11
+ @global_errors = []
12
+
6
13
  @options = options
7
14
 
15
+ @top_level_meta = @options.fetch(:base_meta, {})
16
+ @top_level_links = @options.fetch(:base_links, {})
17
+
8
18
  @key_formatter = @options.fetch(:key_formatter, JSONAPI.configuration.key_formatter)
9
19
  end
10
20
 
11
- def contents
12
- hash = results_to_hash
21
+ def has_errors?
22
+ @error_results.length.positive? || @global_errors.length.positive?
23
+ end
13
24
 
14
- meta = top_level_meta
15
- hash.merge!(meta: meta) unless meta.empty?
25
+ def add_result(result, operation)
26
+ if result.is_a?(JSONAPI::ErrorsOperationResult)
27
+ # Clear any serialized results
28
+ @serialized_results = []
16
29
 
17
- links = top_level_links
18
- hash.merge!(links: links) unless links.empty?
30
+ # In JSONAPI v1 we only have one operation so all errors can be kept together
31
+ result.errors.each do |error|
32
+ add_global_error(error)
33
+ end
34
+ else
35
+ @serialized_results.push result.to_hash(operation.options[:serializer])
36
+ @result_codes.push result.code.to_i
37
+ update_links(operation.options[:serializer], result)
38
+ update_meta(result)
39
+ end
40
+ end
19
41
 
20
- hash
42
+ def add_global_error(error)
43
+ @global_errors.push error
21
44
  end
22
45
 
23
- def status
24
- if @operation_results.has_errors?
25
- @operation_results.all_errors[0].status
46
+ def contents
47
+ if has_errors?
48
+ return { 'errors' => @global_errors }
26
49
  else
27
- @operation_results.results[0].code
50
+ hash = @serialized_results[0]
51
+ meta = top_level_meta
52
+ hash.merge!('meta' => meta) unless meta.empty?
53
+
54
+ links = top_level_links
55
+ hash.merge!('links' => links) unless links.empty?
56
+
57
+ return hash
28
58
  end
29
59
  end
30
60
 
31
- private
61
+ def status
62
+ status_codes = if has_errors?
63
+ @global_errors.collect do |error|
64
+ error.status.to_i
65
+ end
66
+ else
67
+ @result_codes
68
+ end
69
+
70
+ # Count the unique status codes
71
+ counts = status_codes.each_with_object(Hash.new(0)) { |code, counts| counts[code] += 1 }
72
+
73
+ # if there is only one status code we can return that
74
+ return counts.keys[0].to_i if counts.length == 1
75
+
76
+ # :nocov: not currently used
77
+
78
+ # if there are many we should return the highest general code, 200, 400, 500 etc.
79
+ max_status = 0
80
+ status_codes.each do |status|
81
+ code = status.to_i
82
+ max_status = code if max_status < code
83
+ end
84
+ return (max_status / 100).floor * 100
85
+ # :nocov:
86
+ end
32
87
 
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, {})
88
+ private
37
89
 
38
- meta.merge!(@operation_results.meta)
90
+ def update_meta(result)
91
+ @top_level_meta.merge!(result.meta)
39
92
 
40
- @operation_results.results.each do |result|
41
- meta.merge!(result.meta)
93
+ if JSONAPI.configuration.top_level_meta_include_record_count && result.respond_to?(:record_count)
94
+ @top_level_meta[JSONAPI.configuration.top_level_meta_record_count_key] = result.record_count
95
+ end
42
96
 
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
97
+ if JSONAPI.configuration.top_level_meta_include_page_count && result.respond_to?(:page_count)
98
+ @top_level_meta[JSONAPI.configuration.top_level_meta_page_count_key] = result.page_count
99
+ end
46
100
 
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
101
+ if result.warnings.any?
102
+ @top_level_meta[:warnings] = result.warnings.collect do |warning|
103
+ warning.to_hash
49
104
  end
50
105
  end
106
+ end
51
107
 
52
- meta.as_json.deep_transform_keys { |key| @key_formatter.format(key) }
108
+ def top_level_meta
109
+ @top_level_meta.as_json.deep_transform_keys { |key| @key_formatter.format(key) }
53
110
  end
54
111
 
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
112
+ def update_links(serializer, result)
113
+ @top_level_links.merge!(result.links)
114
+
115
+ # Build pagination links
116
+ if result.is_a?(JSONAPI::ResourceSetOperationResult) ||
117
+ result.is_a?(JSONAPI::ResourcesSetOperationResult) ||
118
+ result.is_a?(JSONAPI::RelatedResourcesSetOperationResult)
119
+
120
+ result.pagination_params.each_pair do |link_name, params|
121
+ if result.is_a?(JSONAPI::RelatedResourcesSetOperationResult)
122
+ relationship = result.source_resource.class._relationships[result._type.to_sym]
123
+ unless relationship.exclude_link?(link_name)
124
+ link = serializer.link_builder.relationships_related_link(result.source_resource, relationship, query_params(params))
125
+ end
126
+ else
127
+ unless serializer.link_builder.primary_resource_klass.exclude_link?(link_name)
128
+ link = serializer.link_builder.query_link(query_params(params))
74
129
  end
130
+ end
131
+ @top_level_links[link_name] = link unless link.blank?
75
132
  end
76
133
  end
134
+ end
77
135
 
78
- links.deep_transform_keys { |key| @key_formatter.format(key) }
136
+ def top_level_links
137
+ @top_level_links.deep_transform_keys { |key| @key_formatter.format(key) }
79
138
  end
80
139
 
81
140
  def query_params(params)
@@ -96,40 +155,5 @@ module JSONAPI
96
155
 
97
156
  query_params
98
157
  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
158
  end
135
159
  end