jsonapi-serializer 2.0.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.
@@ -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