jsonapi-serializer 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ ::ActiveRecord::Associations::Builder::HasOne.class_eval do
4
+ # Based on
5
+ # https://github.com/rails/rails/blob/master/activerecord/lib/active_record/associations/builder/collection_association.rb#L50
6
+ # https://github.com/rails/rails/blob/master/activerecord/lib/active_record/associations/builder/singular_association.rb#L11
7
+ def self.define_accessors(mixin, reflection)
8
+ super
9
+ name = reflection.name
10
+ mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
11
+ def #{name}_id
12
+ # if an attribute is already defined with this methods name we should just use it
13
+ return read_attribute(__method__) if has_attribute?(__method__)
14
+ association(:#{name}).reader.try(:id)
15
+ end
16
+ CODE
17
+ end
18
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FastJsonapi
4
+ require 'fast_jsonapi/object_serializer'
5
+ if defined?(::Rails)
6
+ require 'fast_jsonapi/railtie'
7
+ elsif defined?(::ActiveRecord)
8
+ require 'extensions/has_one'
9
+ end
10
+ end
@@ -0,0 +1,5 @@
1
+ require 'fast_jsonapi/scalar'
2
+
3
+ module FastJsonapi
4
+ class Attribute < Scalar; end
5
+ end
@@ -0,0 +1,12 @@
1
+ module FastJsonapi
2
+ class << self
3
+ # Calls either a Proc or a Lambda, making sure to never pass more parameters to it than it can receive
4
+ #
5
+ # @param [Proc] proc the Proc or Lambda to call
6
+ # @param [Array<Object>] *params any number of parameters to be passed to the Proc
7
+ # @return [Object] the result of the Proc call with the supplied parameters
8
+ def call_proc(proc, *params)
9
+ proc.call(*params.take(proc.parameters.length))
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,2 @@
1
+ require 'fast_jsonapi/instrumentation/serializable_hash'
2
+ require 'fast_jsonapi/instrumentation/serialized_json'
@@ -0,0 +1,13 @@
1
+ require 'active_support/notifications'
2
+
3
+ module FastJsonapi
4
+ module ObjectSerializer
5
+ alias serializable_hash_without_instrumentation serializable_hash
6
+
7
+ def serializable_hash
8
+ ActiveSupport::Notifications.instrument(SERIALIZABLE_HASH_NOTIFICATION, { name: self.class.name }) do
9
+ serializable_hash_without_instrumentation
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ require 'active_support/notifications'
2
+
3
+ module FastJsonapi
4
+ module ObjectSerializer
5
+ alias serialized_json_without_instrumentation serialized_json
6
+
7
+ def serialized_json
8
+ ActiveSupport::Notifications.instrument(SERIALIZED_JSON_NOTIFICATION, { name: self.class.name }) do
9
+ serialized_json_without_instrumentation
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,2 @@
1
+ require 'fast_jsonapi/instrumentation/skylight/normalizers/serializable_hash'
2
+ require 'fast_jsonapi/instrumentation/skylight/normalizers/serialized_json'
@@ -0,0 +1,7 @@
1
+ require 'skylight'
2
+
3
+ SKYLIGHT_NORMALIZER_BASE_CLASS = begin
4
+ ::Skylight::Core::Normalizers::Normalizer
5
+ rescue NameError
6
+ ::Skylight::Normalizers::Normalizer
7
+ end
@@ -0,0 +1,20 @@
1
+ require 'fast_jsonapi/instrumentation/skylight/normalizers/base'
2
+ require 'fast_jsonapi/instrumentation/serializable_hash'
3
+
4
+ module FastJsonapi
5
+ module Instrumentation
6
+ module Skylight
7
+ module Normalizers
8
+ class SerializableHash < SKYLIGHT_NORMALIZER_BASE_CLASS
9
+ register FastJsonapi::ObjectSerializer::SERIALIZABLE_HASH_NOTIFICATION
10
+
11
+ CAT = "view.#{FastJsonapi::ObjectSerializer::SERIALIZABLE_HASH_NOTIFICATION}".freeze
12
+
13
+ def normalize(_trace, _name, payload)
14
+ [CAT, payload[:name], nil]
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,20 @@
1
+ require 'fast_jsonapi/instrumentation/skylight/normalizers/base'
2
+ require 'fast_jsonapi/instrumentation/serializable_hash'
3
+
4
+ module FastJsonapi
5
+ module Instrumentation
6
+ module Skylight
7
+ module Normalizers
8
+ class SerializedJson < SKYLIGHT_NORMALIZER_BASE_CLASS
9
+ register FastJsonapi::ObjectSerializer::SERIALIZED_JSON_NOTIFICATION
10
+
11
+ CAT = "view.#{FastJsonapi::ObjectSerializer::SERIALIZED_JSON_NOTIFICATION}".freeze
12
+
13
+ def normalize(_trace, _name, payload)
14
+ [CAT, payload[:name], nil]
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,5 @@
1
+ require 'fast_jsonapi/scalar'
2
+
3
+ module FastJsonapi
4
+ class Link < Scalar; end
5
+ end
@@ -0,0 +1,358 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/time'
4
+ require 'active_support/concern'
5
+ require 'active_support/inflector'
6
+ require 'active_support/core_ext/numeric/time'
7
+ require 'fast_jsonapi/helpers'
8
+ require 'fast_jsonapi/attribute'
9
+ require 'fast_jsonapi/relationship'
10
+ require 'fast_jsonapi/link'
11
+ require 'fast_jsonapi/serialization_core'
12
+
13
+ module FastJsonapi
14
+ module ObjectSerializer
15
+ extend ActiveSupport::Concern
16
+ include SerializationCore
17
+
18
+ SERIALIZABLE_HASH_NOTIFICATION = 'render.fast_jsonapi.serializable_hash'
19
+ SERIALIZED_JSON_NOTIFICATION = 'render.fast_jsonapi.serialized_json'
20
+ TRANSFORMS_MAPPING = {
21
+ camel: :camelize,
22
+ camel_lower: [:camelize, :lower],
23
+ dash: :dasherize,
24
+ underscore: :underscore
25
+ }.freeze
26
+
27
+ included do
28
+ # Set record_type based on the name of the serializer class
29
+ set_type(reflected_record_type) if reflected_record_type
30
+ end
31
+
32
+ def initialize(resource, options = {})
33
+ process_options(options)
34
+
35
+ @resource = resource
36
+ end
37
+
38
+ def serializable_hash
39
+ return hash_for_collection if is_collection?(@resource, @is_collection)
40
+
41
+ hash_for_one_record
42
+ end
43
+ alias to_hash serializable_hash
44
+
45
+ def hash_for_one_record
46
+ serializable_hash = { data: nil }
47
+ serializable_hash[:meta] = @meta if @meta.present?
48
+ serializable_hash[:links] = @links if @links.present?
49
+
50
+ return serializable_hash unless @resource
51
+
52
+ serializable_hash[:data] = self.class.record_hash(@resource, @fieldsets[self.class.record_type.to_sym], @includes, @params)
53
+ serializable_hash[:included] = self.class.get_included_records(@resource, @includes, @known_included_objects, @fieldsets, @params) if @includes.present?
54
+ serializable_hash
55
+ end
56
+
57
+ def hash_for_collection
58
+ serializable_hash = {}
59
+
60
+ data = []
61
+ included = []
62
+ fieldset = @fieldsets[self.class.record_type.to_sym]
63
+ @resource.each do |record|
64
+ data << self.class.record_hash(record, fieldset, @includes, @params)
65
+ included.concat self.class.get_included_records(record, @includes, @known_included_objects, @fieldsets, @params) if @includes.present?
66
+ end
67
+
68
+ serializable_hash[:data] = data
69
+ serializable_hash[:included] = included if @includes.present?
70
+ serializable_hash[:meta] = @meta if @meta.present?
71
+ serializable_hash[:links] = @links if @links.present?
72
+ serializable_hash
73
+ end
74
+
75
+ private
76
+
77
+ def process_options(options)
78
+ @fieldsets = deep_symbolize(options[:fields].presence || {})
79
+ @params = {}
80
+
81
+ return if options.blank?
82
+
83
+ @known_included_objects = {}
84
+ @meta = options[:meta]
85
+ @links = options[:links]
86
+ @is_collection = options[:is_collection]
87
+ @params = options[:params] || {}
88
+ raise ArgumentError, '`params` option passed to serializer must be a hash' unless @params.is_a?(Hash)
89
+
90
+ if options[:include].present?
91
+ @includes = options[:include].reject(&:blank?).map(&:to_sym)
92
+ self.class.validate_includes!(@includes)
93
+ end
94
+ end
95
+
96
+ def deep_symbolize(collection)
97
+ if collection.is_a? Hash
98
+ collection.each_with_object({}) do |(k, v), hsh|
99
+ hsh[k.to_sym] = deep_symbolize(v)
100
+ end
101
+ elsif collection.is_a? Array
102
+ collection.map { |i| deep_symbolize(i) }
103
+ else
104
+ collection.to_sym
105
+ end
106
+ end
107
+
108
+ def is_collection?(resource, force_is_collection = nil)
109
+ return force_is_collection unless force_is_collection.nil?
110
+
111
+ resource.respond_to?(:each) && !resource.respond_to?(:each_pair)
112
+ end
113
+
114
+ class_methods do
115
+ def inherited(subclass)
116
+ super(subclass)
117
+ subclass.attributes_to_serialize = attributes_to_serialize.dup if attributes_to_serialize.present?
118
+ subclass.relationships_to_serialize = relationships_to_serialize.dup if relationships_to_serialize.present?
119
+ subclass.cachable_relationships_to_serialize = cachable_relationships_to_serialize.dup if cachable_relationships_to_serialize.present?
120
+ subclass.uncachable_relationships_to_serialize = uncachable_relationships_to_serialize.dup if uncachable_relationships_to_serialize.present?
121
+ subclass.transform_method = transform_method
122
+ subclass.data_links = data_links.dup if data_links.present?
123
+ subclass.cache_store_instance = cache_store_instance
124
+ subclass.cache_store_options = cache_store_options
125
+ subclass.set_type(subclass.reflected_record_type) if subclass.reflected_record_type
126
+ subclass.meta_to_serialize = meta_to_serialize
127
+ subclass.record_id = record_id
128
+ end
129
+
130
+ def reflected_record_type
131
+ return @reflected_record_type if defined?(@reflected_record_type)
132
+
133
+ @reflected_record_type ||= begin
134
+ name.split('::').last.chomp('Serializer').underscore.to_sym if name&.end_with?('Serializer')
135
+ end
136
+ end
137
+
138
+ def set_key_transform(transform_name)
139
+ self.transform_method = TRANSFORMS_MAPPING[transform_name.to_sym]
140
+
141
+ # ensure that the record type is correctly transformed
142
+ if record_type
143
+ set_type(record_type)
144
+ # TODO: Remove dead code
145
+ elsif reflected_record_type
146
+ set_type(reflected_record_type)
147
+ end
148
+ end
149
+
150
+ def run_key_transform(input)
151
+ if transform_method.present?
152
+ input.to_s.send(*@transform_method).to_sym
153
+ else
154
+ input.to_sym
155
+ end
156
+ end
157
+
158
+ def use_hyphen
159
+ warn('DEPRECATION WARNING: use_hyphen is deprecated and will be removed from fast_jsonapi 2.0 use (set_key_transform :dash) instead')
160
+ set_key_transform :dash
161
+ end
162
+
163
+ def set_type(type_name)
164
+ self.record_type = run_key_transform(type_name)
165
+ end
166
+
167
+ def set_id(id_name = nil, &block)
168
+ self.record_id = block || id_name
169
+ end
170
+
171
+ def cache_options(cache_options)
172
+ # FIXME: remove this if block once deprecated cache_options are not supported anymore
173
+ unless cache_options.key?(:store)
174
+ # fall back to old, deprecated behaviour because no store was passed.
175
+ # we assume the user explicitly wants new behaviour if he passed a
176
+ # store because this is the new syntax.
177
+ deprecated_cache_options(cache_options)
178
+ return
179
+ end
180
+
181
+ self.cache_store_instance = cache_options[:store]
182
+ self.cache_store_options = cache_options.except(:store)
183
+ end
184
+
185
+ # FIXME: remove this method once deprecated cache_options are not supported anymore
186
+ def deprecated_cache_options(cache_options)
187
+ warn('DEPRECATION WARNING: `store:` is a required cache option, we will default to `Rails.cache` for now. See https://github.com/fast-jsonapi/fast_jsonapi#caching for more information.')
188
+
189
+ %i[enabled cache_length].select { |key| cache_options.key?(key) }.each do |key|
190
+ warn("DEPRECATION WARNING: `#{key}` is a deprecated cache option and will have no effect soon. See https://github.com/fast-jsonapi/fast_jsonapi#caching for more information.")
191
+ end
192
+
193
+ self.cache_store_instance = cache_options[:enabled] ? Rails.cache : nil
194
+ self.cache_store_options = {
195
+ expires_in: cache_options[:cache_length] || 5.minutes,
196
+ race_condition_ttl: cache_options[:race_condition_ttl] || 5.seconds
197
+ }
198
+ end
199
+
200
+ def attributes(*attributes_list, &block)
201
+ attributes_list = attributes_list.first if attributes_list.first.class.is_a?(Array)
202
+ options = attributes_list.last.is_a?(Hash) ? attributes_list.pop : {}
203
+ self.attributes_to_serialize = {} if attributes_to_serialize.nil?
204
+
205
+ # to support calling `attribute` with a lambda, e.g `attribute :key, ->(object) { ... }`
206
+ block = attributes_list.pop if attributes_list.last.is_a?(Proc)
207
+
208
+ attributes_list.each do |attr_name|
209
+ method_name = attr_name
210
+ key = run_key_transform(method_name)
211
+ attributes_to_serialize[key] = Attribute.new(
212
+ key: key,
213
+ method: block || method_name,
214
+ options: options
215
+ )
216
+ end
217
+ end
218
+
219
+ alias_method :attribute, :attributes
220
+
221
+ def add_relationship(relationship)
222
+ self.relationships_to_serialize = {} if relationships_to_serialize.nil?
223
+ self.cachable_relationships_to_serialize = {} if cachable_relationships_to_serialize.nil?
224
+ self.uncachable_relationships_to_serialize = {} if uncachable_relationships_to_serialize.nil?
225
+
226
+ # TODO: Remove this undocumented option.
227
+ # Delegate the caching to the serializer exclusively.
228
+ if !relationship.cached
229
+ uncachable_relationships_to_serialize[relationship.name] = relationship
230
+ else
231
+ cachable_relationships_to_serialize[relationship.name] = relationship
232
+ end
233
+ relationships_to_serialize[relationship.name] = relationship
234
+ end
235
+
236
+ def has_many(relationship_name, options = {}, &block)
237
+ relationship = create_relationship(relationship_name, :has_many, options, block)
238
+ add_relationship(relationship)
239
+ end
240
+
241
+ def has_one(relationship_name, options = {}, &block)
242
+ relationship = create_relationship(relationship_name, :has_one, options, block)
243
+ add_relationship(relationship)
244
+ end
245
+
246
+ def belongs_to(relationship_name, options = {}, &block)
247
+ relationship = create_relationship(relationship_name, :belongs_to, options, block)
248
+ add_relationship(relationship)
249
+ end
250
+
251
+ def meta(meta_name = nil, &block)
252
+ self.meta_to_serialize = block || meta_name
253
+ end
254
+
255
+ def create_relationship(base_key, relationship_type, options, block)
256
+ name = base_key.to_sym
257
+ if relationship_type == :has_many
258
+ base_serialization_key = base_key.to_s.singularize
259
+ id_postfix = '_ids'
260
+ else
261
+ base_serialization_key = base_key
262
+ id_postfix = '_id'
263
+ end
264
+ polymorphic = fetch_polymorphic_option(options)
265
+
266
+ Relationship.new(
267
+ owner: self,
268
+ key: options[:key] || run_key_transform(base_key),
269
+ name: name,
270
+ id_method_name: compute_id_method_name(
271
+ options[:id_method_name],
272
+ "#{base_serialization_key}#{id_postfix}".to_sym,
273
+ polymorphic,
274
+ options[:serializer],
275
+ block
276
+ ),
277
+ record_type: options[:record_type],
278
+ object_method_name: options[:object_method_name] || name,
279
+ object_block: block,
280
+ serializer: options[:serializer],
281
+ relationship_type: relationship_type,
282
+ cached: options[:cached],
283
+ polymorphic: polymorphic,
284
+ conditional_proc: options[:if],
285
+ transform_method: @transform_method,
286
+ links: options[:links],
287
+ lazy_load_data: options[:lazy_load_data]
288
+ )
289
+ end
290
+
291
+ def compute_id_method_name(custom_id_method_name, id_method_name_from_relationship, polymorphic, serializer, block)
292
+ if block.present? || serializer.is_a?(Proc) || polymorphic
293
+ custom_id_method_name || :id
294
+ else
295
+ custom_id_method_name || id_method_name_from_relationship
296
+ end
297
+ end
298
+
299
+ def serializer_for(name)
300
+ namespace = self.name.gsub(/()?\w+Serializer$/, '')
301
+ serializer_name = name.to_s.demodulize.classify + 'Serializer'
302
+ serializer_class_name = namespace + serializer_name
303
+ begin
304
+ serializer_class_name.constantize
305
+ rescue NameError
306
+ raise NameError, "#{self.name} cannot resolve a serializer class for '#{name}'. " \
307
+ "Attempted to find '#{serializer_class_name}'. " \
308
+ 'Consider specifying the serializer directly through options[:serializer].'
309
+ end
310
+ end
311
+
312
+ def fetch_polymorphic_option(options)
313
+ option = options[:polymorphic]
314
+ return false unless option.present?
315
+ return option if option.respond_to? :keys
316
+
317
+ {}
318
+ end
319
+
320
+ # def link(link_name, link_method_name = nil, &block)
321
+ def link(*params, &block)
322
+ self.data_links = {} if data_links.nil?
323
+
324
+ options = params.last.is_a?(Hash) ? params.pop : {}
325
+ link_name = params.first
326
+ link_method_name = params[-1]
327
+ key = run_key_transform(link_name)
328
+
329
+ data_links[key] = Link.new(
330
+ key: key,
331
+ method: block || link_method_name,
332
+ options: options
333
+ )
334
+ end
335
+
336
+ def validate_includes!(includes)
337
+ return if includes.blank?
338
+
339
+ includes.each do |include_item|
340
+ klass = self
341
+ parse_include_item(include_item).each do |parsed_include|
342
+ relationships_to_serialize = klass.relationships_to_serialize || {}
343
+ relationship_to_include = relationships_to_serialize[parsed_include]
344
+ raise ArgumentError, "#{parsed_include} is not specified as a relationship on #{klass.name}" unless relationship_to_include
345
+
346
+ if relationship_to_include.static_serializer
347
+ klass = relationship_to_include.static_serializer
348
+ else
349
+ # the serializer may change based on the object (e.g. polymorphic relationships),
350
+ # so inner relationships cannot be validated
351
+ break
352
+ end
353
+ end
354
+ end
355
+ end
356
+ end
357
+ end
358
+ end