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.
- checksums.yaml +4 -4
- data/LICENSE.txt +1 -1
- data/README.md +35 -12
- data/lib/bug_report_templates/rails_5_latest.rb +125 -0
- data/lib/bug_report_templates/rails_5_master.rb +140 -0
- data/lib/jsonapi/active_relation/adapters/join_left_active_record_adapter.rb +26 -0
- data/lib/jsonapi/active_relation/join_manager.rb +297 -0
- data/lib/jsonapi/active_relation_resource.rb +898 -0
- data/lib/jsonapi/acts_as_resource_controller.rb +130 -113
- data/lib/jsonapi/basic_resource.rb +1164 -0
- data/lib/jsonapi/cached_response_fragment.rb +129 -0
- data/lib/jsonapi/callbacks.rb +2 -0
- data/lib/jsonapi/compatibility_helper.rb +29 -0
- data/lib/jsonapi/compiled_json.rb +13 -1
- data/lib/jsonapi/configuration.rb +88 -21
- data/lib/jsonapi/error.rb +29 -0
- data/lib/jsonapi/error_codes.rb +4 -0
- data/lib/jsonapi/exceptions.rb +82 -50
- data/lib/jsonapi/formatter.rb +5 -3
- data/lib/jsonapi/include_directives.rb +22 -67
- data/lib/jsonapi/link_builder.rb +76 -80
- data/lib/jsonapi/mime_types.rb +6 -10
- data/lib/jsonapi/naive_cache.rb +2 -0
- data/lib/jsonapi/operation.rb +18 -5
- data/lib/jsonapi/operation_result.rb +76 -16
- data/lib/jsonapi/paginator.rb +2 -0
- data/lib/jsonapi/path.rb +45 -0
- data/lib/jsonapi/path_segment.rb +78 -0
- data/lib/jsonapi/processor.rb +193 -115
- data/lib/jsonapi/relationship.rb +145 -14
- data/lib/jsonapi/request.rb +734 -0
- data/lib/jsonapi/resource.rb +3 -1251
- data/lib/jsonapi/resource_controller.rb +2 -0
- data/lib/jsonapi/resource_controller_metal.rb +7 -1
- data/lib/jsonapi/resource_fragment.rb +56 -0
- data/lib/jsonapi/resource_identity.rb +44 -0
- data/lib/jsonapi/resource_serializer.rb +158 -284
- data/lib/jsonapi/resource_set.rb +196 -0
- data/lib/jsonapi/resource_tree.rb +236 -0
- data/lib/jsonapi/resources/railtie.rb +9 -0
- data/lib/jsonapi/resources/version.rb +1 -1
- data/lib/jsonapi/response_document.rb +107 -83
- data/lib/jsonapi/routing_ext.rb +50 -26
- data/lib/jsonapi-resources.rb +23 -5
- data/lib/tasks/check_upgrade.rake +52 -0
- metadata +43 -31
- data/lib/jsonapi/cached_resource_fragment.rb +0 -127
- data/lib/jsonapi/operation_dispatcher.rb +0 -88
- data/lib/jsonapi/operation_results.rb +0 -35
- data/lib/jsonapi/relationship_builder.rb +0 -167
- 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
|
@@ -1,81 +1,140 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module JSONAPI
|
2
4
|
class ResponseDocument
|
3
|
-
|
4
|
-
|
5
|
-
|
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
|
12
|
-
|
21
|
+
def has_errors?
|
22
|
+
@error_results.length.positive? || @global_errors.length.positive?
|
23
|
+
end
|
13
24
|
|
14
|
-
|
15
|
-
|
25
|
+
def add_result(result, operation)
|
26
|
+
if result.is_a?(JSONAPI::ErrorsOperationResult)
|
27
|
+
# Clear any serialized results
|
28
|
+
@serialized_results = []
|
16
29
|
|
17
|
-
|
18
|
-
|
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
|
-
|
42
|
+
def add_global_error(error)
|
43
|
+
@global_errors.push error
|
21
44
|
end
|
22
45
|
|
23
|
-
def
|
24
|
-
if
|
25
|
-
@
|
46
|
+
def contents
|
47
|
+
if has_errors?
|
48
|
+
return { 'errors' => @global_errors }
|
26
49
|
else
|
27
|
-
@
|
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
|
-
|
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
|
-
|
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
|
-
|
90
|
+
def update_meta(result)
|
91
|
+
@top_level_meta.merge!(result.meta)
|
39
92
|
|
40
|
-
|
41
|
-
|
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
|
-
|
44
|
-
|
45
|
-
|
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
|
-
|
48
|
-
|
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
|
-
|
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
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
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
|
-
|
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
|