-
Notifications
You must be signed in to change notification settings - Fork 535
/
Copy pathresource_serializer.rb
389 lines (315 loc) · 14.2 KB
/
resource_serializer.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
module JSONAPI
class ResourceSerializer
attr_reader :link_builder, :key_formatter, :serialization_options,
:fields, :include_directives, :always_include_to_one_linkage_data,
:always_include_to_many_linkage_data
# initialize
# Options can include
# include:
# Purpose: determines which objects will be side loaded with the source objects in a linked section
# Example: ['comments','author','comments.tags','author.posts']
# fields:
# Purpose: determines which fields are serialized for a resource type. This encompasses both attributes and
# relationship ids in the links section for a resource. Fields are global for a resource type.
# Example: { people: [:id, :email, :comments], posts: [:id, :title, :author], comments: [:id, :body, :post]}
# key_formatter: KeyFormatter instance to override the default configuration
# serialization_options: additional options that will be passed to resource meta and links lambdas
def initialize(primary_resource_klass, options = {})
@primary_resource_klass = primary_resource_klass
@fields = options.fetch(:fields, {})
@include = options.fetch(:include, [])
@include_directives = options[:include_directives]
@include_directives ||= JSONAPI::IncludeDirectives.new(@primary_resource_klass, @include)
@key_formatter = options.fetch(:key_formatter, JSONAPI.configuration.key_formatter)
@id_formatter = ValueFormatter.value_formatter_for(:id)
@link_builder = generate_link_builder(primary_resource_klass, options)
@always_include_to_one_linkage_data = options.fetch(:always_include_to_one_linkage_data,
JSONAPI.configuration.always_include_to_one_linkage_data)
@always_include_to_many_linkage_data = options.fetch(:always_include_to_many_linkage_data,
JSONAPI.configuration.always_include_to_many_linkage_data)
@serialization_options = options.fetch(:serialization_options, {})
# Warning: This makes ResourceSerializer non-thread-safe. That's not a problem with the
# request-specific way it's currently used, though.
@value_formatter_type_cache = NaiveCache.new{|arg| ValueFormatter.value_formatter_for(arg) }
@_config_keys = {}
@_supplying_attribute_fields = {}
@_supplying_relationship_fields = {}
end
# Converts a resource_set to a hash, conforming to the JSONAPI structure
def serialize_resource_set_to_hash_single(resource_set)
primary_objects = []
included_objects = []
resource_set.resource_klasses.each_value do |resource_klass|
resource_klass.each_value do |resource|
serialized_resource = object_hash(resource[:resource], resource[:relationships])
if resource[:primary]
primary_objects.push(serialized_resource)
else
included_objects.push(serialized_resource)
end
end
end
fail "Too many primary objects for show" if (primary_objects.count > 1)
primary_hash = { 'data' => primary_objects[0] }
primary_hash['included'] = included_objects if included_objects.size > 0
primary_hash
end
def serialize_resource_set_to_hash_plural(resource_set)
primary_objects = []
included_objects = []
resource_set.resource_klasses.each_value do |resource_klass|
resource_klass.each_value do |resource|
serialized_resource = object_hash(resource[:resource], resource[:relationships])
if resource[:primary]
primary_objects.push(serialized_resource)
else
included_objects.push(serialized_resource)
end
end
end
primary_hash = { 'data' => primary_objects }
primary_hash['included'] = included_objects if included_objects.size > 0
primary_hash
end
def serialize_related_resource_set_to_hash_plural(resource_set, _source_resource)
return serialize_resource_set_to_hash_plural(resource_set)
end
def serialize_to_relationship_hash(source, requested_relationship, resource_ids)
if requested_relationship.is_a?(JSONAPI::Relationship::ToOne)
data = to_one_linkage(resource_ids[0])
else
data = to_many_linkage(resource_ids)
end
rel_hash = { 'data': data }
links = default_relationship_links(source, requested_relationship)
rel_hash['links'] = links unless links.blank?
rel_hash
end
def format_key(key)
@key_formatter.format(key)
end
def unformat_key(key)
@key_formatter.unformat(key)
end
def format_value(value, format)
@value_formatter_type_cache.get(format).format(value)
end
def config_key(resource_klass)
@_config_keys.fetch resource_klass do
desc = self.config_description(resource_klass).map(&:inspect).join(",")
key = JSONAPI.configuration.resource_cache_digest_function.call(desc)
@_config_keys[resource_klass] = "SRLZ-#{key}"
end
end
def config_description(resource_klass)
{
class_name: self.class.name,
serialization_options: serialization_options.sort.map(&:as_json),
supplying_attribute_fields: supplying_attribute_fields(resource_klass).sort,
supplying_relationship_fields: supplying_relationship_fields(resource_klass).sort,
link_builder_base_url: link_builder.base_url,
key_formatter_class: key_formatter.uncached.class.name,
always_include_to_one_linkage_data: always_include_to_one_linkage_data,
always_include_to_many_linkage_data: always_include_to_many_linkage_data
}
end
def object_hash(source, relationship_data)
obj_hash = {}
return obj_hash if source.nil?
fetchable_fields = Set.new(source.fetchable_fields)
if source.is_a?(JSONAPI::CachedResponseFragment)
id_format = source.resource_klass._attribute_options(:id)[:format]
id_format = 'id' if id_format == :default
obj_hash['id'] = format_value(source.id, id_format)
obj_hash['type'] = source.type
obj_hash['links'] = source.links_json if source.links_json
obj_hash['attributes'] = source.attributes_json if source.attributes_json
relationships = cached_relationships_hash(source, fetchable_fields, relationship_data)
obj_hash['relationships'] = relationships unless relationships.blank?
obj_hash['meta'] = source.meta_json if source.meta_json
else
# TODO Should this maybe be using @id_formatter instead, for consistency?
id_format = source.class._attribute_options(:id)[:format]
# protect against ids that were declared as an attribute, but did not have a format set.
id_format = 'id' if id_format == :default
obj_hash['id'] = format_value(source.id, id_format)
obj_hash['type'] = format_key(source.class._type.to_s)
links = links_hash(source)
obj_hash['links'] = links unless links.empty?
attributes = attributes_hash(source, fetchable_fields)
obj_hash['attributes'] = attributes unless attributes.empty?
relationships = relationships_hash(source, fetchable_fields, relationship_data)
obj_hash['relationships'] = relationships unless relationships.blank?
meta = meta_hash(source)
obj_hash['meta'] = meta unless meta.empty?
end
obj_hash
end
private
def supplying_attribute_fields(resource_klass)
@_supplying_attribute_fields.fetch resource_klass do
attrs = Set.new(resource_klass._attributes.keys.map(&:to_sym))
cur = resource_klass
while !cur.root? # do not traverse beyond the first root resource
if @fields.has_key?(cur._type)
attrs &= @fields[cur._type]
break
end
cur = cur.superclass
end
@_supplying_attribute_fields[resource_klass] = attrs
end
end
def supplying_relationship_fields(resource_klass)
@_supplying_relationship_fields.fetch resource_klass do
relationships = Set.new(resource_klass._relationships.keys.map(&:to_sym))
cur = resource_klass
while !cur.root? # do not traverse beyond the first root resource
if @fields.has_key?(cur._type)
relationships &= @fields[cur._type]
break
end
cur = cur.superclass
end
@_supplying_relationship_fields[resource_klass] = relationships
end
end
def attributes_hash(source, fetchable_fields)
fields = fetchable_fields & supplying_attribute_fields(source.class)
fields.each_with_object({}) do |name, hash|
unless name == :id
format = source.class._attribute_options(name)[:format]
hash[format_key(name)] = format_value(source.public_send(name), format)
end
end
end
def custom_generation_options
@_custom_generation_options ||= {
serializer: self,
serialization_options: @serialization_options
}
end
def meta_hash(source)
meta = source.meta(custom_generation_options)
(meta.is_a?(Hash) && meta) || {}
end
def links_hash(source)
links = custom_links_hash(source)
if !links.key?('self') && !source.class.exclude_link?(:self)
links['self'] = link_builder.self_link(source)
end
links.compact
end
def custom_links_hash(source)
custom_links = source.custom_links(custom_generation_options)
(custom_links.is_a?(Hash) && custom_links) || {}
end
def relationships_hash(source, fetchable_fields, relationship_data)
relationships = source.class._relationships.select{|k,_v| fetchable_fields.include?(k) }
field_set = supplying_relationship_fields(source.class) & relationships.keys
relationships.each_with_object({}) do |(name, relationship), hash|
include_data = false
if field_set.include?(name)
if relationship_data[name]
include_data = true
if relationship.is_a?(JSONAPI::Relationship::ToOne)
rids = relationship_data[name].first
else
rids = relationship_data[name]
end
end
ro = relationship_object(source, relationship, rids, include_data)
hash[format_key(name)] = ro unless ro.blank?
end
end
end
def cached_relationships_hash(source, fetchable_fields, relationship_data)
relationships = {}
source.relationships.try(:each_pair) do |k,v|
if fetchable_fields.include?(unformat_key(k).to_sym)
relationships[k.to_sym] = v
end
end
field_set = supplying_relationship_fields(source.resource_klass).collect {|k| format_key(k).to_sym } & relationships.keys
relationships.each_with_object({}) do |(name, relationship), hash|
if field_set.include?(name)
relationship_name = unformat_key(name).to_sym
relationship_klass = source.resource_klass._relationships[relationship_name]
if relationship_klass.is_a?(JSONAPI::Relationship::ToOne)
# include_linkage = @always_include_to_one_linkage_data | relationship_klass.always_include_linkage_data
if relationship_data[relationship_name]
rids = relationship_data[relationship_name].first
relationship['data'] = to_one_linkage(rids)
end
else
# include_linkage = relationship_klass.always_include_linkage_data
if relationship_data[relationship_name]
rids = relationship_data[relationship_name]
relationship['data'] = to_many_linkage(rids)
end
end
hash[format_key(name)] = relationship
end
end
end
def self_link(source, relationship)
link_builder.relationships_self_link(source, relationship)
end
def related_link(source, relationship)
link_builder.relationships_related_link(source, relationship)
end
def default_relationship_links(source, relationship)
links = {}
links['self'] = self_link(source, relationship) unless relationship.exclude_link?(:self)
links['related'] = related_link(source, relationship) unless relationship.exclude_link?(:related)
links.compact
end
def to_many_linkage(rids)
linkage = []
rids && rids.each do |details|
id = details.custom_id || details.id
type = details.resource_klass.try(:_type)
if type && id
linkage.append({'type' => format_key(type), 'id' => @id_formatter.format(id)})
end
end
linkage
end
def to_one_linkage(rid)
return unless rid
{
'type' => format_key(rid.resource_klass._type),
'id' => @id_formatter.format(rid.custom_id || rid.id),
}
end
def relationship_object_to_one(source, relationship, rid, include_data)
link_object_hash = {}
links = default_relationship_links(source, relationship)
link_object_hash['links'] = links unless links.blank?
link_object_hash['data'] = to_one_linkage(rid) if include_data
link_object_hash
end
def relationship_object_to_many(source, relationship, rids, include_data)
link_object_hash = {}
links = default_relationship_links(source, relationship)
link_object_hash['links'] = links unless links.blank?
link_object_hash['data'] = to_many_linkage(rids) if include_data
link_object_hash
end
def relationship_object(source, relationship, rid, include_data)
if relationship.is_a?(JSONAPI::Relationship::ToOne)
relationship_object_to_one(source, relationship, rid, include_data)
elsif relationship.is_a?(JSONAPI::Relationship::ToMany)
relationship_object_to_many(source, relationship, rid, include_data)
end
end
def generate_link_builder(primary_resource_klass, options)
LinkBuilder.new(
base_url: options.fetch(:base_url, ''),
primary_resource_klass: primary_resource_klass,
route_formatter: options.fetch(:route_formatter, JSONAPI.configuration.route_formatter),
url_helpers: options.fetch(:url_helpers, options[:controller]),
)
end
end
end