jsonapi-serializers 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,435 @@
1
+ require 'set'
2
+ require 'active_support/inflector'
3
+
4
+ module JSONAPI
5
+ module Serializer
6
+ def self.included(target)
7
+ target.send(:include, InstanceMethods)
8
+ target.extend ClassMethods
9
+ target.class_eval do
10
+ include JSONAPI::Attributes
11
+ end
12
+ end
13
+
14
+ module ClassMethods
15
+ def serialize(object, options = {})
16
+ # Since this is being called on the class directly and not the module, override the
17
+ # serializer option to be the current class.
18
+ options[:serializer] = self
19
+
20
+ JSONAPI::Serializer.serialize(object, options)
21
+ end
22
+ end
23
+
24
+ module InstanceMethods
25
+ attr_accessor :object
26
+ attr_accessor :context
27
+
28
+ def initialize(object, options = {})
29
+ @object = object
30
+ @context = options[:context] || {}
31
+
32
+ # Internal serializer options, not exposed through attr_accessor. No touchie.
33
+ @_include_linkages = options[:include_linkages] || []
34
+ end
35
+
36
+ # Override this to customize the JSON:API "id" for this object.
37
+ # Always return a string from this method to conform with the JSON:API spec.
38
+ def id
39
+ object.id.to_s
40
+ end
41
+
42
+ # Override this to customize the JSON:API "type" for this object.
43
+ # By default, the type is the object's class name lowercased, pluralized, and dasherized,
44
+ # per the spec naming recommendations: http://jsonapi.org/recommendations/#naming
45
+ # For example, 'MyApp::LongCommment' will become the 'long-comments' type.
46
+ def type
47
+ object.class.name.demodulize.tableize.dasherize
48
+ end
49
+
50
+ # Override this to customize how attribute names are formatted.
51
+ # By default, attribute names are dasherized per the spec naming recommendations:
52
+ # http://jsonapi.org/recommendations/#naming
53
+ def format_name(attribute_name)
54
+ attribute_name.to_s.dasherize
55
+ end
56
+
57
+ # The opposite of format_name. Override this if you override format_name.
58
+ def unformat_name(attribute_name)
59
+ attribute_name.to_s.underscore
60
+ end
61
+
62
+ # Override this to provide resource-object metadata.
63
+ # http://jsonapi.org/format/#document-structure-resource-objects
64
+ def meta
65
+ end
66
+
67
+ def self_link
68
+ "/#{type}/#{id}"
69
+ end
70
+
71
+ def relationship_self_link(attribute_name)
72
+ "#{self_link}/links/#{format_name(attribute_name)}"
73
+ end
74
+
75
+ def relationship_related_link(attribute_name)
76
+ "#{self_link}/#{format_name(attribute_name)}"
77
+ end
78
+
79
+ def links
80
+ data = {}
81
+ data.merge!({'self' => self_link}) if self_link
82
+
83
+ # Merge in data for has_one relationships.
84
+ has_one_relationships.each do |attribute_name, object|
85
+ formatted_attribute_name = format_name(attribute_name)
86
+ data[formatted_attribute_name] = {
87
+ 'self' => relationship_self_link(attribute_name),
88
+ 'related' => relationship_related_link(attribute_name),
89
+ }
90
+ if @_include_linkages.include?(formatted_attribute_name)
91
+ if object.nil?
92
+ # Spec: Resource linkage MUST be represented as one of the following:
93
+ # - null for empty to-one relationships.
94
+ # http://jsonapi.org/format/#document-structure-resource-relationships
95
+ data[formatted_attribute_name].merge!({'linkage' => nil})
96
+ else
97
+ related_object_serializer = JSONAPI::Serializer.find_serializer(object)
98
+ data[formatted_attribute_name].merge!({
99
+ 'linkage' => {
100
+ 'type' => related_object_serializer.type.to_s,
101
+ 'id' => related_object_serializer.id.to_s,
102
+ },
103
+ })
104
+ end
105
+ end
106
+ end
107
+
108
+ # Merge in data for has_many relationships.
109
+ has_many_relationships.each do |attribute_name, objects|
110
+ formatted_attribute_name = format_name(attribute_name)
111
+ data[formatted_attribute_name] = {
112
+ 'self' => relationship_self_link(attribute_name),
113
+ 'related' => relationship_related_link(attribute_name),
114
+ }
115
+ # Spec: Resource linkage MUST be represented as one of the following:
116
+ # - an empty array ([]) for empty to-many relationships.
117
+ # - an array of linkage objects for non-empty to-many relationships.
118
+ # http://jsonapi.org/format/#document-structure-resource-relationships
119
+ if @_include_linkages.include?(formatted_attribute_name)
120
+ data[formatted_attribute_name].merge!({'linkage' => []})
121
+ objects = objects || []
122
+ objects.each do |obj|
123
+ related_object_serializer = JSONAPI::Serializer.find_serializer(obj)
124
+ data[formatted_attribute_name]['linkage'] << {
125
+ 'type' => related_object_serializer.type.to_s,
126
+ 'id' => related_object_serializer.id.to_s,
127
+ }
128
+ end
129
+ end
130
+ end
131
+ data
132
+ end
133
+
134
+ def attributes
135
+ attributes = {}
136
+ self.class.attributes_map.each do |attribute_name, attr_data|
137
+ next if !should_include_attr?(attr_data[:options][:if], attr_data[:options][:unless])
138
+ value = evaluate_attr_or_block(attribute_name, attr_data[:attr_or_block])
139
+ attributes[format_name(attribute_name)] = value
140
+ end
141
+ attributes
142
+ end
143
+
144
+ def has_one_relationships
145
+ return {} if self.class.to_one_associations.nil?
146
+ data = {}
147
+ self.class.to_one_associations.each do |attribute_name, attr_data|
148
+ next if !should_include_attr?(attr_data[:options][:if], attr_data[:options][:unless])
149
+ data[attribute_name] = evaluate_attr_or_block(attribute_name, attr_data[:attr_or_block])
150
+ end
151
+ data
152
+ end
153
+
154
+ def has_many_relationships
155
+ return {} if self.class.to_many_associations.nil?
156
+ data = {}
157
+ self.class.to_many_associations.each do |attribute_name, attr_data|
158
+ next if !should_include_attr?(attr_data[:options][:if], attr_data[:options][:unless])
159
+ data[attribute_name] = evaluate_attr_or_block(attribute_name, attr_data[:attr_or_block])
160
+ end
161
+ data
162
+ end
163
+
164
+ def should_include_attr?(if_method_name, unless_method_name)
165
+ # Allow "if: :show_title?" and "unless: :hide_title?" attribute options.
166
+ show_attr = true
167
+ show_attr &&= send(if_method_name) if if_method_name
168
+ show_attr &&= !send(unless_method_name) if unless_method_name
169
+ show_attr
170
+ end
171
+ protected :should_include_attr?
172
+
173
+ def evaluate_attr_or_block(attribute_name, attr_or_block)
174
+ if attr_or_block.is_a?(Proc)
175
+ # A custom block was given, call it to get the value.
176
+ instance_eval(&attr_or_block)
177
+ else
178
+ # Default behavior, call a method by the name of the attribute.
179
+ object.send(attr_or_block)
180
+ end
181
+ end
182
+ protected :evaluate_attr_or_block
183
+ end
184
+
185
+ def self.find_serializer_class_name(object)
186
+ "#{object.class.name}Serializer"
187
+ end
188
+
189
+ def self.find_serializer_class(object)
190
+ class_name = find_serializer_class_name(object)
191
+ class_name.constantize
192
+ end
193
+
194
+ def self.find_serializer(object)
195
+ find_serializer_class(object).new(object)
196
+ end
197
+
198
+ def self.serialize(objects, options = {})
199
+ # Normalize option strings to symbols.
200
+ options[:is_collection] = options.delete('is_collection') || options[:is_collection] || false
201
+ options[:include] = options.delete('include') || options[:include]
202
+ options[:serializer] = options.delete('serializer') || options[:serializer]
203
+ options[:context] = options.delete('context') || options[:context] || {}
204
+
205
+ # Normalize includes.
206
+ includes = options[:include]
207
+ includes = (includes.is_a?(String) ? includes.split(',') : includes).uniq if includes
208
+
209
+ # An internal-only structure that is passed through serializers as they are created.
210
+ passthrough_options = {
211
+ context: options[:context],
212
+ serializer: options[:serializer],
213
+ include: includes
214
+ }
215
+
216
+ if options[:is_collection] && !objects.respond_to?(:each)
217
+ raise JSONAPI::Serializer::AmbiguousCollectionError.new(
218
+ 'Attempted to serialize a single object as a collection.')
219
+ end
220
+
221
+ # Automatically include linkage data for any relation that is also included.
222
+ if includes
223
+ direct_children_includes = includes.reject { |key| key.include?('.') }
224
+ passthrough_options[:include_linkages] = direct_children_includes
225
+ end
226
+
227
+ # Spec: Primary data MUST be either:
228
+ # - a single resource object or null, for requests that target single resources.
229
+ # - an array of resource objects or an empty array ([]), for resource collections.
230
+ # http://jsonapi.org/format/#document-structure-top-level
231
+ if options[:is_collection] && !objects.any?
232
+ primary_data = []
233
+ elsif !options[:is_collection] && objects.nil?
234
+ primary_data = nil
235
+ elsif options[:is_collection]
236
+ # Have object collection.
237
+ passthrough_options[:serializer] ||= find_serializer_class(objects.first)
238
+ primary_data = serialize_primary_multi(objects, passthrough_options)
239
+ else
240
+ # Duck-typing check for a collection being passed without is_collection true.
241
+ # We always must be told if serializing a collection because the JSON:API spec distinguishes
242
+ # how to serialize null single resources vs. empty collections.
243
+ if objects.respond_to?(:each)
244
+ raise JSONAPI::Serializer::AmbiguousCollectionError.new(
245
+ 'Must provide `is_collection: true` to `serialize` when serializing collections.')
246
+ end
247
+ # Have single object.
248
+ passthrough_options[:serializer] ||= find_serializer_class(objects)
249
+ primary_data = serialize_primary(objects, passthrough_options)
250
+ end
251
+ result = {
252
+ 'data' => primary_data,
253
+ }
254
+
255
+ # If 'include' relationships are given, recursively find and include each object.
256
+ if includes
257
+ relationship_data = {}
258
+ inclusion_tree = parse_relationship_paths(includes)
259
+
260
+ # Given all the primary objects (either the single root object or collection of objects),
261
+ # recursively search and find related associations that were specified as includes.
262
+ objects = options[:is_collection] ? objects.to_a : [objects]
263
+ serializers = []
264
+ objects.compact.each do |obj|
265
+ # Use the mutability of relationship_data as the return datastructure to take advantage
266
+ # of the internal special merging logic.
267
+ find_recursive_relationships(obj, inclusion_tree, relationship_data)
268
+ end
269
+
270
+ result['included'] = relationship_data.map do |_, data|
271
+ passthrough_options = {}
272
+ passthrough_options[:serializer] = find_serializer_class(data[:object])
273
+ passthrough_options[:include_linkages] = data[:include_linkages]
274
+ serialize_primary(data[:object], passthrough_options)
275
+ end
276
+ end
277
+ result
278
+ end
279
+
280
+ def self.serialize_primary(object, options = {})
281
+ serializer_class = options.fetch(:serializer)
282
+
283
+ # Spec: Primary data MUST be either:
284
+ # - a single resource object or null, for requests that target single resources.
285
+ # http://jsonapi.org/format/#document-structure-top-level
286
+ return if object.nil?
287
+
288
+ serializer = serializer_class.new(object, options)
289
+ data = {
290
+ 'id' => serializer.id.to_s,
291
+ 'type' => serializer.type.to_s,
292
+ 'attributes' => serializer.attributes,
293
+ }
294
+
295
+ # Merge in optional top-level members if they are non-nil.
296
+ # http://jsonapi.org/format/#document-structure-resource-objects
297
+ data.merge!({'attributes' => serializer.attributes}) if !serializer.attributes.nil?
298
+ data.merge!({'links' => serializer.links}) if !serializer.links.nil?
299
+ data.merge!({'meta' => serializer.meta}) if !serializer.meta.nil?
300
+ data
301
+ end
302
+ class << self; protected :serialize_primary; end
303
+
304
+ def self.serialize_primary_multi(objects, options = {})
305
+ # Spec: Primary data MUST be either:
306
+ # - an array of resource objects or an empty array ([]), for resource collections.
307
+ # http://jsonapi.org/format/#document-structure-top-level
308
+ return [] if !objects.any?
309
+
310
+ objects.map { |obj| serialize_primary(obj, options) }
311
+ end
312
+ class << self; protected :serialize_primary_multi; end
313
+
314
+ # Recursively find object relationships and returns a tree of related objects.
315
+ # Example return:
316
+ # {
317
+ # ['comments', '1'] => {object: <Comment>, include_linkages: ['author']},
318
+ # ['users', '1'] => {object: <User>, include_linkages: []},
319
+ # ['users', '2'] => {object: <User>, include_linkages: []},
320
+ # }
321
+ def self.find_recursive_relationships(root_object, root_inclusion_tree, results)
322
+ root_inclusion_tree.each do |attribute_name, child_inclusion_tree|
323
+ # Skip the sentinal value, but we need to preserve it for siblings.
324
+ next if attribute_name == :_include
325
+
326
+ serializer = JSONAPI::Serializer.find_serializer(root_object)
327
+ unformatted_attr_name = serializer.unformat_name(attribute_name).to_sym
328
+
329
+ # We know the name of this relationship, but we don't know where it is stored internally.
330
+ # Check if it is a has_one or has_many relationship.
331
+ object = nil
332
+ is_collection = false
333
+ is_valid_attr = false
334
+ if serializer.has_one_relationships.has_key?(unformatted_attr_name)
335
+ is_valid_attr = true
336
+ object = serializer.has_one_relationships[unformatted_attr_name]
337
+ elsif serializer.has_many_relationships.has_key?(unformatted_attr_name)
338
+ is_valid_attr = true
339
+ is_collection = true
340
+ object = serializer.has_many_relationships[unformatted_attr_name]
341
+ end
342
+ if !is_valid_attr
343
+ raise JSONAPI::Serializer::InvalidIncludeError.new(
344
+ "'#{attribute_name}' is not a valid include.")
345
+ end
346
+
347
+ # We're finding relationships for compound documents, so skip anything that doesn't exist.
348
+ next if object.nil?
349
+
350
+ # We only include parent values if the sential value _include is set. This satifies the
351
+ # spec note: A request for comments.author should not automatically also include comments
352
+ # in the response. This can happen if the client already has the comments locally, and now
353
+ # wants to fetch the associated authors without fetching the comments again.
354
+ # http://jsonapi.org/format/#fetching-includes
355
+ objects = is_collection ? object : [object]
356
+ if child_inclusion_tree[:_include] == true
357
+ # Include the current level objects if the _include attribute exists.
358
+ # If it is not set, that indicates that this is an inner path and not a leaf and will
359
+ # be followed by the recursion below.
360
+ objects.each do |obj|
361
+ obj_serializer = JSONAPI::Serializer.find_serializer(obj)
362
+ # Use keys of ['posts', '1'] for the results to enforce uniqueness.
363
+ # Spec: A compound document MUST NOT include more than one resource object for each
364
+ # type and id pair.
365
+ # http://jsonapi.org/format/#document-structure-compound-documents
366
+ key = [obj_serializer.type, obj_serializer.id]
367
+
368
+ # This is special: we know at this level if a child of this parent will also been
369
+ # included in the compound document, so we can compute exactly what linkages should
370
+ # be included by the object at this level. This satisfies this part of the spec:
371
+ #
372
+ # Spec: Resource linkage in a compound document allows a client to link together
373
+ # all of the included resource objects without having to GET any relationship URLs.
374
+ # http://jsonapi.org/format/#document-structure-resource-relationships
375
+ current_child_includes = []
376
+ inclusion_names = child_inclusion_tree.keys.reject { |k| k == :_include }
377
+ inclusion_names.each do |inclusion_name|
378
+ if child_inclusion_tree[inclusion_name][:_include]
379
+ current_child_includes << inclusion_name
380
+ end
381
+ end
382
+
383
+ # Special merge: we might see this object multiple times in the course of recursion,
384
+ # so merge the include_linkages each time we see it to load all the relevant linkages.
385
+ current_child_includes += results[key] && results[key][:include_linkages] || []
386
+ current_child_includes.uniq!
387
+ results[key] = {object: obj, include_linkages: current_child_includes}
388
+ end
389
+ end
390
+
391
+ # Recurse deeper!
392
+ if !child_inclusion_tree.empty?
393
+ # For each object we just loaded, find all deeper recursive relationships.
394
+ objects.each do |obj|
395
+ find_recursive_relationships(obj, child_inclusion_tree, results)
396
+ end
397
+ end
398
+ end
399
+ nil
400
+ end
401
+ class << self; protected :find_recursive_relationships; end
402
+
403
+ # Takes a list of relationship paths and returns a hash as deep as the given paths.
404
+ # The _include: true is a sentinal value that specifies whether the parent level should
405
+ # be included.
406
+ #
407
+ # Example:
408
+ # Given: ['author', 'comments', 'comments.user']
409
+ # Returns: {
410
+ # 'author' => {_include: true},
411
+ # 'comments' => {_include: true, 'user' => {_include: true}},
412
+ # }
413
+ def self.parse_relationship_paths(paths)
414
+ relationships = {}
415
+ paths.each { |path| merge_relationship_path(path, relationships) }
416
+ relationships
417
+ end
418
+ class << self; protected :parse_relationship_paths; end
419
+
420
+ def self.merge_relationship_path(path, data)
421
+ parts = path.split('.', 2)
422
+ current_level = parts[0].strip
423
+ data[current_level] ||= {}
424
+
425
+ if parts.length == 1
426
+ # Leaf node.
427
+ data[current_level].merge!({_include: true})
428
+ elsif parts.length == 2
429
+ # Need to recurse more.
430
+ merge_relationship_path(parts[1], data[current_level])
431
+ end
432
+ end
433
+ class << self; protected :merge_relationship_path; end
434
+ end
435
+ end
@@ -0,0 +1,5 @@
1
+ module JSONAPI
2
+ module Serializer
3
+ VERSION = '0.1.0'
4
+ end
5
+ end
@@ -0,0 +1,11 @@
1
+ require "jsonapi-serializers/version"
2
+ require "jsonapi-serializers/attributes"
3
+ require "jsonapi-serializers/serializer"
4
+
5
+ module JSONAPI
6
+ module Serializer
7
+ class Error < Exception; end
8
+ class AmbiguousCollectionError < Error; end
9
+ class InvalidIncludeError < Error; end
10
+ end
11
+ end