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